RPCs
Overview
RPCs in Fusion are fire-and-forget messages sent between clients. Unlike state synchronization, which continuously replicates property values, RPCs deliver discrete events: damage notifications, chat messages, ability activations and other one-shot actions.
Rpc Structure
C++
class Rpc {
public:
uint64_t Id; // RPC identifier
uint64_t Sequence; // Per-RPC monotonic sequence (assigned by SendUserRpc)
RpcFlags Flags; // Strong-typed delivery / error flags
uint32_t DeliveryAttempts; // Number of attempts so far
PlayerId OriginPlayer; // Sender (uint16_t)
PlayerId TargetPlayer; // Recipient (0 = all)
ObjectId TargetObject; // Target object ((0,0,0) = broadcast)
uint64_t EventHash; // Method/event hash for dispatch
Data Bytes; // Serialized payload
bool IsInternal() const; // True if Id is in [1, 1023]
};
Key Fields
| Field | Purpose |
|---|---|
Id |
Unique RPC identifier. IDs 1-1023 are reserved for SDK internal RPCs. User RPCs use IDs >= 1024. |
Sequence |
Monotonic per-sender sequence number. Assigned in place by Client::SendUserRpc; do not set it manually. |
Flags |
RpcFlags bitset (delivery hints + error indicators). See RpcFlags. |
DeliveryAttempts |
Count of delivery attempts. When the server exhausts its retry budget, it sets RpcFlags::MaxDeliveryAttemptsReached and the RPC surfaces via OnRpcError. |
TargetPlayer |
Specific player to receive the RPC. 0 for all players, or a PlayerId for targeted delivery. |
TargetObject |
The networked object this RPC is addressed to. ObjectId(0,0,0) indicates a broadcast RPC. |
EventHash |
Hash identifying the event/method. Combined with Id, this routes the RPC to the correct handler. Replaces the older paired DescriptorTypeHash+EventHash scheme — there is now a single hash on the wire. |
Bytes |
Opaque payload containing serialized arguments. |
The older Rpc::TargetComponent (uint16_t) and Rpc::DescriptorTypeHash (uint64_t) fields have been removed; routing now uses the (Id, EventHash) pair, and per-component sub-targeting is handled at the integration layer instead of in the wire format.
Creating and Sending RPCs
CreateUserRpc
C++
Rpc Client::CreateUserRpc(
uint64_t id, // RPC ID (must be >= 1024)
PlayerId targetPlayer, // 0 = all, specific PlayerId = targeted
ObjectId targetObject, // Object target or (0,0,0) for broadcast
uint64_t EventHash, // Method hash for dispatch
const char* data, // Serialized payload bytes
size_t dataLength // Payload length
);
This constructs an Rpc struct with the local player as OriginPlayer. The data parameter should contain pre-serialized arguments. Older SDKs took a separate DescriptorTypeHash argument before EventHash — that parameter has been removed.
CreateUserRpc does not copy the payload — Rpc::Bytes aliases the caller's buffer.
Keep the buffer alive until SendUserRpc returns (which serializes the bytes into the outgoing RPC buffer); free it afterwards.
The Rpc struct must not outlive the payload.
SendUserRpc
C++
bool Client::SendUserRpc(Rpc& rpc);
Serializes the RPC into the outgoing RPC buffer. Takes a non-const reference because the SDK assigns Rpc::Sequence in place. RPCs are batched and flushed during the next UpdateFrameEnd() send cycle. Returns false (with a warning) if id is in the internal range (1023 or below), true otherwise.
Complete Example
C++
// 1. Serialize payload
FusionCore::WriteBuffer payload;
payload.Int(damageAmount);
payload.UIntVar(targetEntityId);
FusionCore::Data payloadData = payload.Take();
// 2. Create the RPC (Bytes aliases payloadData — do not free it yet)
auto rpc = client->CreateUserRpc(
1024, // User RPC ID
0, // All players
targetObjectId, // Object-targeted
FusionCore::Crc64(u8"OnDamage", std::strlen("OnDamage")), // Event hash
reinterpret_cast<const char*>(payloadData.Ptr),
payloadData.Length
);
// 3. Send (the SDK fills in Sequence and copies the bytes out)
client->SendUserRpc(rpc);
// 4. Clean up payload — safe now that SendUserRpc returned
payloadData.Free();
Receiving RPCs
Successful RPCs arrive through the OnRpc broadcaster. Failed deliveries (any bit of the RpcFlags::ErrorFlags mask set) surface separately via OnRpcError:
C++
RealtimeCore::Common::Broadcaster<void(Rpc&)> OnRpc;
RealtimeCore::Common::Broadcaster<void(Rpc&)> OnRpcError;
Only user RPCs reach these broadcasters — internal RPCs (IDs 1-1023) are consumed by the SDK before dispatch and never surface on OnRpc, so handlers do not need an IsInternal() guard.
The Rpc and its Bytes payload are freed by the SDK when the callback returns; copy anything you need to keep.
Subscribe to receive RPCs:
C++
subs += client->OnRpc.Subscribe([](FusionCore::Rpc& rpc) {
if (rpc.TargetObject.IsSome()) {
dispatch_to_object(rpc);
} else {
dispatch_broadcast(rpc);
}
});
subs += client->OnRpcError.Subscribe([](FusionCore::Rpc& rpc) {
using namespace FusionCore;
if (HasFlag(rpc.Flags, RpcFlags::PlayerMissing)) {
// Target player left before the RPC could be delivered.
}
if (HasFlag(rpc.Flags, RpcFlags::ObjectMissing)) {
// Target object was destroyed.
}
if (HasFlag(rpc.Flags, RpcFlags::MapIncorrect)) {
// Target object's map differs from sender's expectation.
}
if (HasFlag(rpc.Flags, RpcFlags::MaxDeliveryAttemptsReached)) {
// Retry budget exhausted.
}
});
Routing by Hash
The combination of (Id, EventHash) identifies the handler:
Idis the user-visible RPC identifier (must be >= 1024).EventHashis typicallyCrc64(method_name)and lets a singleIdcover multiple events without collisions.
The integration layer computes hashes at registration time and matches them against incoming RPCs.
Reading the Payload
C++
void handle_damage_rpc(FusionCore::Rpc& rpc) {
FusionCore::ReadBuffer reader(rpc.Bytes);
int32_t damage = reader.Int();
uint32_t entityId = reader.UIntVar();
if (reader.Failed()) {
return; // Truncated or malformed payload — reads past the end return 0
}
// Apply damage...
}
Always check ReadBuffer::Failed() after parsing — the payload comes from a remote peer, and reads past the end of a truncated buffer silently return zero.
ID Ranges
| Range | Owner | Purpose |
|---|---|---|
| 1 - 1023 | SDK internal | Map changes, ownership requests, prediction inputs, etc. |
| 1024+ | User | Application-defined RPCs |
Internal RPC Constants
C++
constexpr uint64_t RPC_INTERNAL_MIN_ID = 1;
constexpr uint64_t RPC_INTERNAL_MAX_ID = 1023;
constexpr uint64_t RPC_INTERNAL_MAP_CHANGE = 1;
constexpr uint64_t RPC_INTERNAL_OBJECT_PRIORITY = 2;
constexpr uint64_t RPC_INTERNAL_MAP_ADD = 3;
constexpr uint64_t RPC_INTERNAL_MAP_REMOVE = 4;
constexpr uint64_t RPC_INTERNAL_OWNERSHIP_REQUEST = 5;
constexpr uint64_t RPC_INTERNAL_REJECT_SUB_OBJECT = 6;
constexpr uint64_t RPC_INTERNAL_DESTROYED_MAP_ACTORS = 7;
constexpr uint64_t RPC_INTERNAL_FORCE_DESTROY_OBJECT = 8;
constexpr uint64_t RPC_INTERNAL_FORCE_ALIVE_OBJECT = 9;
constexpr uint64_t RPC_INTERNAL_INPUT = 10;
constexpr uint64_t RPC_INTERNAL_PLAYER_INTEREST = 11;
constexpr uint64_t RPC_INTERNAL_OWNERSHIP_RESPONSE = 12;
Internal RPCs are consumed entirely by the SDK — they never reach OnRpc or OnRpcError.
The IsInternal() method identifies an RPC in the internal range.
User code cannot send RPCs with IDs in the internal range — SendUserRpc rejects them.
Target Filtering
Player Targeting
| TargetPlayer Value | Behavior |
|---|---|
0 |
Delivered to all players |
Specific PlayerId |
Delivered only to that player |
MASTER_CLIENT_PLAYER_ID (0xFFFF) |
Delivered to the current master client |
OBJECT_OWNED_PLAYER_ID (0xFFFD) |
Delivered to the owner of TargetObject |
The receiving SDK re-checks the target locally (master-client status and object ownership can change in flight).
An RPC that no longer matches its target is forwarded back for redelivery instead of being dispatched, unless RpcFlags::DontReplyWithResult is set.
Object Targeting
| TargetObject Value | Behavior |
|---|---|
ObjectId(0, 0, 0) (.IsNone()) |
Broadcast RPC -- not object-targeted |
Valid ObjectId (.IsSome()) |
Delivered to the specific networked object |
RpcFlags
C++
enum class RpcFlags : uint32_t {
None = 0,
ReturnResultOnFailure = 1 << 0,
IncorrectTargetForward = 1 << 1,
DontReplyWithResult = 1 << 2,
PlayerMissing = 1 << 5,
ObjectMissing = 1 << 6,
MapIncorrect = 1 << 7,
MaxDeliveryAttemptsReached = 1 << 8,
ErrorFlags = PlayerMissing | ObjectMissing
| MapIncorrect | MaxDeliveryAttemptsReached,
};
bool HasFlag(RpcFlags flags, RpcFlags flag);
bool HasAnyFlag(RpcFlags flags, RpcFlags mask);
RpcFlags is now an enum class (was a wrapper struct in older SDKs). Compose with | and inspect with HasFlag for single flags. For multi-bit masks like ErrorFlags, use HasAnyFlag — HasFlag requires all bits of the mask to be set, while HasAnyFlag matches any. The ErrorFlags mask groups all delivery-failure indicators; an incoming RPC with any of them set is routed to OnRpcError.
Wire Format
The Rpc struct has built-in serialization for the SDK's packet protocol:
C++
static Rpc Rpc::Read(ReadBuffer& reader);
static void Rpc::Write(WriteBuffer& writer, const Rpc& rpc);
These are used internally by the SDK for packet construction and parsing. User code does not need to call them directly.
Delivery Guarantees
RPCs travel on the reliable Streaming channel of Fusion's Notify connection, which provides ordered delivery with acknowledgment tracking — lost packets are detected and retransmitted by the transport.
The delivery-attempt budget is enforced server-side: when the server exhausts its attempts for a target (or the target object/player goes missing), it stamps the RPC's Flags with the appropriate RpcFlags::ErrorFlags bit and returns it, surfacing via OnRpcError instead of OnRpc.
While the channel is congested, outgoing RPCs are not dropped — they accumulate in an unbounded local buffer and are flushed once the channel can queue again. Sustained bursts therefore increase memory use and arrival latency rather than losing messages.
This means:
- Successful RPCs are guaranteed to arrive in order from the same sender.
- Failed RPCs are reported (rather than silently lost) — subscribe to
OnRpcErrorto react. - Duplicate delivery does not occur.
Unreliable Path
The Notify connection has three channels: Game (state), Streaming (reliable RPCs) and Unreliable.
The SDK maintains an internal unreliable RPC path that flushes onto the Unreliable channel each send cycle, but it is not currently reachable from user code — SendUserRpc always sends reliably.
Common Mistakes
| Mistake | Symptom |
|---|---|
| Using RPC ID < 1024 | SendUserRpc returns false and logs a warning |
Freeing the payload before SendUserRpc |
Use-after-free — CreateUserRpc aliases the buffer |
Retaining rpc.Bytes after the OnRpc callback returns |
Dangling pointer — the SDK frees the payload |
| Sending RPCs before the client is running | RPCs sit in the outgoing buffer until the client is running |
Not freeing payload Data after Take() |
Memory leak |
Ignoring OnRpcError |
No visibility when RPCs fail (player left, object destroyed, map mismatch) |
Not checking ReadBuffer::Failed() after parsing |
Truncated payloads decode as zeros |
| Mismatched read/write order in payload | Corrupted arguments |
Passing a hand-set Sequence to SendUserRpc |
Overwritten by the SDK at send time — pointless |
Related
- Architecture -- Crc64 for hash generation
- Scene Management -- Internal RPCs drive the multi-map session
- Time -- When RPCs are sent and received in the frame loop