Original version of document written by Pim Goossens in 2024. License: public-domain/CC0 (https://creativecommons.org/publicdomain/zero/1.0/) --- Forsaken demo files are essentially logs of network packets sent or received by the player. Since the network protocol has changed over time, so has the demo file format, despite always having the same version number. A few definitions for types referenced below: vec: vector, 12 bytes; 3 floats for X,Y,Z quat: quaternion, 16 bytes; 4 floats for W,X,Y,Z sp3: short position vector, 6 bytes: X,Y,Z rounded to 16-bit integers su3: short unit vector, 6 bytes; X,Y,Z scaled (* 32767) to 16-bit sq: short quaternion, 8 bytes; W,X,Y,Z scaled the same as su3 x16: 8.8 fixed point (a scaling factor of 256) The byte order is little-endian; columns are: - @: offset - #: size in bytes - T: data type - ?: description Demo file header: @ # T ? -- -- --- ------------------------------------------ 0 4 u32 0x0109 (DEMO_MULTIPLAYER_VERSION) 4 2 u32 CopyOfSeed1 (?) 6 2 u32 CopyOfSeed2 (?) 8 4 u32 RandomPickups (0 or 1) 12 8 u64 Enabled pickups, 1 bit for each 20 4 u32 Flags (0=deathmatch; Team/CTF/Bounty?) 24 2 u16 RandomStartPosModify (?) 26 Short level name + NUL Every event starts with a header containing a timestamp, its size, and an ID associated with the player sending or receiving it. The size field is useful when skipping past unknown data. Event header (16 bytes): @ # T ? -- -- --- ------------------------------------------ 0 8 u64 Timestamp (in microseconds?) 8 4 u32 Size (not including the header itself) 12 4 u32 Sender ID The next byte indicates the event type, and the format of the data following it depends on that, although most (all?) start off with a player ID. Player name (10 bytes): Note: the timestamp for these is always just 1 @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0x7f 1 1 u8 Player number 2 8 NUL-terminated player name (max. 7 characters) padded to 8 bytes Bike number / score update (8 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xf4 (MSG_BIKENUM) 1 1 u8 Player number 2 2 u16 Number of kills (or frags/score?) 4 2 u16 Number of deaths 6 1 u8 Bike number 7 1 u8 ? Player joining (12 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0x11 (MSG_HEREIAM) 1 1 u8 Player number (255)? 2 1 ? 3 1 ? 4 4 u32 Sender ID (same as in the header) 8 4 ? (0 or 1) Player number (2 bytes): Note: sent only for players joining an ongoing game @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xfb 1 1 u8 Player number "Very short" position update (40 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xf5 (MSG_VERYSHORTUPDATE) 1 1 u8 Player number 2 1 ? 3 1 ? 4 4 u32 Ship state (normal/dying/dead, orbitals, power, invuln/cloak, etc) 8 1 u8 Ship status (normal, dying, dead, etc) [1] 9 1 u8 Group (section of the level) the player is in 10 6 sp3 Position 16 6 su3 Movement direction (may be zero if player is not moving) 22 6 su3 Rotation (XYZ * 32767 / 45) (unreliable?) 28 2 s16 Banking * 32767 / 30 30 2 x16 Speed (scales direction to get velocity) 32 8 sq Ship orientation "Very short" weapon fire + position update (44 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xf6 (MSG_VERYSHORTFUPDATE) 1 1 u8 Player number 2 1 ? 3 1 ? 4 4 u32 Ship state (normal/dying/dead, orbitals, power, invuln/cloak, etc) 8 1 u8 Primary weapon (defined in primary.h) 9 1 u8 Secondary weapon (defined in secondary.h) 10 1 u8 Group 11 1 u8 Trojax charge level (the next 6 fields are the same as for 0xf5) 12 6 sp3 Position 18 6 su3 Movement direction (may be zero if player is not moving) 24 6 su3 Rotation (XYZ * 32767 / 45) (unreliable?) 30 2 s16 Banking * 32767 / 30 32 2 x16 Speed (scales direction to get velocity) 34 8 sq Ship orientation 42 2 ? Position update (68 bytes): @ # T ? -- -- ---- ------------------------------------------ 0 1 u8 0x11 (MSG_UPDATE) 1 1 u8 Player number 2 1 ? 3 1 ? 4 4 u32 Ship state (normal/dying/dead, orbitals, power, invuln/cloak, etc) 8 1 u8 Game status (synchronizing, playing, between levels, ...) 9 1 u8 Group 10 2 ? 12 12 vec Position 24 12 vec Velocity 36 12 vec Rotation (unreliable?) 48 4 f32 Banking (always extremely close to zero?) 52 16 quat Ship orientation Weapon fire + position update (68 bytes): @ # T ? -- -- ---- ------------------------------------------ 0 1 u8 0x1f (MSG_FUPDATE) 1 1 u8 Player number 2 1 ? 3 1 ? (the next 5 fields are the same as for 0xf6) 4 4 u32 Ship state (normal/dying/dead, orbitals, power, invuln/cloak, etc) 8 1 u8 Primary weapon 9 1 u8 Secondary weapon 10 1 u8 Group 11 1 u8 Trojax charge level (the next 5 fields are the same as for 0x1f) 12 12 vec Position 24 12 vec Velocity 36 12 vec Rotation (unreliable?) 48 4 f32 Banking 52 16 quat Ship orientation Mine laid (76 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0x1f (MSG_FUPDATE) 1 1 u8 Player number 2 1 ? 3 1 ? 4 2 u16 Owner type (defined in primary.h; 1=ship) 6 2 u16 Owner (always same as player number?) 8 2 u16 Bullet ID 10 2 u16 Group 12 12 vec Position 24 12 vec Offset? 36 12 vec Direction? 48 12 vec Up? 60 12 vec Drop direction? 72 4 u32 Secondary weapon (defined in secondary.h) Status update (28 bytes): Note: sent when player changes status (joining -> playing -> leaving) @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xbb (MSG_STATUS) 1 1 u8 Player number 2 1 ? 3 1 ? 4 4 u32 1 for host, 0 for everyone else 8 2 u16 Game status (synchronizing, playing, between levels, ...) 10 2 ? 12 2 ? 14 2 ? 16 2 ? 18 2 ? 20 1 ? 21 1 ? 22 1 ? 23 1 ? 24 4 f32 ? Ship destroyed (20 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0x77 (MSG_SHIPDIED) 1 1 u8 Player number 2 1 u8 Player number of killer 3 1 u8 ? 4 1 u8 Weapon type (0=primary, 1=secondary) 5 1 u8 Weapon 6 2 u16 ? Drop pickup (56 bytes): Note: usually seen when a ship is destroyed @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0x88 (MSG_DROPPICKUP) 1 1 u8 Player number 2 1 ? 3 1 ? 4 2 s16 ? 6 2 ? 8 2 u16 Group 10 2 ? 12 12 vec Position 24 12 vec Direction? 36 4 f32 Speed? 40 2 s16 ? 42 4 u32 Sparkle (pickup spawn effect; 0 or 1)? 46 2 ? 48 4 f32 ? 52 2 s16 ? 54 2 ? Remove pickup (8 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0x99 (MSG_KILLPICKUP) 1 1 u8 Player number 2 2 s16 Owner (previous, or -1 if none?) 4 2 u16 Pickup ID 6 2 u16 Style (picked up vs despawned?) Titan bits (184 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xfd (MSG_TITANBITS) 1 1 u8 Player number 2 1 ? 3 1 ? 4 2 u16 Owner type (defined in primary.h; 1=ship) 6 2 u16 Owner (always same as player number?) 8 2 u16 Bullet ID 10 2 u16 Group 12 12 vec Position 24 12 vec Offset? 36 12 vec Up? 48 12 vec Drop direction? 60 12 vec Direction of (shrapnel) bit 1 72 12 vec Direction of (shrapnel) bit 2 ... 156 12 vec Direction of (shrapnel) bit 9 (NUMTITANBITS - 1) 168 12 vec Direction of (shrapnel) bit 10 (NUMTITANBITS) 180 4 u32 Secondary weapon ID of bits (defined in secondary.h) Shockwave (28 bytes): @ # T ? -- -- --- ------------------------------------------ 0 1 u8 0xff (MSG_SHOCKWAVE) 1 1 u8 Player number 2 2 ? (always 0, 1, or 2) 4 2 u16 Owner (always same as player number?) 6 2 u16 Group 8 12 vec Position 20 4 ? (always 255) 24 4 f32 Shockwave size Chat message (131 bytes): @ # T ? -- --- --- ------------------------------------------ 0 1 u8 0x3f (MSG_TEXTMSG) 1 1 u8 Player number 2 128 Message text, NUL terminated but padded to 128 bytes 130 1 u8 Message type (0-2=taunt1/2/3, 5=normal chat message)