Skip to content

Legacy Networking

Recorded so that nobody is tempted to preserve any of it. This entire layer is deleted.

Sources: libraries/sv_chatbox.lua, metatables/sv_player.lua (SetCSVar), libraries/sv_player.lua (the auto-broadcast), sv_umsgs.lua, sh_init.lua:46.


1. The four transports

The original uses four different mechanisms, none of which exists in a modern engine.

Transport Direction Used for
usermessage / umsg.* Server → client Everything discrete. 64 byte limit.
datastream Server → client Bulk tables too large for a usermessage
NWVar / DTInt Server → all Entity state (door flags, warrants, arrest, tied)
concommand "cider" Client → server Every client action, as an array of strings

There is no use of the net library anywhere. Client→server traffic is entirely console commands and datastream.Hook.


2. Usermessages

19 registered handlers. Names and the hot per-second variable names are string-pooled in gamemode/sv_umsgs.lua — a manual optimisation that a modern engine does automatically.

Message Purpose
cider_Inventory_Item One inventory delta: item:string, amount:long
cider.chatBox.message / .playerMessage Chat lines
CSVar The generic per-player variable channel (§3)
MoneyAlert Money change notification
cider_Menu, cider_BuyDoor, cider_CloseContainerMenu UI triggers
cider_IncomingAccess, cider_massAccessSystem, cider_WipeAccess Access list editing
cider.laws.update Law changes
LogEvent Log lines to the client viewer
TeamChange, PlayerKilled* Notifications
ViewModFP / ViewModTP / ClearViewMod Camera modification

The 64 byte cap applies to all of them, so any long string — a chat line, a door name, an item description — is silently truncated (D-06).

datastream covers what does not fit: helpReplace (the full help table, sent on PlayerInitialized), cider_Laws, and container and vehicle payloads — 17 call sites.


3. CSVars — the bespoke replication system

meta:SetCSVar(class, key, value) (metatables/sv_player.lua:914) is a hand-rolled typed variable channel:

CLASS_STRING · CLASS_LONG · CLASS_SHORT · CLASS_CHAR
CLASS_FLOAT  · CLASS_BOOL · CLASS_VECTOR · CLASS_ANGLE · CLASS_ENTITY

Each write sends a CSVar usermessage carrying the class byte, the key string and the value, received client-side as cider._LocalPlayerVariable.

The change check is broken

if (self.CSVars[var] == nil or self.CSVars ~= value) then

The second clause compares self.CSVarsthe table itself — against value. A table is never equal to a number or string, so the condition is always true. The dirty check never fires and every SetCSVar call sends a message regardless of whether anything changed.

Auto-broadcast

player.AddAutoCSVar(type, name) registers a variable to be re-sent every second, from the 1 s branch of the master tick (libraries/sv_player.lua). Thirteen are registered:

_NextSpawnGender  _Gender     _ScaleDamage  _HideHealthEffects  _Sleeping
_GPS              _beTied     _Stunned      _StuckInWorld       _JobTimeLimit
_JobTimeExpire    _Salary     (+ _Money, sent manually alongside)

Combined with the broken dirty check, this is 14 usermessages per player per second, forever, whether or not any value changed. Most of these are booleans that change a handful of times per session. A 32-player server sends ~450 messages/second to communicate almost nothing (D-11).


4. Entity state

NWBool / NWString for named state ("Arrested", "Tied", "Incapacitated", "Exausted", "Warrant", "Name", "cider_ownerName", "cider_Container"), and GetDTInt(3) as an untyped bitfield for door and container flags (D-16).

Note that _Locked exists as a plain Lua field and as the OBJ_LOCKED bit, updated separately — two sources of truth for one fact.


5. Client → server

Everything: concommand.Add("cider", …).

cider door buy
cider giveitem lexi cake 5

Raw strings, no types, no bounds. The dispatcher checks argument count and an access flag, then pcalls. Rate limiting is per-command _Next* timestamps written by hand in each handler. See 09_CHAT_AND_COMMANDS.md and D-02.


6. Summary of failures

Failure Effect
Broken dirty check + timed rebroadcast Constant bandwidth proportional to player count, independent of activity
Delta-only inventory sync with no reconciliation One lost message desyncs a client until relog
64-byte frame cap Silent truncation of any long string
No client→server type validation Every handler parses by hand; an entire class of exploit surface
Four transports, no abstraction Every payload hand-serialised at the call site
Untyped bitfields and stringly-typed NWVars No schema; meaning held by convention across files
Two sources of truth for lock state They can disagree

7. What the new design takes from this

Nothing structural. But the requirements the legacy layer was trying to meet are real, and they carry forward:

  1. Per-player private state (money, salary, hunger) → [Sync] on a player-owned component, replicated to the owner.
  2. Public entity state (door locked, owner name, container flag) → [Sync] properties on the relevant component, visible to all.
  3. Discrete events (a chat line, a notification, a money alert) → [Rpc.Broadcast] or a targeted RPC, not a state variable.
  4. Bulk data (help text, law text, container contents) → sent on demand, once, not polled.
  5. Client requests[Rpc.Host] with typed parameters, re-validated host-side.

The full specification is Architecture/07_NETWORKING.md.

The rule that falls out of this document: replicate state on change, send events as events, and never poll. Every legacy networking defect is a violation of one of those three.