Skip to main content
Version: 0.1.0

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.

Minimal RPC class definition
public partial class MyRpc(ILogger logger) : ClientRpcHandler
{
// RPC handlers go here
}
important

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.

Minimal RPC handler
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 modeDescription
AreaOfInterestOthersMessage is sent to all other players in the sender's area of interest
AreaOfInterestAllMessage is sent to all players in the sender's area of interest, including the sender
GlobalOthersMessage is sent to all other players on the server
GlobalAllMessage is sent to all players on the server, including the sender

Sending data

RPC handlers support passing data in parameters that are either:

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.

Passing primitive data
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.

Complex RPC parameter example
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.

Generating serialization code
[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.

Writing serialization code manually
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.

NameTypeDescription
__senderPlayerIdThe identifier of the player sending the RPC

This parameter can be used alongside other parameters, but is not visible in the generated "Send..." method.

Using injected parameters
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.

Client-side server RPC handling
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.