Custom RPC
The OblivionMP SDK supports two kinds of Remote Procedure Call (RPC) messages:
- Client-relayed RPC — sent by a client, relayed by the server to other clients. The server does not react to these messages; it only forwards them. This is what 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
To add custom RPC procedures to your mod, define a partial class that extends ClientRpcHandler.
public partial class MyRpc(ILogger logger) : ClientRpcHandler
{
// RPC handlers go here
}
For any of the RPC handlers to be registered, the class must be added to the dependency injection container in your mod's RegisterServices method.
protected override void RegisterServices(IDependencyContainer services)
{
services.RegisterSingleton<MyRpc>();
}
Defining RPC handlers
To add a new RPC handler to your mod, add a method decorated with the RpcEvent attribute.
Method names must start with "On..." — a corresponding "Send..." method for sending the RPC will be generated automatically in the same class.
public partial class MyRpc(ILogger logger) : 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 in the sender's area of interest |
| AreaOfInterestAll | Message is sent to all players in the sender's area of interest, 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 handler can have any number of these parameters declared in any order. The generated "Send..." method has 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(ILogger logger) : 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(ILogger logger) : 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 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 BuffData(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(ILogger logger) : ClientRpcHandler
{
[RpcEvent(RelayMode.AreaOfInterestOthers)]
private void OnComplexEvent(int number, PlayerId __sender, BuffData buffData)
{
// example: do something with the sender's player entity
}
// generated by the SDK
private void SendComplexEvent(int number, BuffData buffData) { ... }
}
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 → server) and response (server → client) shapes of an RPC — see server-side development for how they're defined.
On the client, define a partial class extending ServerRpcClient to send requests to the server and handle its responses. As with client-relayed RPC, the class must be registered in your mod's RegisterServices method.
public partial class ServerRpc(ILogger logger) : ServerRpcClient
{
// called when the server responds to our AddWalletBalance request
partial void OnAddWalletBalance(int amount)
{
logger.LogInformation("Gold added: {Amount}", amount);
}
}
Calling the generated Send... method sends the request to the server:
SDK.Services.Resolve<ServerRpc>().SendAddWalletBalance();
Only the legs of the contract 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.