ADR-0005 — Host-authoritative networking¶
Status: ✅ Accepted Date: 2026-07-27 Supersedes: — Superseded by: —
Context¶
The original's networking is unsalvageable and instructive. Four transports
(usermessage, datastream, NWVar/DTInt, and concommand for everything
client→server), none of which survives in a modern engine
(Legacy/11).
More importantly, its authority model has two failures that are independent of the transport and would recur in any naive port:
Nothing validates client input. Every client action arrives at a single
concommand.Add("cider", …) dispatcher as an array of raw strings. It checks argument
count and one access flag, then pcalls the handler. No type validation, no range
validation, no distance check, no ownership check beyond what each handler happens to
implement (D-02).
State is polled, not replicated. SetCSVar's dirty check compares the CSVars table
itself against the incoming value, so it is always true and the check never fires; thirteen
variables are then re-sent to every player every second regardless of change. A server
full of idle players pays exactly as much as one in a firefight
(D-11). Meanwhile inventory is delta-only with no
reconciliation, so one dropped message desyncs a client until relog
(D-05).
The engine offers a direct replacement. A GameObject becomes networked via
NetworkSpawn(), after which
[Sync] properties
replicate on change and
RPCs can be invoked
remotely. [Sync] alone permits the object's owner to write; [Sync(SyncFlags.FromHost)]
restricts writes to the host. RPCs are directed with [Rpc.Broadcast], [Rpc.Host] and
[Rpc.Owner]. Both host-mode P2P and dedicated servers are supported.
The relevant point: [Sync] — the shorter, more obvious attribute — is the permissive one.
The safe choice is the longer one, so the default must be stated explicitly or it will drift.
Decision¶
The host is authoritative over all gameplay state. Clients render and request; they never decide. Every client→server request is a typed
[Rpc.Host]call, re-validated host-side before anything is mutated.
Four rules, applied without exception:
1. Host-owned by default.
// Money is host-authoritative: a client that could write this could print money.
[Sync(SyncFlags.FromHost)] public int Money { get; private set; }
Plain [Sync] is permitted only where the owning client is genuinely authoritative — chiefly
its own input and view intent — and each use carries a comment justifying it. Anything a
player could profit from forging (money, inventory, permissions, ownership, arrest state,
warrants, job) is FromHost, always.
2. Requests are typed, and validated host-side.
[Rpc.Host]
public void RequestPurchaseDoor(Guid doorId)
{
// Never trust the caller. Re-check identity, existence, distance, state and funds.
var character = Rpc.Caller.GetCharacter();
if (character is null || !character.IsInitialised) return;
var door = _property.Find(doorId);
if (door is null || door.IsOwned) return;
if (character.DistanceTo(door) > InteractionRange) return;
if (!_economy.CanAfford(character, _config.DoorCost)) return;
_property.Purchase(character, door); // atomic: money and ownership, or neither
}
Client-side checks exist only to grey out buttons. They are advisory and are never the gate.
3. Replicate state on change; send events as events.
| Kind | Mechanism |
|---|---|
| Durable state (money, hunger, lock status, owner) | [Sync] property |
| A moment in time (chat line, notification, gunshot) | RPC |
| Bulk data (help text, laws, container contents) | Sent once, on demand |
Nothing is polled. A timer that re-sends unchanged data is a defect, not an optimisation.
4. Private state replicates only to its owner. A character's inventory, money and needs are on a player-owned object and do not go to everyone.
The declared budget, and the concrete conventions, are in Architecture/07_NETWORKING.md.
Alternatives considered¶
Client-authoritative with server reconciliation¶
Clients simulate and the host corrects. Lower latency for the acting player.
Rejected. This is a roleplay framework whose entire economy runs on scarcity and trust; optimistic client authority over money, inventory or arrest state is an invitation. The latency benefit is real but applies mainly to movement and shooting, where the engine already handles prediction, and not to the inventory-and-property operations that make up most of this game's interactions.
Trust the client, detect cheating afterwards¶
Simplest, and defensible on a small friends-only server.
Rejected: the original's community was public and multi-server. Detection after the fact still requires the validation logic, and then additionally requires a rollback story.
An authoritative dedicated server only, no host mode¶
Simpler to reason about — one trust model.
Rejected because S&box supports both and host mode is how most people will first play. The
FromHost model covers both: in host mode the host player is the authority, which is the
same trust assumption as a dedicated server plus trusting the host, and that is a documented
property rather than a surprise.
Consequences¶
Positive¶
- An entire class of exploit is structurally prevented; typed RPC parameters remove the parsing bugs for free.
- Bandwidth is proportional to activity, not to player count.
- No desync-until-relog:
[Sync]state converges. - One transport instead of four, with no hand-rolled serialisation at call sites.
- Both host mode and dedicated servers work from the same code.
Negative¶
- Every client action costs a round trip, so the UI must show pending states rather than assuming success. This is the main user-visible cost.
- Validation logic is written twice — advisory on the client for UI, authoritative on the host. Mitigated by putting the rule in a shared engine-agnostic type that both call, which the testing standard already requires.
- Host-side validation is easy to forget in a new RPC, so it needs the review checklist to catch it.
- Host migration is out of scope; if the host leaves, the session ends.
Neutral¶
- The host is trusted. In host mode that means trusting a player — stated plainly in the operator documentation, not hidden.
- Every
[Sync]needs a deliberate decision about who may write it, which is friction by design.
Compliance¶
- Review checklist item: every new
[Rpc.Host]method is walked against the exploit checklist in Standards/03_TESTING_STANDARDS.md. - Every plain
[Sync](non-FromHost) property carries a justifying comment; absence is a review rejection. - Validation rules live in engine-agnostic types and are unit-tested independently of the RPC.
- Per-milestone: measure bytes/second per player at idle. It should be near zero. A non-zero idle cost means something is polling.