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
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
| Option | Default | Meaning |
|---|---|---|
direction | "incoming" | incoming, outgoing or both |
max_size | 4096 | Largest accepted payload (1..65535); larger packets are dropped |
rate_limit | 0 | Accepted packets per player per second; 0 = unlimited |
client_mod | false | true = only clients advertising the capability may send it |
HandleCustomRPC returns false and logs why.3. The RPC object
| Field | Type | Meaning |
|---|---|---|
rpc.id | int | RPC id |
rpc.playerid | int | Sending player (incoming) |
rpc.payload | string | Raw bytes |
rpc.length | int | Payload length in bytes |
rpc.reader | Reader | Bounds-checked binary reader |
rpc.direction | string | "incoming" or "outgoing" |
rpc.is_valid | bool | False when the frame was rejected |
4. Reader methods
| Method | Bytes | Notes |
|---|---|---|
read_int8() / read_uint8() | 1 | Signed / unsigned |
read_int16() / read_uint16() | 2 | Little-endian |
read_int32() / read_uint32() | 4 | Little-endian |
read_float() | 4 | IEEE-754 float32 |
read_bool() | 1 | Non-zero is true |
read_string(n) | n | Raw bytes, no length prefix |
remaining() | — | Bytes left |
valid() | — | False after any short read |
tell() | — | Current offset |
valid() to false and never crashes the server.5. Building payloads
RPCPack assembles little-endian fields so the client mod can parse them:
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)
| Tag | Size |
|---|---|
u8 | 1 byte |
u16 | 2 bytes |
u32 / i32 | 4 bytes |
f32 | 4 bytes |
s | raw 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.
ClientJoin version string: 0.3.7+knight-rpc
The bundled knight-client advertises it by default (-clientmod) and can send a test packet:
./knight-client -host 127.0.0.1 -port 7777 -name RPCBot -customrpc 200 -rpc1 42 -rpc2 8
7. Helpers
| Function | Returns | Purpose |
|---|---|---|
HandleCustomRPC(id, handler, options) | bool | Register; false on a rejected id or option |
SendCustomRPC(playerid, id, payload) | bool | One player; playerid = -1 broadcasts |
RPCPack(tag, value, ...) | string | Build a little-endian payload |
CustomRPCIds() | array | Currently registered ids |
LastCustomRPCStatus() | string | ok, ignored, too_large, rate_limited, no_client_mod |
8. Security model
- The frame is parsed by the engine; a malformed RPC frame is dropped before your handler.
- Payloads larger than
max_sizenever reach the handler. - The rate limit is per player and per id, counted in one-second buckets.
- The reader cannot read past the payload and never panics.
- Stock RPC ids cannot be hijacked.
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.
HandleRPC(230, onFallback, { priority: 1, max_size: 4096, rate_limit: 20 });
HandleRPC(230, onExact, {
priority: 50,
direction: "incoming",
filter: { match: "prefix", bytes: "AA" }
});
| Option | Default | Meaning |
|---|---|---|
priority | 0 | Higher runs first (-1000..1000); equal priority keeps the newest first |
direction | incoming | incoming, outgoing or both |
max_size | 4096 | Payloads above this skip this handler |
rate_limit | 0 | Accepted packets per second for this handler |
client_mod | false | Require the +knight-rpc capability |
filter | match 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
| Match | True when |
|---|---|
any | The payload is accepted (default) |
exact | The inspected window equals bytes |
prefix | The inspected window starts with bytes |
contains | bytes appears anywhere in the inspected window |
bytes | Same 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.
// 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.
| Option | Meaning |
|---|---|
direction | Validated for symmetry with HandleRPC |
match | exact, contains, prefix, bytes or any |
bytes | The bytes to look for |
offset | Window start |
length | Window length |
playerid | If given (>= 0), the sender must be connected |
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
| Function | Returns | Purpose |
|---|---|---|
HandleRPC(id, handler, options) | bool | Add a prioritized handler |
UnregisterRPC(id) | bool | Remove every HandleRPC entry for an id |
RPCHookCount(id) | int | How many HandleRPC entries an id has |