Knight docs
EN ID
GitHub

Custom RPC

Exchange your own binary packets between a client mod and the server.

SA-MP leaves a large part of the RPC id space unused. Knight lets a gamemode claim those ids and exchange its own binary payloads with a modified client. The engine validates the frame, enforces a size limit and a per-player rate limit, then hands the payload to your handler.

1. Register a handler

knight
function onInventoryRPC(RPC rpc) {
    let item = rpc.reader.read_int32();
    let amount = rpc.reader.read_int32();

    if (!rpc.reader.valid()) { return; }   // truncated payload

    SendCustomRPC(rpc.playerid, 201, RPCPack("u32", item, "u32", amount));
};

system::start() {
    HandleCustomRPC(200, onInventoryRPC, {
        direction: "incoming",
        max_size: 4096,
        rate_limit: 20,
        client_mod: true
    });
};

2. Options

OptionDefaultMeaning
direction"incoming"incoming, outgoing or both
max_size4096Largest accepted payload (1..65535); larger packets are dropped
rate_limit0Accepted packets per player per second; 0 = unlimited
client_modfalsetrue = only clients advertising the capability may send it
Warning
Ids the stock protocol already dispatches (for example 101 chat, 25 ClientJoin, 50 ServerCommand) cannot be claimed; HandleCustomRPC returns false and logs why.

3. The RPC object

FieldTypeMeaning
rpc.idintRPC id
rpc.playeridintSending player (incoming)
rpc.payloadstringRaw bytes
rpc.lengthintPayload length in bytes
rpc.readerReaderBounds-checked binary reader
rpc.directionstring"incoming" or "outgoing"
rpc.is_validboolFalse when the frame was rejected

4. Reader methods

MethodBytesNotes
read_int8() / read_uint8()1Signed / unsigned
read_int16() / read_uint16()2Little-endian
read_int32() / read_uint32()4Little-endian
read_float()4IEEE-754 float32
read_bool()1Non-zero is true
read_string(n)nRaw bytes, no length prefix
remaining()—Bytes left
valid()—False after any short read
tell()—Current offset
Note
Every read is bounds-checked. A short or hostile payload zero-fills the missing values, sets valid() to false and never crashes the server.

5. Building payloads

RPCPack assembles little-endian fields so the client mod can parse them:

knight
let payload = RPCPack("u8", 7, "u16", 300, "u32", 70000, "f32", 1.5, "s", "hi");
SendCustomRPC(playerid, 201, payload);          // one player
SendCustomRPC(-1, 201, payload);               // everyone (broadcast)
TagSize
u81 byte
u162 bytes
u32 / i324 bytes
f324 bytes
sraw string bytes

6. Which clients may send it

With client_mod: true, only clients that append +knight-rpc to the version string in their ClientJoin are accepted. Everything else is silently ignored, so a stock 0.3.7 client can never trigger the handler.

text
ClientJoin version string: 0.3.7+knight-rpc

The bundled knight-client advertises it by default (-clientmod) and can send a test packet:

bash
./knight-client -host 127.0.0.1 -port 7777 -name RPCBot     -customrpc 200 -rpc1 42 -rpc2 8

7. Helpers

FunctionReturnsPurpose
HandleCustomRPC(id, handler, options)boolRegister; false on a rejected id or option
SendCustomRPC(playerid, id, payload)boolOne player; playerid = -1 broadcasts
RPCPack(tag, value, ...)stringBuild a little-endian payload
CustomRPCIds()arrayCurrently registered ids
LastCustomRPCStatus()stringok, ignored, too_large, rate_limited, no_client_mod

8. Security model

9. Multiple handlers: HandleRPC

HandleCustomRPC installs one handler per id. HandleRPC installs many: each has a priority and an optional payload filter, they run from highest priority down, and the first one that matches consumes the packet. If none matches, the packet is ignored.

knight
HandleRPC(230, onFallback, { priority: 1, max_size: 4096, rate_limit: 20 });
HandleRPC(230, onExact, {
    priority: 50,
    direction: "incoming",
    filter: { match: "prefix", bytes: "AA" }
});
OptionDefaultMeaning
priority0Higher runs first (-1000..1000); equal priority keeps the newest first
directionincomingincoming, outgoing or both
max_size4096Payloads above this skip this handler
rate_limit0Accepted packets per second for this handler
client_modfalseRequire the +knight-rpc capability
filtermatch any{ match, bytes, offset, length } — see below

Handlers and HandleCustomRPC coexist: the engine tries the HandleRPC chain first, then falls back to the HandleCustomRPC handler for the same id.

10. Payload filters

MatchTrue when
anyThe payload is accepted (default)
exactThe inspected window equals bytes
prefixThe inspected window starts with bytes
containsbytes appears anywhere in the inspected window
bytesSame as contains, for arbitrary binary bytes

offset (default 0) and length (default: to the end) select the window before matching. Out-of-range values never match and never crash.

knight
// payload: AA 00 05
CheckRPC(230, payload, { match: "prefix", bytes: "AA" });                  // true
CheckRPC(230, payload, { match: "exact", bytes: "AA", length: 2 });        // true
CheckRPC(230, payload, { match: "contains", bytes: "{0x05}" });        // true
CheckRPC(230, payload, { match: "exact", bytes: "AA", offset: 2 });        // false

11. CheckRPC

CheckRPC(id, payload, options) is a pure predicate: it registers nothing and returns whether an id and payload match your criteria. Useful inside a handler to route on a header, or before sending a reply.

OptionMeaning
directionValidated for symmetry with HandleRPC
matchexact, contains, prefix, bytes or any
bytesThe bytes to look for
offsetWindow start
lengthWindow length
playeridIf given (>= 0), the sender must be connected
knight
system::start() {
    let probe = RPCPack("u8", 65, "u8", 65, "u8", 1);
    Log("prefix={CheckRPC(230, probe, { match: "prefix", bytes: "AA" })}");
    Log("contains={CheckRPC(230, probe, { match: "contains", bytes: "A" })}");
    Log("offset={CheckRPC(230, probe, { match: "exact", bytes: "AA", length: 2 })}");
};

12. Managing handlers

FunctionReturnsPurpose
HandleRPC(id, handler, options)boolAdd a prioritized handler
UnregisterRPC(id)boolRemove every HandleRPC entry for an id
RPCHookCount(id)intHow many HandleRPC entries an id has
Warning
Stock protocol ids (chat, ClientJoin, ServerCommand, dialogs, sync...) are refused by both APIs, so internal RPCs cannot be overridden.