Custom RPC
One of the core WukongMP SDK functionalities is the support for arbitrary Remote Procedure Call (RPC) functions. These can have arbitrary payloads and support a number of Relay Modes.
RPC handlers allow you to define your own events that are sent to other players connected to the same server.
Declaring an RPC handler classâ
In order to add custom RPC procedures to your mod, you must define a partial class that extends RpcClassBase.
public partial class MyRpc(IRpcClient client, IRelaySerializer serializer) : RpcClassBase(client, serializer)
{
// RPC handlers go here
}
For any of the RPC handlers to be registered, the class must be added to the DI container in your mod's Initialize method.
Consult the documentation for ModBase.
Defining RPC handlersâ
In order to add a new RPC handler to your mod, add a method decorated with the RpcEvent attribute.
Method names must start with "On..." â corresponding "Send..." methods for sending the RPC will be generated automatically in the same class.
public partial class MyRpc(IRpcClient client, IRelaySerializer serializer) : RpcClassBase(client, serializer)
{
[RpcEvent(RelayMode.AreaOfInterestOthers)]
private void OnMyCustomCall()
{
// do something when this message is received
}
// generated by the SDK
private void SendMyCustomCall() { ... }
}
The attribute requires a parameter of type RelayMode, which indicates how the message will be propagated to players when it reaches the server.
Currently, the following modes are supported:
| Relay mode | Description |
|---|---|
| AreaOfInterestOthers | Message is sent to all other players on the same level |
| AreaOfInterestAll | Message is sent to all players on the same level, including the sender |
| GlobalOthers | Message is sent to all other players on the server |
| GlobalAll | Message is sent to all players on the server, including the sender |
Sending dataâ
RPC handlers support passing data in parameters that are either:
- primitive data types
- structs, decorated with [DeriveINetSerializable].
- special parameters injected by the SDK
An RPC hander can have any number of these parameters declared in any order. The generated "Send..." methods will have the same parameters (excluding the injected ones).
Primitive data typesâ
We support all primitive data types defined in the C# runtime: bool, char, sbyte, byte, short, ushort, int, uint, long, ulong, float, double, and string.
public partial class MyRpc(IRpcClient client, IRelaySerializer serializer) : RpcClassBase(client, serializer)
{
[RpcEvent(RelayMode.AreaOfInterestOthers)]
private void OnMyCustomCall(int number, string text)
{
// do something when this message is received
}
// generated by the SDK
private void SendMyCustomCall(int number, string text) { ... }
}
Complex data typesâ
You can use complex data structures in your RPC parameters. These must extend the INetSerializable interface.
The fields of the payload structures can be of any serializable type, including other complex types, provided they also extend INetSerializable.
public partial class MyRpc(IRpcClient client, IRelaySerializer serializer) : RpcClassBase(client, serializer)
{
[RpcEvent(RelayMode.AreaOfInterestOthers)]
private void OnPlayerBuffed(BuffData payload)
{
// do something when this message is received
}
// generated by the SDK
private void SendPlayerBuffed(BuffData payload) { ... }
}
The SDK provides the [DeriveINetSerializable] attribute, which makes the SDK automatically generate serialization code for most cases. The structure must be declared partial for this to work.
[DeriveINetSerializable]
public partial struct BuffData(int buffId, float duration) : INetSerializable
{
public int BuffId = buffId;
public float Duration = duration;
}
You can also implement the interface manually if you want to have full control over the data sent over the wire.
We use the LiteNetLib library as a base for our networking stack. You can refer to its documentation to learn how to use NetDataWriter and NetDataReader.
public struct BuffAddData(int buffId, float duration) : INetSerializable
{
public int BuffId = buffId;
public float Duration = duration;
public void Serialize(NetDataWriter writer)
{
writer.Put(BuffId);
writer.Put(Duration);
}
public void Deserialize(NetDataReader reader)
{
BuffId = reader.GetInt();
Duration = reader.GetFloat();
}
}
Special parametersâ
Right now there is only one special parameter that can be used. We might expand this functionality in future versions of the SDK.
| Name | Type | Description |
|---|---|---|
__sender | PlayerId | The identifier of the player sending the RPC |
This parameter can be used alongside other parameters, but is not visible in the generated "Send..." method.
public partial class MyRpc(IRpcClient client, IRelaySerializer serializer) : RpcClassBase(client, serializer)
{
[RpcEvent(RelayMode.AreaOfInterestOthers)]
private void OnComplexEvent(int number, PlayerId __sender, BuffData buffData)
{
// example: do something with sender nickname
if (WukongApi.Sync.TryGetPlayerInfoById(__sender, out var playerName, out _))
{
Logging.LogInfo("Received data from {Sender}", playerName);
}
}
// generated by the SDK
private void SendComplexEvent(int number, BuffData buffData) { ... }
}
Executing callbacks on the main threadâ
RPC handlers are executed on a separate network thread, so they should not interact with the game world directly, as doing so can cause crashes. However, you can schedule callbacks to be executed on the main thread using the RunOnMainThread method available in your RPC handler class.
public partial class MyRpc(IRpcClient client, IRelaySerializer serializer) : RpcClassBase(client, serializer)
{
[RpcEvent(RelayMode.AreaOfInterestAll)]
private void OnDespawnAllMonsters()
{
// destroying an Unreal Engine pawn must be done on the main thread
RunOnMainThread(() =>
{
foreach (var monster in WukongApi.Sync.AreaTamers)
{
monster.Tamer?.CurrentRef.DestroyTamer(); // calls BGU_UnrealWorldUtil.DestroyActor
}
});
}
}