Custom RPC
The WukongMP SDK supports two kinds of Remote Procedure Call (RPC) messages:
- Client-relayed RPC, sent by a client and relayed by the server to other clients. The server does not react to these messages, it only forwards them. This is what most of this page covers.
- Server RPC, sent by a client to the server, which runs your server-side logic and may reply. Contracts and server-side handlers for these are defined in server-side development. This page covers how to handle the client side of a server RPC.
Client-relayed RPC
Client-relayed RPC lets you define your own events that are sent to other players connected to the same server, without any server-side logic involved. These can have arbitrary payloads and support a number of Relay Modes.
Declaring an RPC handler class
In order to add custom RPC procedures to your mod, you must define a partial class that extends ClientRpcHandler.
public partial class MyRpc : ClientRpcHandler
{
// 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.
protected override void Initialize(IDependencyContainer services)
{
services.RegisterSingleton<MyRpc>();
}
Dependencies are injected through the constructor, so a handler that needs a logger or another service takes it as a primary constructor parameter: public partial class MyRpc(ILogger logger) : ClientRpcHandler.
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...", and corresponding "Send..." methods for sending the RPC will be generated automatically in the same class.
public partial class MyRpc : ClientRpcHandler
{
[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 : ClientRpcHandler
{
[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 : ClientRpcHandler
{
[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 : ClientRpcHandler
{
[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) { ... }
}
Threading
Messages arrive on a separate network thread, but the SDK schedules every [RpcEvent] handler onto the game thread before invoking it. Your handler body can touch the game world directly, so working with Unreal Engine objects needs no extra ceremony:
public partial class MyRpc : ClientRpcHandler
{
[RpcEvent(RelayMode.AreaOfInterestAll)]
private void OnDespawnAllMonsters()
{
foreach (var monster in WukongApi.Sync.AreaTamers)
{
monster.Tamer?.CurrentRef.DestroyTamer(); // calls BGU_UnrealWorldUtil.DestroyActor
}
}
}
Because handlers run on the game thread, they also share its budget. Anything expensive belongs in a system or on a background task, not in the handler body.
Handling server RPC on the client
Server RPC contracts are declared in a common project shared by the server and client mods, and describe both the request (client to server) and response (server to client) shapes of an RPC. See server-side development for how they are defined.
On the client, define a partial class extending ServerRpcClient to send requests to the server and handle its responses. It must carry [ServerRpcFor] naming the contract class it implements, exactly like its server-side counterpart. As with client-relayed RPC, the class must be registered in your mod's Initialize method.
These are scheduled onto the game thread for you, exactly like [RpcEvent] handlers, so the body can touch the game world directly:
[ServerRpcFor(typeof(RpcContracts))]
public partial class MyServerRpc : ServerRpcClient
{
// called when the server responds to our ScaleBossHp request
partial void OnBossHpScaleConfirm(int scalingPercent, int players)
{
WukongApi.Chat.ShowLocalMessage(
$"Boss HP is set to {scalingPercent}% and multiplied by {players} players.",
FLinearColor.Gray);
}
}
These handlers used to run on the network thread, so anything touching the game world had to be wrapped in RunOnGameThread. The scheduling is done for you now. RunOnGameThread still exists, but calling it from a handler is redundant, so you can unwrap those bodies.
Calling the generated Send... method sends the request to the server:
WukongApi.Services.Resolve<MyServerRpc>().SendScaleBossHp(150);
Only the legs declared by the shared contract are generated: a one-way client-to-server RPC has no On... handler stub on the client, and a one-way server-to-client RPC has no Send... method. Only the legs of the class named in [ServerRpcFor] are generated, so use one class per contract set. See Binding a class to its contract.