-
-
Notifications
You must be signed in to change notification settings - Fork 14
API Packets
Packets are used to send data, object states, events and RPCs across the network. Each packet can be uniquely defined for its purpose, e.g. when a player connects your mod may send a ClientboundMyModSettingsPacket packet from the server to the new player. Upon receiving the packet, your mod on the client side will validate and adopt the settings.
Servers and clients register for callbacks for each type of packet they expect to receive. All expected packets must have a listener registered.
Caution
If an unregistered packet type is received a de-sync can occur.
The server can:
- Send a packet to an individual client
- Broadcast a packet to all clients
Note
Broadcast packets will be sent to/received by the local client, unless the excludeSelf parameter is set, or the local client's player is specified in the excludePlayer parameter.
Clients can:
- Send a packet to the server
Packets are defined in either a class or a struct and are made up of zero or more public properties.
The properties can be defined as primitive types (and arrays thereof), or as complex types e.g. classes and structs.
Warning
Packets are reused by the system. If a received packet and its data are not going to be consumed immediately (e.g. passed to a coroutine), clone the packet or copy the data prior to returning from the callback.
To maintain clarity and consistency, it's recommended to follow these naming conventions for your packets:
- Prefix the packet class name with the direction
-
ClientBound- Packets sent from the server to one or more clients (e.g.ClientBoundMyModSettingsPacket) -
ServerBound- Packets sent from a client to the server (e.g.ServerBoundMyModActionPacket) -
Common- Packets that can be sent in either direction (e.g.CommonMyModWidgetInteractionPacket)
-
- Include an identifier for your mod, e.g.
PJfor 'Passenger Jobs' mod. - Describe the packet's data
WidgetCreation - Suffix all packet class names with
Packetfor easy identification.
The Multiplayer API provides two interface classes for defining your custom packets, IPacket for simple packets using primitive data types (or arrays of primitives) and ISerializablePacket for complex packets.
Packets inheriting from IPacket will automatically be serialised and deserialised, while packets inheriting from ISerializablePacket require you to provide your own serialisation and deserialisation routines.
To define a simple packet, inherit from the IPacket class and add public properties to represent all of the variables that need to be exchanged.
Acceptable data types:
- Primitives: bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, char
- Built-in classes: IPEndPoint, Guid
- Arrays of primitives and built-in classes
- Enums derived from primitives e.g.
enum MyEnum : byte - UnityEngine classes: Vector2, Vector3, Quaternion
Packets based on IPacket can not contain complex data types (e.g. lists, dictionaries, etc.), nor can they contain custom classes or structs.
IPackets do not support null values. If null values need to be included in your packet, use ISerializablePacket instead.
Example packet:
using MPAPI.Interfaces.Packets;
namespace MyMod.Packets;
public class CommonMyModWidgetInteractionPacket : IPacket
{
public uint WidgetNetId {get; set;}
public bool WidgetPowered {get; set;}
public float[] Values {get; set;}
}Packets can contain helper methods; these will be ignored by the automatic serialiser/deserialiser.
Example packet:
using MPAPI.Interfaces.Packets;
namespace MyMod.Packets;
public class CommonMyModWidgetInteractionPacket : IPacket
{
public uint WidgetNetId {get; set;}
public bool WidgetPowered {get; set;}
public float[] Values {get; set;}
public static CommonMyModWidgetInteractionPacket From(MyModWidget widget)
{
return new()
{
WidgetNetId = widget.GetNetId(),
WidgetPowered = widget.IsPowered,
Values = widget.GetSimulationValues().ToArray()
};
}
}Packets based on ISerializablePacket are much more flexible than those based on IPacket, however the flexibility comes at a cost; you are required to implement custom serialisation and deserialisation routines. Automatic serialisation is not available.
When writing custom serialisers and deserialisers the process should be treated as writing and reading from a Queue, with all elements being read in the same order they were written. It is important that the number of bytes written and number of bytes read is always equal.
Caution
It is critical that the serialisation and deserialisation is balanced and completed in the same order, otherwise data corruption and de-syncs are likely to occur.
Example packet:
using MPAPI.Interfaces.Packets;
using MPAPI.Util;
using System.Collections.Generic;
using System.IO;
namespace MyMod.Packets;
public class ClientBoundMyModWidgetSpawnPacket : ISerializablePacket
{
public ushort WidgetNetId {get; set;}
public bool WidgetPowered {get; set;}
public Vector3? Position {get; set;}
public Quaternion? Rotation {get; set;}
public Dictionary<uint, bool>? States {get; set;}
void ISerializablePacket.Serialize(BinaryWriter writer)
{
// Serialise NetId
writer.Write(WidgetNetId);
// Serialise Powered state
writer.Write(WidgetPowered);
// Serialise state of Position - write 'true' if a value exists, write 'false' if Position is null
writer.Write(Position != null);
// Use API utility to write the Position value, if it exists
if (Position != null)
writer.WriteVector3((Vector3)Position);
// Serialise state of Rotation - write 'true' if a value exists, write 'false' if Rotation is null
writer.Write(Rotation != null);
// Use API utility to write the Rotation value, if it exists
if (Rotation != null)
writer.WriteQuaternion((Quaternion)Rotation);
// Serialise States
writer.Write(States?.Count ?? 0);
if (States != null)
foreach (var kvp in States)
{
writer.Write(kvp.Key);
writer.Write(kvp.Value);
}
}
void ISerializablePacket.Deserialize(BinaryReader reader)
{
// Deserialise NetId
WidgetNetId = reader.ReadUInt16();
// Deserialise Powered state
WidgetPowered = reader.ReadBoolean();
// Check if Position is available
if (reader.ReadBoolean())
Position = reader.ReadVector3();
// Check if Rotation is available
if (reader.ReadBoolean())
Rotation = reader.ReadQuaternion();
// Deserialise States
var statesCount = reader.ReadInt32();
if (statesCount > 0)
{
States = new Dictionary<uint, bool>(statesCount);
for (var i = 0; i < statesCount; i++)
{
var key = reader.ReadUInt32();
var value = reader.ReadBoolean();
States[key] = value;
}
}
}
}Before you can receive packets, you must register a listener (callback) for each packet type your mod expects to receive. Registration can only occur after the IServer and/or IClient instances have been created.
Important
Packet listeners should be registered during OnServerStart or OnClientStart events to ensure they are ready before any packets are received. Failing to register a listener for an expected packet type can cause de-syncs.
Packets implementing IPacket are registered using RegisterPacket(), while packets implementing ISerializablePacket are registered using RegisterSerializablePacket().
The callback signature is different between server and client callbacks due to the server needing to know the packet source.
Example of registering an IPacket callback on the server:
internal class MyModServer : MonoBehaviour
{
IServer server;
const float MAX_WIDGET_DIST_SQR = 10 * 10;
protected void Awake()
{
// Retrieve the current IServer instance
server = MultiplayerAPI.Server;
// Subscribe to network packets
server.RegisterPacket<CommonMyModWidgetInteractionPacket>(OnWidgetInteractionPacket);
}
private void OnWidgetInteractionPacket(CommonMyModWidgetInteractionPacket packet, IPlayer player)
{
// Validate the packet
if (packet.WidgetNetId == 0)
{
Debug.Log($"Received {packet.GetType()}, but WidgetNetId is 0");
return;
}
if (!MyModWidget.TryGetFromNetId(packet.WidgetNetId, out MyModWidget widget))
{
Debug.Log($"Received {packet.GetType()}, but Widget with NetId {packet.WidgetNetId} was not found");
return;
}
if (player.Position - widget.transform.position).sqrMagnitude > MAX_WIDGET_DIST_SQR){
Debug.Log($"Received {packet.GetType()}, but player {player.Username} is too far from {widget.Name}");
return;
}
widget.SetPowered(packet.WidgetPowered);
widget.UpdateSimValues(packet.Values);
}
}Example of registering a IPacket and ISerializablePacket callbacks on the client:
internal class MyModClient : MonoBehaviour
{
IClient client;
protected void Awake()
{
// Retrieve the current IClient instance
client = MultiplayerAPI.Client;
// Subscribe to network packets
client.RegisterPacket<CommonMyModWidgetInteractionPacket>(OnWidgetInteractionPacket);
client.RegisterSerializablePacket<ClientBoundMyModWidgetSpawnPacket>(WidgetSpawnPacket);
}
private void OnWidgetInteractionPacket(CommonMyModWidgetInteractionPacket packet)
{
// Validate the packet
if (packet.WidgetNetId == 0)
{
Debug.Log($"Client received {packet.GetType()}, but WidgetNetId is 0");
return;
}
if (!MyModWidget.TryGetFromNetId(packet.WidgetNetId, out MyModWidget widget))
{
Debug.Log($"Client received {packet.GetType()}, but Widget with NetId {packet.WidgetNetId} was not found");
return;
}
// Update widget state
widget.SetPowered(packet.WidgetPowered);
widget.UpdateSimValues(packet.Values);
}
private void OnWidgetSpawnPacket(ClientBoundMyModWidgetSpawnPacket packet)
{
Debug.Log($"Client received {packet.GetType()}");
// Spawn new widget
MyModWidget.Instantiate(packet);
}
}namespace MyMod.Widget;
public class MyWidget
{
protected void Awake()
{
if (MultiplayerAPI.Instance.IsHost)
{
MyMod.Server.SendWidgetSpawnPacket(this);
}
}
}internal class MyModServer : MonoBehaviour
{
IServer server;
// ... other methods ...
public void SendWidgetSpawnPacket(MyWidget widget)
{
// Generate a packet
var packet = ClientBoundMyModWidgetSpawnPacket.From(widget);
// Send the packet to all players, except the local client
server.SendSerializablePacketToAll(packet, excludeSelf: true);
}
}namespace MyMod.Widget;
public class MyWidget
{
private OnPlayerInteraction(bool powered, float[] values)
{
MyMod.Client?.SendWidgetInteractionPacket(netId, powered, values);
}
}internal class MyModClient : MonoBehaviour
{
IClient client;
// ... other methods ...
public void SendWidgetInteractionPacket(ushort netId, bool powered, float[] values)
{
// Generate a packet
CommonMyModWidgetInteractionPacket packet = new()
{
WidgetNetId = netId,
WidgetPowered
};
// Send the packet to server
server.SendPacketToServer(packet);
}
}While the Multiplayer API doesn't impose strict packet size limits, it's important to keep packets reasonably sized for optimal network performance.
Multiplayer will automatically compress ISerializablePackets when the size exceeds 1024 bytes.
Consider:
-
Using a netId system comprised of ushorts to identify objects, rather than strings
For objects already sync'd by Multiplayer use
MultiplayerAPI.Instance.TryGetNetId()andMultiplayerAPI.Instance.TryGetObjectFromNetId()to retrieve the netId and corresponding object respectively. -
Avoid sending strings
Where possible use enums to represent string constants; this can also help with multi-language support
Getting Started
Guides
- The Basics
- Packet Naming Conventions
- Defining Packets
- Registering Packet Listeners
- Sending Packets