DarqlineMP Protocol
DarqlineMP runs over UDP. Every multi-byte value is little-endian, and every packet starts with packet_id: u8. The current wire revision is PROTOCOL_VERSION = 146 (u16). The server compares it only during the handshake, by strict equality: a ClientHello with any other revision gets no answer. The revision is unrelated to the release versions of the client and the server; those are not sent on the wire and do not decide compatibility. The envelope (revision check, per-connection session token, stale-packet protection and strict validation) is set out in ADR 0004.
This specification describes only the current format.
Golden examples of every packet live in ../fixtures/protocol/, one .bin file per packet (a few packets have extra variants). The server's codec writes them and its tests check that encoding still produces the same bytes; every other implementation can check itself against the same files. Each packet section links its examples.
Pages
- Connection: handshake, server query, player IDs, disconnect, acks and keepalive, authentication.
- Player: the player's own state and its relay, streaming, spawn and death, and the server-applied player state.
- Combat: shots, projectiles, damage, health and armor, weapons, explosions and fire.
- Vehicles: vehicle state relay, seats, damage, attachments, trains and the server-applied vehicle state.
- World: pickups, clock and weather, garage doors, world markers, colshapes, objects and waypoints.
- Chat, HUD and dialogs: chat and commands, nametags, the player list, actions, dialogs, menus, offers and HUD elements.
- Resource files and views: resource client files and resource views.
- Map editing: the in-game map editor.
Packet IDs
0xFF is outside the revision: the server query keeps its own frozen format, so a launcher of any revision can read it.
Reliable event lane
Discrete events travel on the reliable event lane (ADR 0007); a packet is on the lane when its layout carries event_id: u32. Continuous state (PlayerSync, RemotePlayerSync and the other frame packets) stays unreliable and latest-wins and does not use the lane.
Each connection has one stream per direction. All reliable packets of that direction share the stream and are ordered by its event_id. The scheme is Go-Back-N with a cumulative ack: ClientAck acknowledges the Server → Client stream, ServerAck the Client → Server stream.
event_idis a 1-based, monotonic position in the stream and starts again at 1 on every connection. Events therefore need no generation tag: the session token on Client → Server packets and the per-connection streams keep a reconnect or a reusedplayer_idapart from the previous connection.- Receiver. It delivers only the next expected
event_id. A duplicate (< expected) or an event ahead of a gap (> expected) is discarded unapplied. Every reliable packet it receives, delivered or not, is answered with an ack that carries the highestevent_iddelivered without a gap. Re-acking a duplicate repairs a lost ack; a gap holds the ack in place, so the sender resends from the gap. - Sender. It assigns
event_id, keeps each event until it is acked, and resends an event once its age since the last send reaches the RTO of 250 ms (fixed, not adapted to the round trip). An ack removes every pending event withevent_id <= ack. There is no per-event give-up; a dead peer is removed by the disconnect rules. - Resend timing on the server. The server checks for due resends every 100 ms, so a lost event goes out again 250–350 ms after its last send. Per connection and check it resends at most 32 due events, oldest first.
- No gaps. A sender that refuses to queue an event does not consume an
event_idfor it, so the stream never has a gap. - Server queue limits. With 256 unacked events on a connection, the server drops new droppable events (such as chat, skills, pickups and wanted level) instead of queueing them, without disconnecting. Events that must not be lost (such as lifecycle edges, satchel detonation and the clock) are still queued up to 512 unacked; reaching 512 disconnects the client. The server also disconnects a client whose unacked events have waited 15 s without an ack moving the queue, even if other packets keep the connection alive.
Sequence and generation
Continuous state needs protection against UDP reordering and against a player_id that is reused by a new player. RemotePlayerSync carries the subject's player_token (its generation) and a seq. Tokens come from one server-wide counter, so they are monotonic: a later generation of any player_id has a larger token. Other Server → Client packets about a player carry its player_token too, so a delayed packet cannot resurrect a reused player_id.
The receiver keeps per player_id:
gen: theplayer_tokenof the live generation;last_seq: the last appliedseqof that generation;- a tombstone: the
player_tokenof the generation that has just left.
Rules:
RemotePlayerSync(id, token, seq). A packet whose token equals the tombstone is dropped: a departed player is not resurrected. With a live generation,token == genapplies the packet only ifseqis newer thanlast_seqin serial-number arithmetic (the wrappingu32difference, read asi32, is positive);token > genis a new generation (theidwas reused, possibly with itsPlayerLeavelost) and is adopted;token < genis a straggler of an earlier owner and is dropped. With no live generation the packet is adopted (gen = token,last_seq = seq).PlayerLeave(id, token). The receiver removes the remote player only whentoken == genand ignores the packet otherwise. After the removal the tombstone holds that generation, so delayed packets of it are dropped; adopting a new generation clears the tombstone.
Because tokens are monotonic, a reordered packet older than the live generation never replaces it. The tombstone covers only the most recent departed generation: a straggler of an earlier one that arrives while the id has no live generation is adopted, and the next owner's larger token replaces it.
PlayerStreamOut removes a remote player from view but keeps its generation; the last_seq it carries stops stragglers from showing the player again, and a newer frame shows it again.
Strict decoding
Both sides decode strictly: a malformed packet is dropped whole, never partially applied.
- Exact length. A packet must be exactly as long as its layout: a fixed-size packet exactly its size, a packet with a variable part exactly the length its count and length fields give. A short packet or one with trailing bytes is dropped.
- Finite floats. A
Frame(PlayerSync,RemotePlayerSync) with any non-finitef32(NaN or infinity) is dropped, and so is aVehicleState(VehicleFrame,RemoteVehicleFrame). Other packets state their float checks in their own sections. - Unknown or empty. An unknown
packet_idor an empty datagram is dropped. A revision mismatch is refused at the handshake, as described above.
