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:

  • Id is the user-visible RPC identifier (must be >= 1024).
  • EventHash is typically Crc64(method_name) and lets a single Id cover 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 HasAnyFlagHasFlag 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 OnRpcError to 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
  • 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
Back to top