Skip to content

Combat ​

Shots, projectiles, satchels, stealth kills, damage reports, the server's health ledger, the weapon inventory commands and the gamemode's area effects (explosions, fire). Damage is victim-confirmed: every client computes the hits on its own player and reports them; the server validates each report against an accepted cause, keeps the health ledger and commits deaths.

Damage model ​

Damage groups and relations ​

A player's damage group is the id of its team while that team's friendly fire is off, and 0 otherwise. The client does not receive teams, their names or colors; the group is visible to everyone the player is shown to (RemotePlayerSync), so the membership of such teams is not secret. Two different players with the same non-zero group are teammates: damage between them is refused before it applies (no hit reaction either), on the client's own player and on the remote players, and no DamageReport is sent.

There is no dedicated packet: a team change broadcasts nothing, whatever the team's size. Instead the server stamps the relation "the sender is a teammate of THIS recipient" (relation_rev, teammate) into every RemotePlayerSync, RemoteVehicleFrame, RemoteWeaponShot, RemoteVehicleShot, RemoteProjectileCreate and RemoteSatchelDetonate; when the recipients of one relay differ in relation, the relay goes out as two packets. CreateExplosion and CreateFire carry the relation of their creator to the recipient: a server-made effect has no creator frame that would deliver a team change made just before it. The revision is global, grows on every change of any group and is compared with wraparound past u32::MAX (like seq, see Sequence and generation).

  • A shot and a volley are judged by their own packet: the stamp in that packet decides their damage; anything that arrives later does not change it.
  • Everything else (melee, ramming, explosions and fire) is judged by the value with the newest relation_rev for the same generation of the sender: a late packet does not roll it back, and neither does a packet of an older generation of that id (player_token compared with wraparound past u32::MAX). Frames arrive at least every 100 ms (heartbeat), so there is no silent sender with a stale relation: without a frame there are no new actions of it, and a new recipient learns the relation from the first frame. A server effect with a creator updates this value itself.

A hit from an action accepted before a change is judged by the old value: the victim may apply it, and the server then discards that report. A grenade in flight and a fire already burning are judged by the value at the moment of the hit; the discrepancy is at most one heartbeat.

Health accounting and death ​

The server keeps the health and armor of each player's life (ADR 0045); the gamemode reads this ledger and the relay carries it as Frame.hp / armor.

TopicRule
Start of a life100 / 0 from the spawn or respawn grant. The first frame of the life does not touch the ledger, and reports numbered before its damage_seq (no further than 64 past the last accepted one) belong to the previous life and are discarded
Commands (SetHealth, SetArmor, GiveHealth, GiveArmor)Set or add at once, when sent; each carries the connection's next rev. Available to the gamemode only in a life the server counts: after the first frame of the life, while the player is alive and not counted dead; otherwise refused
Owner's frameLowers the ledger to its hp / armor, never raises it. A frame above the ledger raises the god_mode flag (up to 1.0 above the ledger is tolerated: the game rounds health), except a frame that precedes a report credited less than 1 s ago
Accepted damage reportA report sent with its hit (bullet, melee, explosion, ramming by a player) deducts its loss from the ledger (from armor as much as the ledger has, the rest from health), but not below its hp_after / armor_after. A sum of fire, spray or environmental damage lowers the ledger to its hp_after / armor_after. A report numbered no newer than the damage_seq of a credited frame does not touch the ledger (the frame already showed its loss) and only gives attribution. A rejected report does not touch the ledger
FloorAn accepted bullet report (WeaponShot, VehicleShot) and an accepted melee report deduct at least a floor, armor first. Bullet: 0.33 × the weapon's damage from the WeaponProperties revision the shot echoed, at the shooter's tier (within 5 s of a skill change, the lower of the two tiers); vehicle guns at the std tier; the Predator's guns (52): 0.33 × 15; a shotgun: one pellet. A blow with any melee weapon, the fist or a gun butt: 1.32 (0.33 × the weakest blow). The melee floor and the Predator bullet floor are multiplied by the class multiplier (DamageMultipliers); while the victim's client may still apply the previous table, by the lower of that and the current one. A gun-butt blow takes no multiplier. 0.01 is subtracted from every floor. A report that arrives after a frame credited without it collects the rest of the floor beyond what was deducted by the frame that passed its damage_seq after the last vital command; the loss such a frame showed counts toward the floor of only the first report after the frame starts, so one hit's loss never pays another hit's floor. The body part and the claimed damage do not affect the floor; fire, spray, explosion, ramming and environmental reports have no floor
RevisionA frame or report echoing a vital_rev older than the last command sent does not touch the ledger (a report still gives attribution) until the command is settled: echoed, 2 s after the ClientAck of its event, or 15 s after it was sent. A frame does not take an echo newer than what was sent, and a report with such an echo is discarded
OrderA frame with a damage_seq newer than the last credited report waits for that report up to 1 s (a new frame does not restart the wait), then is credited on its own; one loss is deducted once
Death by the ledgerWhen the ledger's health reaches 0, on an accepted vehicle explosion (VehicleExplode with an explosion) for its occupants (except the killer's teammates while friendly fire is off), or on an accepted stealth kill, the server waits for the DeathReport for 2 × RTT + 1 s (RTT counted up to 1 s; a stealth kill adds 10 s for its animation), then commits the death itself: the victim gets SetHealth(0) (critical; the victim gets a respawn only after its ClientAck of that event), everyone else PlayerWasted with the default pose (0, 0, 15) (a stealth kill: the knife victim pose (4, 87, 352)), the gamemode player_death with the killer from the last damage. Leaving the vehicle after an accepted explosion does not cancel the death; of two counted deaths, the earlier deadline applies
LeavingA player counted dead leaves dead: player_death comes before its player_disconnect

Packets ​

WeaponShot (0x09) ​

Client → Server, unreliable. The computed ray of one instant-hit shot: the client lets its weapon fire and sends the resulting segment start → end, not the trigger press. No damage travels on the wire: every recipient replays the ray and computes the hit itself, and the victim reports the damage (DamageReport). hit is only the shooter's claim for the gamemode and is not relayed.

[u8  packet_id = 0x09]
[u64 session_token]    // the sender's token; a mismatch drops the packet
[u32 order]            // the sender's monotonic shot counter, shared with VehicleShot (duplicate / stale guard)
[u32 weapon]           // weapon id
[f32 start_x/y/z]      // start of the ray (the muzzle)
[f32 end_x/y/z]        // far end of the ray (the resolved aim or impact point)
[u16 props_rev]        // weapon properties revision applied when the shot was fired (see WeaponProperties)
[u8  hit_kind]         // where the ray ended: 0 the world or nothing, 1 a player, 2 a vehicle, 3 an object; any other value drops the packet
[u32 hit_id]           // id of the player, vehicle or object; must be 0 with hit_kind = 0
[u32 hit_gen]          // player_token of the player or epoch of the vehicle (<= 255); must be 0 with hit_kind = 0 and 3 (object ids are not reused)

Total: 52 bytes. A hit_kind / hit_id / hit_gen combination outside these rules, or a non-finite start / end (NaN / Inf), drops the packet at decode. The server identifies the sender by address and session_token and validates the shot (ADR 0024); a failed check drops the packet:

  • lifecycle: the shooter is spawned and alive (a corpse and a player not yet spawned do not fire). Shots of a frozen player and of a player driving a remote-controlled vehicle are ignored;
  • weapon: a hitscan weapon only, pistols to rifles 22–34 and the minigun 38 (a spoofed or garbage weapon never reaches the recipients), and not disallowed by the gamemode (WeaponProperties);
  • inventory: the weapon is in the server-owned inventory with total_ammo > 0. It need not equal the armed weapon: an accepted shot itself moves the armed weapon, which closes the switch-and-fire-at-once race;
  • seat: a driver fires only 28 / 29 / 32; a passenger fires 22..=33, and not from an aircraft or a boat; nobody fires from a train;
  • origin: on foot, start within 5 + (speed of the last frame in m/s, capped at 60) × 0.25 of the last position; seated, within 25 of the vehicle;
  • range: the ray length within the per-weapon limit (a conservative server table, src/weapons.rs), multiplied by the ratio of the gamemode's range of the weapon to the game's at the shooter's skill tier (for the sniper rifle 34 the limit only grows). The table is the one of revision props_rev (WeaponProperties); within 5 s of a skill change, the more lenient limit of the old and the new tier applies;
  • cadence and order: at least the per-weapon minimum interval since the last shot, multiplied by the ratio of the gamemode's shorter fire loop to the game's (never below 15 ms), and a strictly increasing order. A seated shooter uses half the drive-by interval instead, if the gamemode set one (DriveByIntervals). On average the shooter also holds the game's fire interval of the weapon at its tier, scaled the same way (a seated shooter with a gamemode drive-by interval: that interval): each shot costs 0.9 of it, a burst of up to three shots is tolerated, and switching to another weapon gives up to 1 s of headroom.

An accepted shot takes one round from the server's inventory (one packet = one trigger pull = one round, a shotgun's pellets included), raises the gamemode event and is relayed as RemoteWeaponShot to the players the shooter is shown to (Player streaming). The event is player_weapon_fire(player, weapon, x, y, z, hit_player, hit_vehicle, hit_object) with x, y, z = end; only the resource that created the object receives hit_object. The hit claim does not affect acceptance of the shot. The event names a target only when the claim checks out:

  • a player: the token matches, the player is alive, shown to the shooter, not the shooter itself, and end is no further than 4 m from the path it moved along during the shooter's RTT (at most 1 s) + 500 ms;
  • a vehicle: the epoch matches, the vehicle is shown to the shooter and its center is no further than 40 m from end;
  • an object: shown to the shooter (ObjectState), not attached to a bone (bone-attached copies have no collision), not broken before the window starts (a broken object counts only for the part of the window before it broke, at the place and parent recorded when it broke; a restored one only from the moment it was restored), and end within the same window is no further than the model's reach (the far corner of its collision from the model origin, a server table) + 0.5 m from the object's path, including the moves that ended or were replaced within this window (a teleport does not join the path). For an attached object the path is that of the root of its chain, with the offsets along the chain summed and the root's own margin: a player — 4 m from its path, a vehicle — 40 m from its last center. A change of parent (attach, detach) or of an attached object's offset within the window keeps the previous root with its previous reach valid until the moment of the change, for the object and everything attached to it: the ObjectState may not have reached the shooter yet. RTT within the window counts as at most 1 s.

Otherwise the event goes out without a target.

Example: weapon_shot.bin, weapon_shot_object.bin.

RemoteWeaponShot (0x0A) ​

Server → Client, unreliable.

[u8  packet_id = 0x0A]
[u32 player_id]
[u32 player_token]     // the shooter's generation
[u32 order]            // relayed from WeaponShot
[u32 weapon]
[f32 start_x/y/z]
[f32 end_x/y/z]
[u16 props_rev]        // weapon properties revision of the accepted WeaponShot
[u32 relation_rev]     // see Damage groups and relations
[u8  teammate]
[ShotRewind rewind]    // 34 bytes: where the recipient stood at the moment the shooter saw

Total: 82 bytes. The recipient replays the shot with the weapon properties table of its props_rev (WeaponProperties, "Revision on relay").

ShotRewind — the rewind block, shared with RemoteVehicleShot (ADR 0041):

[u8  rewound]          // 0 none, 1 the recipient's body, 2 the vehicle it sits in; any other value drops the packet
[f32 x/y/z]            // position of the body or the vehicle (as VehicleState.pos); 0 with rewound = 0
[u32 vehicle_id]       // the vehicle with rewound = 2, else 0
[u8  vehicle_epoch]    // its generation with rewound = 2, else 0
[f32 rot[4]]           // its rotation, unit quaternion x, y, z, w (as VehicleState.rot) with rewound = 2, else zeros

NaN / Inf in any field drops the packet. The server computes the block per recipient for the moment (now − the shooter's RTT, at most 400 ms) from the path it accepted: linear between frames (rotation along the shortest arc); past the last frame the position continues at the same velocity for at most 150 ms and the rotation stays the last one. A recipient with a rewind gets a datagram of its own.

  • rewound = 1: the recipient is on foot, in the world, has no server teleport its client has not reached yet, and its body is no further than 3 m from the ray extended by 10 m. For the trace, the recipient puts its body there if that is no further than 30 m from it.
  • rewound = 2: the recipient sits in a vehicle the shooter is not in, the vehicle is neither frozen nor wrecked, the server has its frames (a gamemode move and a freeze change start the path anew from where it stands), and its center is no further than 30 m from the ray extended by 10 m. For the trace, the recipient puts its copy of that vehicle there if it sits in it, the generation matches, the quaternion's norm is within 0.5..1.5 and the shift is at most 60 m; it moves itself along with the vehicle.

Every other recipient gets rewound = 0. The recipient applies the shot only for the current live generation of the sender (player_token checked as in RemotePlayerSync; a straggler of a departed or replaced owner is dropped) and drops duplicates and stale shots by order (per id, strictly greater only; a late shot is worse than a lost one, there is no reliable layer). The stored ray and the order mark reset on a generation change and on leave; PlayerStreamOut resets the ray and raises the mark to its last_shot_order: a shot of the same generation delayed past a stream-out and a new stream-in is stale. The client replays the ray from the sender's remote player with the weapon of the packet; each recipient computes the hit and the damage itself (the victim then reports it).

Example: remote_weapon_shot.bin.

ProjectileCreate (0x30) ​

Client → Server, on the reliable event lane.

[u8  packet_id = 0x30]
[u64 token]            // the sender's session_token
[u32 event_id]         // reliable C→S lane order; the server answers ServerAck
[u32 weapon]           // projectile type: grenade 16, teargas 17, molotov 18, rocket 19 (launcher 35 / 36), heat-seeking rocket 20 (launcher 36), satchel 39; from a vehicle: 19, 20, flare 58
[f32 origin_x/y/z]     // spawn point of the projectile (at / in front of the thrower)
[f32 velocity_x/y/z]   // the projectile's launch velocity as the game resolved it, world units per physics step (1/50 s)
[f32 forward_x/y/z]    // rocket orientation (forward = aim); zero for thrown projectiles and the flare
[u8  from_vehicle]     // 1: launched by the vehicle the sender drives; 0: by the sender itself; any other value is malformed
[u32 target_id]        // vehicle the heat-seeking rocket 20 locked onto; 0xFFFFFFFF none
[u8  target_epoch]     // its life at the moment of the lock; 0 with no target, otherwise malformed
[u32 seen_event]       // the last S→C reliable event the client had received when it threw

Total: 63 bytes. Non-finite origin, velocity or forward drops the packet. A projectile is a discrete event whose loss would be visible, so it rides the same reliable C→S lane (the same event_id stream) as ClientChat, ClientCommand and DeathReport, not best-effort like WeaponShot. The server acknowledges every delivered event (ServerAck) and then validates it; a failed check drops the throw without relaying it or spending ammo:

  • the sender is spawned and alive; a frozen sender's throw is dropped;
  • seen_event is not past the last event sent to the client, and not before the last scope barrier (below);
  • on foot: not seated and not driving a remote-controlled vehicle; weapon is a projectile weapon (16..=20, 39); the backing weapon is not disallowed by the gamemode and is in the server inventory with ammo; origin within the same reach as a WeaponShot start;
  • from a vehicle: the vehicle rules below, origin within 50 of the vehicle;
  • velocity magnitude at most the speed of what fires it (the thrower or the vehicle, per step) + 0.6, and never above 5.0 per step;
  • the lock: a heat-seeking rocket 20 has a valid target (below), anything else has none;
  • cadence: on foot at least 300 ms after the last accepted throw; from a vehicle at most 10 launches per second.

The server normalizes forward of rockets 19 / 20 to unit length (with no direction, zero) and zeroes it for everything else. The wire carries the projectile type (rocket = 19), not the launcher (35 / 36) that fired it: for thrown weapons (16 / 17 / 18 / 39) the held weapon is the projectile, while a rocket comes from a launcher in the heavy slot. The server resolves the backing launcher for the ammo check: 19 → 36 if the sender holds it, else 35 (both are valid: a heat-seeking launcher without a lock fires an ordinary rocket 19); 20 → 36. An accepted throw on foot takes one round from the backing weapon, raises the gamemode event player_projectile_create and is relayed to the players the sender is shown to as RemoteProjectileCreate. An accepted satchel arms the satchel latch (SatchelDetonate). The blast of a grenade or rocket, the fire of a molotov and the gas of teargas are attributed to the creator on every client; their damage is victim-confirmed.

Heat-seeking rocket and aircraft ordnance. The game locks onto vehicles only. A rocket 20 must carry a target: a vehicle of the life the client locked (target_epoch), not wrecked and streamed to the sender; everything else must carry none; otherwise the packet is dropped (a target that blew up and respawned does not inherit the lock). from_vehicle = 1: the sender sits in the driver's seat of an unwrecked vehicle that fires this — the Hunter (425) with rocket 19, the Hydra (520) with rockets 19 and 20 and flare 58; the inventory is not checked or spent (aircraft ordnance is unlimited in the game), origin within 50 of the vehicle, at most 10 launches per second (the game fires a pair of rockets every 500 ms and a flare every 2 s). An accepted rocket 19 / 20 becomes a damage cause (DamageReport): launched from a vehicle, its reach grows by the distance it flies at its launch speed in 3 s; a rocket 20 also counts near its target vehicle, wherever that is. The flare deals no damage.

Scope barrier. Every time a live player changes virtual world or interior, the server sends it ScopeChanged as a barrier: on a TeleportPlayer into another interior, after the teleport; on a respawn into another interior, on the new grant (a repeated grant sets no barrier). The connect spawn sets none: before it the player has no throws or traces of them. A throw with seen_event below the event_id of the last barrier was made before the client learned of the change and is rejected: it belongs to the scope the player left. A seen_event above the last event sent to the client is malformed; the throw is rejected.

Example: projectile_create.bin.

RemoteProjectileCreate (0x31) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x31]
[u32 player_id]
[u32 player_token]     // the sender's generation
[u32 event_id]         // reliable S→C lane order (per recipient; the client answers ClientAck)
[u32 weapon]
[f32 origin_x/y/z]
[f32 velocity_x/y/z]   // the sender's resolved launch velocity, applied as is
[f32 forward_x/y/z]    // rocket orientation (aim); zero for thrown projectiles
[u32 launcher_id]      // vehicle that launched the projectile; 0xFFFFFFFF on foot
[u8  launcher_epoch]   // its life; 0 with no vehicle, otherwise malformed
[u32 target_id]        // the target of a rocket 20 the server accepted; 0xFFFFFFFF none
[u8  target_epoch]     // the target's life; 0 with no target, otherwise malformed
[u32 relation_rev]     // see Damage groups and relations
[u8  teammate]

Total: 68 bytes. The recipient spawns its own projectile with the sender's remote player of this generation as the creator and sets the state on the projectile itself: its velocity is velocity, and a rocket's orientation is forward (a rocket flies along its orientation, not its velocity; without it the rocket would follow the remote player's facing). With a launcher, the creator is the recipient's copy of that vehicle of the same life, whether or not the remote player sits in it on that client; while there is no such copy, the projectile waits for it up to 2 s and is then dropped. With a target, the projectile homes on the recipient's copy of that target of the same life (otherwise it flies straight).

The explosion is attributed to the creator; a rocket from a vehicle is attributed to its shooter: at its explosion the creator becomes the sender's player (rather than the vehicle or whoever drives it at the moment of impact), so the damage, fire and vehicle explosion it causes carry the shooter. A rocket 20 from a hand-held launcher explodes at full strength, as on the shooter's client. A client that cannot attribute these explosions this way neither sends nor applies such rockets. The damage is victim-confirmed through DamageReport; there is no extra damage packet. There are no duplicates (the reliable lane delivers each event once).

A satchel (39) travels in the same ProjectileCreate (ballistic, forward = 0) but does not explode on its own; it is detonated by SatchelDetonate. A satchel thrown on foot that the server rejected exists only on the thrower's client: the server removes it with DestroySatchels carrying that throw's event_id.

Example: remote_projectile_create.bin.

SatchelDetonate (0x32) ​

Client → Server, on the reliable event lane (the same lane as ProjectileCreate).

[u8  packet_id = 0x32]
[u64 token]            // the sender's session_token
[u32 event_id]         // reliable C→S lane order

Total: 13 bytes. A press of the detonator (weapon 40). Only a request: the client sends it when it has at least one satchel of its own, at most once per second, and explodes nothing itself — its satchels explode on the server's answer (DetonateSatchels). The packet carries no position and no ammo. The server checks the lifecycle (spawned and alive), the satchel latch (armed by an accepted ProjectileCreate(39)) and the rate (at most once per 1 s per connection: a refused event keeps the satchels, so without the rate limit a client could flood it), then asks the gamemode through the refusable player_detonate_satchels(player) (ADR 0038). A refusal changes nothing: the satchels stay on every copy and the latch stays armed. An allowed detonation consumes the latch (all satchels blow up at once: one detonation per batch of throws), becomes a damage cause (DamageReport) and goes to the owner as DetonateSatchels and to the players it is shown to as RemoteSatchelDetonate. Both are critical: never dropped, a queue overflow disconnects (Disconnect). dlmp.detonate_satchels(player) takes the same path without the event.

Satchels go away without exploding, and the latch is cleared: on the owner's death (the owner gets DestroySatchels after everything the server sent before the death; observers drop them on PlayerWasted), when the owner's remote player is hidden from an observer (together with the remote player) and when the owner changes virtual world or interior (the owner gets ScopeChanged; observers lose the remote player). A detonation an observer received before PlayerWasted / PlayerStreamOut / PlayerLeave explodes the satchels before they are removed.

Example: satchel_detonate.bin.

RemoteSatchelDetonate (0x33) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x33]
[u32 player_id]
[u32 player_token]     // the owner's generation
[u32 event_id]         // reliable S→C lane order (per recipient; the client answers ClientAck)
[u32 relation_rev]     // see Damage groups and relations
[u8  teammate]

Total: 18 bytes. A detonation of a player's satchels, sent to the players it is shown to (the owner itself gets DetonateSatchels). The recipient detonates only the satchels of this generation of the sender's remote player, never anyone else's. The explosions are attributed to the sender's remote player; damage is victim-confirmed, there is no extra damage packet.

Example: remote_satchel_detonate.bin.

StealthKillRequest (0x34) ​

Client → Server, on the reliable event lane (the same lane as SatchelDetonate and ClientChat, whose ordering gives replay and duplicate protection).

[u8  packet_id = 0x34]
[u64 session_token]
[u32 event_id]         // reliable C→S lane order; the server answers ServerAck
[u32 target_id]        // the victim
[u32 target_token]     // the victim's generation (protects against id reuse)

Total: 21 bytes. Only an attempt: the attacker sees on its own client that a stab from behind looks valid and asks the server. The client does not kill the other player itself and plays no presentation before the answer.

The server re-checks everything against its own state and trusts no claim of the client: both players are spawned and alive and in the same world scope, not in the same damage group (Damage groups and relations), the attacker has weapon 4 armed (in hand, not merely in the inventory), the distance is ≤ 2.5, the attacker is behind the victim (dot(victim.pos − attacker.pos, victim.forward) > 0, where forward = (−sin yaw, cos yaw) from the victim's heading), the victim does not run (move_state < 6) and does not crouch, the attacker's cadence (at most one accepted attempt per 2.5 s) and the victim is not already doomed by another confirmed kill (an accepted kill holds its victim for up to 14 s or until its death). A refusal is logged with the exact rule and changes nothing; the reliable event is acknowledged all the same.

A request that passes spends the attacker's cadence and goes to the refusable player_stealth_kill(player, victim) (ADR 0038): on a refusal nobody plays anything and the victim is not reserved. After the handlers the server checks the rules again (except the cadence): a handler may have changed the world, the weapon, or kicked a player.

On acceptance the server stamps the victim's last damage {killer = attacker, weapon = 4, body_part = 9}, so the death that follows gives the gamemode the right attribution through the usual player_death, with no extra field on the wire.

Example: stealth_kill_request.bin.

RemoteStealthKill (0x35) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x35]
[u32 attacker_id]
[u32 attacker_token]
[u32 victim_id]
[u32 victim_token]
[u32 event_id]         // reliable S→C lane order (per recipient; the client answers ClientAck)

Total: 21 bytes. A stealth kill the server confirmed, sent to both participants and everyone the victim is shown to (the participants are in the same scope). Each recipient resolves both ids relative to itself — its own id is the local player, any other id that player's remote player — and plays the paired presentation (the attacker half on one, the victim half on the other). This packet is the only thing that authorizes the presentation.

Delivery is never-drop (critical): a lost confirmation would leave a kill that one client played and the other did not.

The death is not carried here. The victim half kills the victim itself (about 2 s in, at the end of the animation), and the victim's client reports that death the usual way: DeathReport → PlayerWasted with the pose weapon = 4 / anim_group = 87 / anim = 352. There is no separate stealth flag on the wire. Without a DeathReport the server commits the death itself (Health accounting and death).

Example: remote_stealth_kill.bin.

DamageReport (0x0B) ​

Client → Server, unreliable. Damage confirmed by the victim (bullet, melee, explosion, fire, spray, vehicle, environment). Only the damaged player reports its OWN damage: it played the hit, so it is the authority of what the hit did. An attacker never sends trusted damage. The server validates and attributes the report by weapon class: a bullet is correlated with a recent WeaponShot of the named attacker; melee (0 = fists, 1..=15 = melee weapons and gifts, 46 = parachute; shot_order = 0) has no WeaponShot, so proximity, possession of the weapon and cadence are checked instead. Both are validation and attribution; an accepted report also lowers the server's ledger (Health accounting and death). A report is accepted only from a spawned, living victim, with a loss of at most 2 × 200 (twice the largest absolute health) and body_part 0..=9. A report whose attacker of the same generation is in the victim's damage group (Damage groups and relations) is discarded before the class checks, with no player_damage and no last damage. For a bullet, the ray extended by 10 m must have passed the victim's path, since the moment the shooter saw (RemoteWeaponShot), within 3 + the victim's speed (its vehicle's, if seated; m/s, capped at 60) × 0.15, plus 5 more when seated. The target health model is ADR 0045.

[u8  packet_id = 0x0B]
[u64 session_token]       // the VICTIM's token; a mismatch drops the packet
[u32 damage_seq]          // the victim's report counter, assigned at send time; sums of fire, spray and environmental
                          // damage are sent before a report sent at once, so the numbers follow the order of the damage
[u32 attacker_id]         // the attacker's player_id; NO_PLAYER = 0xFFFFFFFF for environmental damage
[u32 attacker_token]      // the attacker's generation; 0 for environmental damage
[u32 shot_order]          // >= 1: the order of the WeaponShot (a bullet); 0: no bullet -> melee, gun butt, explosion, fire, ...
[u32 weapon]              // weapon id / damage cause
[u32 body_part]           // body part hit, 0..=9
[f32 requested_damage]    // damage requested (before armor)
[f32 applied_hp_delta]    // actual change of health (negative)
[f32 applied_armor_delta] // actual change of armor (negative)
[f32 hp_after]            // health after the hit
[f32 armor_after]         // armor after the hit
[u16 vital_rev]           // revision of the last health / armor command the client had taken when the damage came (as Frame.vital_rev)

Total: 55 bytes. A report with hp_after <= 0 is not sent in this packet but as LethalDamageReport. The server identifies the victim by address and session_token, drops non-finite numbers (NaN / Inf) at decode, then branches by the class of weapon:

Bullet (hitscan, shot_order != 0). The server finds the attacker by (attacker_id, attacker_token) and checks that it has a recent accepted shot — a WeaponShot or a ray of a vehicle's guns (VehicleShot) — with the same weapon, within the correlation window (500 ms) and with the same order; the server remembers up to the last 32 rays. Hits from the Predator's guns arrive with weapon = 52 and are correlated the same way; for them shot_order must be the non-zero number of the ray (a report with 0 is discarded).

Melee, or a hitscan weapon with shot_order == 0. The latter is a gun butt / pistol whip (a close blow attributed to the held firearm, with no bullet; a bullet always carries order >= 1). Both branches check that the attacker is alive and spawned (the same generation token), holds this weapon in its server-owned inventory (fists 0 always), is within 6 of the victim and is not over the melee cadence (one blow per 40 ms); a self-attribution is not accepted.

Explosion (weapon = 51), fire (37) and spray (41 spraycan, 42 extinguisher), shot_order = 0. Credited if the attacker of the same generation (it need not be alive) has an accepted cause near the victim:

DamageCauseWindowDistance of the victim from the cause point
51ProjectileCreate 16 (grenade)3 s60
51ProjectileCreate 19 (rocket)4 s120
51ProjectileCreate 20 (heat-seeking rocket)11 s120
51SatchelDetonate1 s—
37ProjectileCreate 18 (molotov), 19 / 20 (rocket), or PlayerSync with weapon = 37 and Fire (flamethrower in the inventory)65 s80
41, 42PlayerSync with the same weapon and Fire (the weapon in the inventory)2 s15
51a vehicle explosion credited to the attacker (VehicleExplode)1 s30
51CreateExplosion with this creator1 s30
51a Rhino cannon shot (VehicleShot), from the ray segment1 s30
37a vehicle explosion credited to the attacker65 s80
37CreateExplosion with this creator65 s80
37CreateFire with this creator65 s80
37a Rhino cannon shot, from the ray segment65 s80

The distance of a rocket launched from a vehicle is larger by the distance it flies at its launch speed in 3 s; a rocket 20 also matches a victim within the same distance of its target vehicle. The freshest matching cause is taken; its weapon (16, 19, 20, 39, 18, 37; for CreateFire 37; for a vehicle explosion, a Rhino cannon shot and CreateExplosion 51), not the damage class, goes to player_damage and the last damage. The cause and the victim must be in the same scope (virtual world + interior). An explosion is credited to a victim at most twice and only within 1 s of the first accepted hit on it; after that the cause is spent for that victim. The victim sums fire and spray per attacker and weapon and sends them at most every 250 ms; the server rejects such reports from one attacker with the same weapon more often than every 200 ms, except the first lethal one of the life (hp_after <= 0). A firing flamethrower leaves a cause every 20 units of its path (spray every 5), so fire left behind stays within reach. The fire window also covers fire that spread from the cause (its author is the author of the cause).

Ramming (weapon = 49), running over or helicopter blades (50), shot_order = 0. The attacker is the driver of the vehicle that dealt the damage; credited if it is of the same generation, alive, sits in the driver's seat (seat 0) of an unwrecked vehicle in the victim's scope, within 25 of the victim by the last accepted position. A passenger, or a vehicle without a driver, gives no attacker.

Environmental damage, attacker_id = NO_PLAYER, attacker_token = 0, shot_order = 0. Damage no player dealt: drowning (53) and falls (54) always, and fire (37), explosions (51), ramming (49) and running over (50) when their source is not a remote player (the world, the victim itself, a vehicle without a driver). The victim sums it per cause and sends it at most every 250 ms; the server accepts only these six causes and at most every 200 ms per cause, except the first lethal one of the life. There is no attacker: player_damage comes with attacker = nil, and the last damage becomes {no attacker, cause, body_part}, so a death whose last damage was environmental comes without a killer and with the cause in weapon (the last blow decides). Environmental damage does not clear the vehicle killer window (VehicleExplode).

On success the server deduplicates (a real shot is credited to a victim once: the server remembers the (attacker_id, attacker_token, shot_order) credited to it within the correlation window, up to 64, and rejects a report beyond them; plus a monotonic damage_seq; the state advances only for an accepted report); otherwise it rejects the report. An accepted report also raises the gamemode event player_damage(victim, attacker, weapon, body_part, amount) and stamps the victim's last damage {attacker_id, attacker_token, weapon, body_part}, from which the server derives the killer and weapon of the death. The report lowers the ledger by the rules of Health accounting and death; a sum of fire or environmental damage uses the vital_rev of its last tick.

Example: damage_report.bin.

LethalDamageReport (0x44) ​

Client → Server, on the reliable event lane (the same stream as DeathReport).

[u8  packet_id = 0x44]
[u64 session_token]
[u32 event_id]            // position in the C→S reliable stream (shared with DeathReport)
[...]                     // the DamageReport fields after session_token, in the same order

Total: 59 bytes. A report after which the victim has hp_after <= 0: the same DamageReport, but on the reliable lane and queued before the DeathReport of this death, so the server credits it before committing the death and a lost datagram cannot give the death to a previous attacker or to nobody. The server acknowledges the stream (ServerAck), discards a report with hp_after > 0 and otherwise handles it as a DamageReport.

Example: lethal_damage_report.bin.

GiveWeapon (0x16) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x16]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[u32 weapon_type]      // grantable weapon id: 1..=46 except 19..=21
[u32 ammo]             // ammo, <= 65535
[u8  set_as_current]   // 1 = equip it, 0 = put it in the inventory (the held weapon stays)
[u16 rev]              // inventory revision AFTER this change; the client echoes it in Frame.weapon_rev

Total: 16 bytes. A weapon grant from the gamemode or the operator to the recipient's own player (recipient-implicit), multiplexed into the same reliable S→C stream. The server validates weapon_type before sending (1..=46, except 19 / 20 / 21, vehicle and aircraft weapons a player cannot hold) and ammo <= 65535: every legal server total must fit Frame.total (u16) without clamping, otherwise the reconciliation would read the clamp as spent ammo and lower the store. An invalid id is never sent. The client adds weapon_type with ammo to its player's inventory; set_as_current = 1 also equips it (with 0 the held weapon stays; the parachute is not equipped even with 1). Others see the held weapon through the usual frame sync. The server owns the inventory (ADR 0024): a give commits the whole slot into the store, after it is queued; the given ammo is added to the ammo the slot already holds when the slot has the same weapon or shares its ammo between weapons, otherwise it replaces it; rev is the new inventory revision (see weapon_rev in PlayerSync). Grants are events, each must apply, so the client applies them in order without dropping, not latest-wins. The gamemode grants weapons with player:give_weapon(weapon, ammo, set_current) (spawn loadouts and weapon pickups use the same call).

Example: give_weapon.bin.

GiveHealth (0x17) / GiveArmor (0x18) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x17 | 0x18]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[f32 amount]           // how much to add to the recipient's current health / armor
[f32 max]              // ceiling: the client clamps the sum to it and never lowers the value
[u16 rev]              // the connection's health / armor command revision (echoed in Frame.vital_rev)

Total: 15 bytes each. An additive reward (for example, a pickup) for the recipient's own player, multiplexed into the same reliable S→C stream. The client drops non-finite amount / max (NaN / Inf), then sets clamp(current + amount, current, max): it adds to the live value and clamps to max, but never lowers it below the current one (a value already above max gains nothing); the server adds to its ledger the same way. A dead client drops the grant, as with SetHealth. Grants are events: they apply in the same ordered queue as weapon grants. The server validates amount / max (finite, > 0, <= 200) before sending. An absolute set is SetHealth / SetArmor, not this packet. Server sources: ServerApi::add_health / add_armor (for example, a health or armor pickup).

Example: give_health.bin, give_armor.bin.

SetHealth (0x1E) / SetArmor (0x1F) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x1E | 0x1F]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[f32 value]            // absolute target value
[u16 rev]              // the connection's health / armor command revision (echoed in Frame.vital_rev)

Total: 11 bytes each. An absolute set on the recipient's own player (ADR 0045), multiplexed into the same reliable S→C stream. The client drops a non-finite value (NaN / Inf), then sets clamp(value, 0, 200) — lowering is allowed (unlike the additive GiveHealth / GiveArmor, which never lower). The server puts the same value into its ledger when sending; others see it in Frame.hp / armor. value = 0 on a living player kills it through the client's normal death path (DeathReport, ADR 0017); without a DeathReport the server commits the death itself (Health accounting and death). A set that arrives while the player is dead is dropped by the client (not deferred: the respawn stays server-gated), but its rev echo is still taken. It applies in the same single ordered queue as GiveHealth / GiveArmor (drop-while-dead for both, no never-lower for the set), so the reliable order between an additive grant and an absolute set of the same value holds. The server validates value (finite, 0..=200) before sending. Server sources: the sandbox /sethp / /setarmour and ServerApi::set_health / set_armor.

Example: set_health.bin, set_armor.bin.

DriveByIntervals (0x46) ​

Server → Client, on the reliable event lane, critical.

[u8  packet_id = 0x46]
[u32 event_id]               // position in the per-recipient S→C reliable stream
[u16 driveby_interval × 12]  // weapons 22..=33 in order: 0 = the on-foot rate, else ms between shots, 30..=5000

Total: 29 bytes. The server-wide drive-by rate table: how many ms pass between two shots from a vehicle, for the driver and the passengers, with each weapon (the driver fires only 28, 29, 32). 0 (the default) is the weapon's on-foot rate at the shooter's skill, which the client takes from the game; a number is set by the gamemode through dlmp.set_driveby_interval. The initial table rides in ServerHello; this packet goes to every connected client on a change. The client applies it to its own drive-by and to the pose of remote passengers; remote bullets are replayed from RemoteWeaponShot as they are. The client decoder rejects a value other than 0 outside 30..=5000 (and a ServerHello with one). A set interval replaces, for a seated shooter, the minimum interval of WeaponShot with half of it.

Example: driveby_intervals.bin.

DamageMultipliers (0xA0) ​

Server → Client, on the reliable event lane, critical. Per-class damage multipliers set by the gamemode.

[u8  packet_id = 0xA0]
[u32 event_id]          // position in the per-recipient S→C reliable stream
[f32 multipliers × 22]  // per class: 0..=15, 46, 37, 41, 42, 51, 52; 0..=10, 1 = the game's damage

Total: 93 bytes. Authority: the server (the gamemode, dlmp.set_damage_multiplier); the victim's client only applies it. The class is the weapon the damage arrives with, not its cause: melee (the fist, weapons 1..=15, parachute 46), fire 37 (molotov, flamethrower, a burning player, the fire of an explosion), spraycan 41, extinguisher 42, explosion 51 (grenade, satchel, rockets, vehicle explosion, the Rhino cannon, CreateExplosion), the Predator's guns 52. Firearm bullets take their damage from damage in WeaponProperties; a gun-butt blow has no multiplier.

The server sends the whole table to everyone on every change (setting the same value sends nothing) and to a new connection right after ServerHello if any class is not 1; a new connection starts with 1 for all. The client multiplies the damage of every hit on its own player by the class multiplier before applying it, whoever dealt it (a remote player, the player itself, the world); at 0 the hit is refused as for a teammate (Damage groups and relations): no damage, no reaction, no DamageReport. Deaths the server commits (a vehicle explosion with occupants, a stealth kill) do not depend on the multiplier. The server multiplies the floor of a melee blow and of a Predator bullet by it (Health accounting and death). Until the connection has acknowledged (ClientAck) the last table sent to it, and for 2 s after that, the floor takes the lower of the class multipliers sent to it since the last settled table and the one it had before them (for a new connection: 1): the hit may have been dealt under any of them. An unacknowledged table with no acknowledgement progress for 15 s disconnects the connection (Disconnect). Fire, spray and explosion damage stays the victim's observation. The client decoder rejects a packet with a value outside 0..=10 (or NaN); the server refuses such a change (dlmp.set_damage_multiplier → nil, err).

Example: damage_multipliers.bin.

TakeWeapon (0x24) / SetWeaponAmmo (0x25) / ClearWeapons (0x26) / SetArmedWeapon (0x27) ​

Server → Client, on the reliable event lane.

[u8  packet_id = 0x24]  // TakeWeapon
[u32 event_id]
[u32 weapon_type]      // grantable id (1..=46 except 19..=21)
[u16 rev]              // inventory revision after the change (echoed in Frame.weapon_rev; the same in all four)

[u8  packet_id = 0x25]  // SetWeaponAmmo
[u32 event_id]
[u32 weapon_type]      // grantable id
[u32 ammo]             // TOTAL ammo (<= 65535)
[u16 clip]             // explicit ammo in the clip; 0xFFFF = not set
[u16 rev]

[u8  packet_id = 0x26]  // ClearWeapons
[u32 event_id]
[u16 rev]

[u8  packet_id = 0x27]  // SetArmedWeapon
[u32 event_id]
[u32 weapon_type]      // 0 = fists, or a grantable id
[u16 rev]

Totals: 11 / 17 / 7 / 11 bytes. The removal, retune and switch half of GiveWeapon: all Server → Client, recipient-implicit, on the same reliable S→C stream as GiveWeapon / SetHealth (their own event_id, a shared ClientAck). The client applies them in one ordered queue with the gives and SetHealth, so the reliable order of actions on one inventory (give → take) and on one health value holds. After every SUCCESSFUL apply the client remembers the packet's rev and echoes it in Frame.weapon_rev (a failed give does not move the echo; a no-op "no such weapon" is a legitimate apply). Apply semantics:

  • TakeWeapon — remove one weapon (if it is held, switch to fists). Not in the inventory → no-op.
  • SetWeaponAmmo — ammo is the total (absolute set). An explicit clip (not the sentinel) sets the ammo in the clip, clamped to the total (the decoder rejects clip > ammo); with the sentinel the clip is only clamped to the total. Not in the inventory → no-op. Only the active weapon rides in Frame, so other players see the ammo of an inactive weapon only once it is equipped.
  • ClearWeapons — the whole inventory; the player ends up unarmed.
  • SetArmedWeapon — switch the held weapon by type; 0 = fists (always present); a type not in the inventory → no-op.

The server owns each player's inventory (13 slots + the armed weapon, ADR 0024): every change is committed to the store after it is queued and carries a new rev. The store stays live by reconciliation with what Frame shows under the revision gate (the armed weapon within the inventory, ammo only downward) plus one round off per accepted WeaponShot. A death does not clear the store (the corpse keeps its weapons); the respawn grant clears it, as the client clears its inventory on revival. Remote players show the resulting held weapon and ammo through the usual weapon state in Frame; a change to unarmed (active type → 0) shows fists. The server validates before sending: TakeWeapon / SetWeaponAmmo — a grantable id (as a give), SetWeaponAmmo.clip <= ammo; SetArmedWeapon — {0} ∪ grantable (fists are valid too). Server sources: the sandbox /takeweapon, /setammo, /clearweapons, /armed and the operator takeweapon, setammo, clearweapons, armed, through ServerApi::take_weapon / set_ammo / clear_weapons / set_armed_weapon.

Example: take_weapon.bin, set_weapon_ammo.bin, clear_weapons.bin, set_armed_weapon.bin.

CreateExplosion (0x5A) ​

Server → Client, on the reliable event lane. An explosion the gamemode made, sent to every spawned player in the explosion's scope (distance does not matter; a recipient whose queue is full skips it). Every recipient creates the explosion itself, so the damage to players and vehicles and the fire it starts are computed locally and victim-confirmed. With a creator, the recipient makes that player the creator of the explosion: the creator's own client its own player, everyone else the creator's remote player of the same generation; without such a remote player the explosion has no creator. There is no radius: the strength and the size come from the type.

[u8  packet_id = 0x5A]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[f32 x, y, z]          // explosion point
[u8  type]             // the game's explosion type 0..12 (0 grenade, 1 molotov, 2 rocket, 3 weak rocket, 4 car, 5 quick
                       // car, 6 boat, 7 aircraft, 8 mine, 9 object, 10 tank shell, 11 small, 12 tiny)
[u8  creator_present]  // 1: there is a creator, 0: none (id and token zeros); any other value is malformed
[u32 creator_id]
[u32 creator_token]    // the creator's generation
[u32 relation_rev]     // see Damage groups and relations; ignored without a creator
[u8  teammate]

Total: 32 bytes. A type > 12 and a point outside the world (non-finite, |x| or |y| > 6000, z outside −200..4000, the bounds of a vehicle frame) are dropped at decode. The server checks in create_explosion: the point in the world, the type 0..12, the interior 0..255; the creator is a spawned player in the explosion's scope (the default scope is the creator's, without a creator the main world). An explosion with a creator becomes that player's cause: explosion damage 51 near it and fire damage 37 from it are credited to the creator (DamageReport), as is a vehicle it blows up (VehicleExplode); the weapon in player_damage is 51.

Example: create_explosion.bin.

DetonateSatchels (0x5B) ​

Server → Client, on the reliable event lane, critical. A detonation of the recipient's own satchels (recipient-implicit); it pairs with RemoteSatchelDetonate to the observers (SatchelDetonate).

[u8  packet_id = 0x5B]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[u32 through_event]    // the last C→S reliable event of this player the server processed

Total: 9 bytes. The client explodes its satchels thrown by a ProjectileCreate with event_id ≤ through_event; one thrown later (the server has not accepted it yet, and others will see it after the detonation) stays.

Example: detonate_satchels.bin.

DestroySatchels (0x5C) ​

Server → Client, on the reliable event lane, critical. The recipient's own satchels the server does not have go away without exploding (recipient-implicit).

[u8  packet_id = 0x5C]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[u32 from_event]       // first C→S reliable event of the throws; 0 also covers satchels the server does not know of
[u32 through_event]    // last C→S reliable event of the throws

Total: 13 bytes. The client removes its satchels thrown by a ProjectileCreate with event_id in from_event..=through_event (with from_event = 0, also those that never reached the server). The server sends from = through = event_id for a rejected satchel throw, and 0..=the last processed event on the death of a player with satchels.

Example: destroy_satchels.bin.

CreateFire (0x5E) ​

Server → Client, on the reliable event lane. A fire the gamemode lit, sent to every spawned player in the fire's scope (distance does not matter; a recipient whose queue is full skips it), in order with the projectile events. From then on every client runs the fire on its own, unsynchronized: it lights it at the point, the fire does not spread and does not merge with a neighbor (otherwise it would diverge between clients), it sets players and vehicles right next to it on fire, and its damage is computed locally and victim-confirmed. With a creator, the recipient makes that player the fire's creator: the creator's own client its own player, everyone else the creator's remote player of the same generation; without such a remote player the fire has no creator. Under water and when the client's fire pool is full the fire does not light.

[u8  packet_id = 0x5E]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[f32 x, y, z]          // fire point
[f32 size]             // fire strength 1..3: flame size; burns ~5–6.5 s, then loses 1 every 7 s and goes out at <= 1
[u8  creator_present]  // 1: there is a creator, 0: none (id and token zeros); any other value is malformed
[u32 creator_id]
[u32 creator_token]    // the creator's generation
[u32 relation_rev]     // see Damage groups and relations; ignored without a creator
[u8  teammate]

Total: 35 bytes. A size outside 1..3 (or non-finite) and a point outside the world (as for CreateExplosion) are dropped at decode. The server checks in create_fire: the point in the world, size 1..3 (default 1.8), the interior 0..255; the creator is a spawned player in the fire's scope (the default scope is the creator's, without a creator the main world). A fire with a creator becomes that player's cause: fire damage 37 near it is credited to the creator (DamageReport); the weapon in player_damage is 37. A client far from the fire may put it out earlier (each client burns it down on its own).

Example: create_fire.bin.

ExtinguishFire (0x5F) ​

Server → Client, on the reliable event lane. The gamemode puts out fire around a point; sent to every spawned player in the scope, in order with CreateFire.

[u8  packet_id = 0x5F]
[u32 event_id]         // position in the per-recipient S→C reliable stream
[f32 x, y, z]          // center
[f32 radius]           // meters, (0, 100]

Total: 21 bytes. A radius outside (0, 100] and a point outside the world are dropped at decode; the server checks the same in extinguish_fire (default radius 1; the scope is the main world unless given). The client puts out every fire within the radius except burning players (a player's burning is SetOnFire / Frame.on_fire).

Example: extinguish_fire.bin.

WeaponProperties (0x60) ​

Server → Client, on the reliable event lane, to every connection, critical. Properties of a hitscan weapon at a skill tier, set by the gamemode.

[u8  packet_id = 0x60]
[u32 event_id]          // position in the per-recipient S→C reliable stream
[u8  weapon]            // 22..=34 or 38
[u8  skill]             // tier: 0 poor, 1 std, 2 pro; poor and pro only for 22..=32
[u16 mask]              // bit i = property i is set by the gamemode; bits 14..15 are 0
[f32 values[14]]        // the value of a set property, 0 for an unset one
[u16 rev]               // the properties revision this record belongs to
[u8  following]         // how many records of the same revision follow: 0..=35, 0 for a change

Total: 68 bytes. Authority: the server (the gamemode, dlmp.set_weapon_property); the client only applies it. The packet is the whole state of one record (weapon × tier). The server sends it to everyone on a change, and every record with at least one property set to a new connection right after ServerHello, all with the current revision and following counting down to 0; when the revision is not 0 and no record has a property set, a new connection gets one record (22, std) without properties as the carrier of the revision. A record without set properties means the game's values.

Revision. Every change is a new server revision (u16, from 0). The client applies the packets in reliable lane order together with the weapon actions (a GiveWeapon after a clip change gets the new clip). A revision is complete once its record with following = 0 and all before it are written; the client echoes the last complete revision in WeaponShot.props_rev / VehicleShot.props_rev (before the first: 0; a new connection: 0 again). A record the client failed to write holds itself and all following ones: no newer revision becomes complete until the record is written (retried every frame) or a new connection replaces the queue. The server checks a shot against the table of its revision: the current one, or one replaced no more than 5 s ago (a shot fired before a change applied and arriving after it is checked against the previous table); any other revision is dropped. A shot's revision may not be older than the revision of the connection's last accepted shot or gun volley (shared order) while that one is in force: the client never rolls an applied revision back. A revision's age counts from the current one, so the comparison survives u16 wraparound.

Revision on relay. RemoteWeaponShot / RemoteVehicleShot carry the props_rev of the accepted shot; the recipient replays it only with the table of that revision:

  • the revision is the last complete one — with the written table;
  • the revision is newer than the last complete one (the unreliable shot overtook the reliable WeaponProperties or the rest of its revision) — the shot waits for it to become complete, at most 500 ms, then is dropped;
  • the revision is older — the recipient rebuilds its table from the last up to 1024 written changes (including records of a revision not yet complete), substitutes it into the weapon table for the replay, then restores the written one. A revision the recipient never saw (before a gap in the numbering: a connect after revision 1 sends only the current table) or pushed out of the history is dropped.

A failed table write (the substitution or the restore after it) drops that shot; until the table is back to the written one, shots wait (at most 500 ms) and following revisions are not applied.

A cannon volley does not read weapon properties: the recipient does not check its props_rev.

iPropertyRangeiPropertyRange
0damage, per hit0..=1000, whole4–6fire_loop_start / _end, fire_time, s0..=5 / 0..=5 / 0..=2
1range, m1..=3507–9crouch_fire_loop_start / _end, crouch_fire_time, s0..=5 / 0..=5 / 0..=2
2accuracy0.01..=10010move_speed: the pace of walking while aiming (1 is the game's for most weapons, ~2 m/s)0..=2
3clip1..=1000, whole11flag move_and_aim: the weapon stays aimed while moving0 / 1
12flag move_and_fire: firing while moving0 / 1
13flag heavy: no sprint and no jump; std tier only, applies to all tiers0 / 1

The move_speed limit keeps aimed walking within the server's on-foot speed bound (PlayerSync): at 2, about 4 m/s. A flag is sent as 1 (on) or 0 (off); an unset flag is the game's.

The client decoder rejects a packet with a weapon or tier without properties, a property bit ≥ 14, a value out of range (or NaN), a fractional damage / clip, a flag other than 0 / 1, heavy on a tier other than std, a non-zero value of an unset property, and a fire loop with both ends set and the end not after the start. The client applies the set properties over the game's values of the record and does not apply a record whose loop, with the game's values, does not start before it ends; a new connection returns all records to the game's. The server refuses the same change (dlmp.set_weapon_property → nil, err).

A player fires at the tier its server-side weapon skill has reached (tiers as in the game: pro from the pro record's threshold, std from the std record's threshold, else poor). The server scales the bounds of WeaponShot (range and cadence, WeaponShot) and of VehicleShot (the range of vehicle guns) by the same records, and takes the clip of a new grant from the record of the player's tier.

Disallowed weapons. The gamemode disallows a grantable weapon (dlmp.set_weapon_allowed); this is not sent on the wire: the server takes the weapon from all holders (TakeWeapon, critical), refuses to give it and drops WeaponShot and ProjectileCreate (on foot) with it; a hit with it does not correlate, as there is no such shot and no such weapon in the inventory. Vehicle weapons (VehicleShot) are not affected.

Example: weapon_properties.bin.

Documentation licensed under CC BY 4.0. DarqlineMP is not affiliated with Rockstar Games or Take-Two Interactive. Grand Theft Auto is a trademark of Take-Two Interactive Software, Inc.