Date: 2026-05-18
This is a living implementation plan for the current protocol-5 multiplayer
refactor. All currently identified wire/protocol-breaking pieces are intended
to be completed inside protocol 5, so testers do not have to retest several
intermediate incompatible versions.
The goal is to make multiplayer easier to support and debug. We should not
add more compatibility layers, packet guessing, or special cases around the
old DELETE_OBJECT + CREATE_OBJECT semantics unless we are doing a temporary
hotfix.
The old network protocol exposes too much of the local client object model.
For example, item pickup/drop is encoded as low-level object destruction and creation:
- world item:
NID_STUFF; - inventory/device item:
NID_DEVICE; - pickup/drop is represented as a paired
DELETE_OBJECTandCREATE_OBJECT.
That makes the server guess intent from object lifecycle packets. This is why we get fragile cases like:
- another player tries to delete a world
STUFFwhile picking it up; - the Rust server rejects it as a non-owner delete;
- then it accepts the new
DEVICE; - now the same logical item exists in conflicting states.
The protocol should describe gameplay intent, not local implementation details.
-
Server owns multiplayer truth. Clients may request actions, but the server validates and commits state.
-
One gameplay action = one network command. Pickup, drop, slot change, world switch, and player equipment change should not be reconstructed from several low-level object events.
-
No hidden semantic meaning in generic packets.
DELETE_OBJECTshould mean object deletion, not “maybe pickup confirmation if body byte is 1”. -
Prefer explicit snapshots over ordering assumptions. After world switch/reconnect the client should receive a complete state snapshot for that world instead of relying on lucky ordering of later updates.
-
Break protocol compatibility cleanly. If we change semantics, bump protocol version and reject old clients with a clear error. Do not keep multiple old/new item-transfer paths alive unless absolutely necessary for a short transition.
-
Keep the number of new concepts small. Do not introduce complicated reliability classes, ownership exceptions, or per-object hacks if a simpler authoritative state model solves the issue.
Protocol 5 is the current compatibility break for this refactor.
Do not bump the protocol again while finishing the remaining protocol-5 pieces:
- end-to-end item transfer;
- deterministic world-entry snapshot boundary;
- client/server handling required by those packet ids.
Only bump again if we introduce another incompatible packet format after
protocol 5 has already been handed to testers as a stable baseline.
Reason: testers cannot continuously retest every intermediate bump. We should finish the coherent protocol-5 change set first, then test that as one multiplayer compatibility boundary.
Long-term, a networked item should be represented by one logical item identity:
item_id
data_id
state = InWorld | InInventory | Deleted
owner_player_id / owner_vanger_id
slot
world
position
NID_STUFF and NID_DEVICE can remain client-side/rendering/storage forms,
but they should not be the authoritative multiplayer identity.
The server should not need to infer that 0x060B004F and 0x0601004F are two
faces of the same item by decoding object id type bits.
Replace low-level item transfer with explicit commands.
For protocol 5 this is implemented as one generic transfer request:
ITEM_TRANSFER(kind, old_id, old_time, delete_marker, new_item_object)
where kind is Pickup or Drop. This covers the currently observed
protocol-breaking item bug: a single gameplay transfer must no longer be split
into independent DELETE_OBJECT and CREATE_OBJECT lifecycle packets.
If we later need explicit item slot moves or item use requests, they should
reuse this state model or add new requests before protocol 5 is treated as a
stable external baseline. They are not part of the current observed bug because
the current slot/use paths do not require replacing the old split
pickup/drop lifecycle.
The semantics must stay direct:
- client requests an action;
- server validates current state;
- server applies the state transition atomically;
- server broadcasts the resulting item state.
Server sends state, not guessed lifecycle:
ITEM_STATE(item_id, full state)
ITEM_REMOVED(item_id, reason)
For the first implementation we can still translate server state into existing
client object creation/removal internally, but that translation should happen
on the client side from explicit item state, not by receiving arbitrary
DELETE_OBJECT from another client.
World entry should be deterministic:
- client sends
SET_WORLD; - server updates player world;
- server replies
SET_WORLD_RESPONSE; - server sends one
WORLD_SNAPSHOT; - only after snapshot completion does the client apply live updates.
WORLD_SNAPSHOT should contain:
- players in the world;
- their vangers;
- current equipment/slots;
- world items;
- relevant global objects;
- current object counters/state needed to avoid duplicate ids.
This is simpler than trying to repair missing remote weapons or invisible players after the fact.
Remote weapons should not depend on incidental inventory/object ordering.
For protocol 5 this is handled through the deterministic world snapshot
rather than by introducing another packet family:
PLAYERS_DATAdescribes remote players;VANGERsnapshot entries create the remote mechoses first;SLOTand inventoryDEVICEsnapshot entries follow after the vanger;- active
SHELLentries can be replayed after equipment state exists.
This gives the client a stable construction order without inventing a separate equipment protocol before we know that the existing object representation is insufficient. If later testing proves that live weapon/shot state still needs a dedicated event, that should be designed as an extension of the same snapshot state model, not as another hidden ordering dependency.
The “remote player has no visible weapons / shooting is not visible” bug should be handled by the same protocol break, but as a separate piece of work from item pickup/drop.
The right fix is WORLD_SNAPSHOT.
After SET_WORLD, the server should send a complete snapshot for the target
world before the client starts applying live updates:
- players data;
VANGERobjects;SLOTobjects;- inventory
DEVICEobjects; - active
SHELLobjects if they must survive world entry timing; - world
STUFF.
Only after the snapshot is complete should normal live CREATE_OBJECT,
UPDATE_OBJECT, DELETE_OBJECT, HIDE_OBJECT, shot, and slot updates be
processed.
This removes the fragile window where a client receives slot/device/update
events for a remote player, but does not yet have the local VangerUnit needed
to attach those events to something visible.
Important: this should not become another pile of per-packet exceptions. The snapshot is a boundary:
SET_WORLD
SET_WORLD_RESPONSE
WORLD_SNAPSHOT_BEGIN
WORLD_SNAPSHOT_ENTRY...
WORLD_SNAPSHOT_END
live updates
During snapshot loading, live updates can be queued or ignored according to a simple rule, but they should not be mixed with partially constructed world state.
These old semantics should be phased out for multiplayer gameplay state:
- non-owner
DELETE_OBJECTas item pickup; - paired
STUFF/DEVICEobject ids as authoritative item identity; - object ownership inferred only from
NetOwnerinside client-provided body; - world entry depending on later
UPDATE_OBJECTto create missing vangers; - slot state buffered indefinitely waiting for missing local objects;
- server accepting local client object lifecycle as truth without semantic validation.
All protocol-breaking pieces that were already identified for this refactor are implemented in the current client/server patch. The remaining work should be internal cleanup, better tests, or snapshot-content refinements that reuse the same protocol-5 packet ids.
Implemented:
- protocol was bumped to
5; - old clients are rejected at handshake instead of being mixed with protocol-5 clients;
- client sends explicit
ITEM_TRANSFERfor pickup/drop instead of sendingDELETE_OBJECT + CREATE_OBJECTas two independent requests; - Rust server validates
ITEM_TRANSFERand commits server-sidevanjectsatomically: either old object is replaced by new object, or nothing changes; - server validates that old/new ids are paired
STUFF/DEVICEforms of the same item identity: same station, same world, same counter, different object type; - accepted transfer is broadcast as explicit
ITEM_STATE; - generic item create/update is also broadcast as
ITEM_STATE, not asUPDATE_OBJECT; - generic item delete is broadcast as explicit
ITEM_REMOVED, not asDELETE_OBJECT; - server rejects old protocol-4 style item-transfer
DELETE_OBJECTmarker (body[0] == 1) instead of treating it as hidden pickup semantics; - server rejects item
CREATE_OBJECTif the pairedSTUFF/DEVICEform is already present, so old split transfer cannot leave both faces alive; - client applies
ITEM_STATEthrough one explicit item-state handler and marks the previous paired object as waiting for conversion before applying the new state; - client applies
ITEM_REMOVEDthrough one explicit item-removal handler; - Rust server sends a world snapshot after
SET_WORLD; - snapshot includes remote
VANGER,SLOT,DEVICE,SHELL, and worldSTUFF, ordered asVANGER -> SLOT -> DEVICE -> SHELL -> STUFF; - snapshot item entries use
ITEM_STATE, so world-entry item state and live item state have the same semantics; - client treats
WORLD_SNAPSHOT_BEGIN/ENDas a real loading boundary and ignores stale pre-snapshot object/player/item updates.
This is not a full separate logical-item database yet: internally the server
still stores the currently active item face as a Vanject, and the paired id is
decoded from the item body. That is acceptable for protocol 5 because the
wire format no longer exposes the old split lifecycle semantics. A later server
data-structure cleanup can keep the same ITEM_TRANSFER, ITEM_STATE, and
ITEM_REMOVED packets.
Status: implemented.
- Bump protocol version.
- Reject old clients clearly.
- Add explicit
ITEM_TRANSFERrequest packet. - Client sends pickup/drop as one request.
- Server validates and commits pickup/drop atomically in server state.
- Keep old generic object packets only for objects that are still truly generic.
This removes the worst ambiguity on the request path: the server no longer has
to guess whether a non-owner DELETE_OBJECT is a real delete or an item pickup.
Status: implemented at the protocol level.
Server-to-client item state is now explicit:
ITEM_STATE {
item_state,
previous_id,
full_update_object_data
}
ITEM_REMOVED {
item_id,
paired_id,
reason
}
Why this matters:
- pickup/drop no longer creates a window where clients see only
DELETE_OBJECTor onlyCREATE_OBJECT; - world item, inventory item, and snapshot item use the same server-authored state event;
- a true item removal is distinguishable from a transfer;
- old
DELETE_OBJECT body[0] == 1semantics are rejected, not emulated.
The remaining “logical item table” idea is now an internal server refactor, not
a necessary protocol break. If we later replace HashMap<i32, Vanject> with a
dedicated item table, it can still emit the same ITEM_STATE / ITEM_REMOVED
events.
Status: implemented.
- Server sends
WORLD_SNAPSHOT_BEGIN, ordered snapshot entries, andWORLD_SNAPSHOT_ENDafterSET_WORLD. - Snapshot includes players data and the object types needed to reconstruct remote vangers, their slots/equipment, active shells, and visible items.
- Snapshot item entries use
ITEM_STATE. - Client enters
wait_beginstate when requestingSET_WORLD. - Client ignores stale object/player/item packets before
WORLD_SNAPSHOT_BEGIN. - Client applies snapshot entries while
loading. - Client returns to normal live-update processing only after
WORLD_SNAPSHOT_END.
This removes the fragile window where a client receives slot/device/update
events for a remote player before it has the local VangerUnit needed to attach
those events.
Status: implemented for item transfer semantics.
- Old pickup/drop through split
DELETE_OBJECT + CREATE_OBJECTis no longer a valid protocol-5 item transfer path. - Server rejects protocol-4 item-transfer delete markers.
- Server rejects paired item creates that would resurrect the old split transfer bug.
- Non-item
CREATE_OBJECT,UPDATE_OBJECT,DELETE_OBJECT, andHIDE_OBJECTremain available for generic object lifecycle.
The following can be done after multiplayer tests without another protocol bump:
-
add a reliable live-join replay for players that are already in the world when another player enters that world. The
SET_WORLDsnapshot currently protects the joining client, but logs from 2026-05-19 still show a short window on existing clients whereUPDATE_OBJECT VANGERcan arrive before the local remoteVangerUnitis fully available (ignored_missing_vanger). This should be fixed with a small ordered spawn/snapshot bundle to the existing world clients:PLAYERS_DATA for the entering player VANGER state SLOT state DEVICE/ITEM_STATE state SHELL state if needed then live UPDATE_OBJECTThe important point is semantic ordering, not a new packet family. Reuse existing protocol-5 packets and make the server send them reliably before the entering player's replaceable realtime updates can overtake them. This should address the remaining "remote player exists but weapons/shot visuals are missing" class of bugs without another protocol bump;
-
investigate
UPDATE_OBJECTfor missing static type-14 objects. The 2026-05-19 server log contains several harmless-but-noisyignored_missingupdates such as0x880E02E4, all decoded asstatic_object=true,type_id=14. These are not item-transfer objects and did not break the session, but the server currently reports them as errors. We need to decide whether type-14 objects should be accepted/created as replicated static world state, or whether missing updates for that class are expected and should be downgraded to an ignored/rate-limited diagnostic; -
make client diagnostic logs unambiguous when several local clients are run from the same
data/directory. The 2026-05-19 local test hadstation=1andstation=2writing into the samenetwork-client.log, which is useful enough for rough analysis but makes exact ordering harder. A future diagnostic-only cleanup can include the process id, connection id, or final station id in the file name; -
improve Rust server internals by replacing paired-id decoding with a real logical item table while keeping the same wire packets;
-
extend snapshot contents if logs show a missing object class, as long as existing
ITEM_STATE/UPDATE_OBJECTsnapshot entries are enough; -
simplify client receive paths now that item state and snapshot boundaries are explicit;
-
remove temporary high-volume diagnostics after protocol-5 multiplayer is stable;
-
tune logging and server visibility rules.
- Item pickup/drop cannot leave both
STUFFandDEVICEversions alive. - Two players racing for one item produce one winner and one clear rejection.
- Another player’s trunk cannot lose items because the owner died or changed world.
- World switch/reconnect always reconstructs visible players and their weapons.
- Server logs describe gameplay decisions, not only low-level packet effects.
- The amount of special-case networking code decreases after the refactor.