Commit graph

28 commits

Author SHA1 Message Date
fe628a2cc4 Remove PVS data from demo format, derive during playback
Stop recording currentOrigin, absmin, absmax, linked per entity
(44 bytes per moving entity per frame). During playback, G_RunFrame
computes currentOrigin via BG_EvaluateTrajectory and calls
trap_LinkEntity to register in BSP for PVS.

svFlags still recorded (1-bit change flag + 4 bytes when changed).
Entity linking moved from SVD_ReadFrame (server, no trajectory eval)
to G_RunFrame demo mode (game module, has BG_EvaluateTrajectory).

Scan all MAX_GENTITIES during playback since recorded entities may
have indices above level.num_entities (game-module-spawned count).

Demo format bumped to v3. Significant file size reduction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 05:00:21 +08:00
c4dca5f950 Remove unnecessary SNAPFLAG_RESET_ENTITIES on unpause
Entity positions change by one server frame (50ms) on unpause —
normal interpolation handles this fine. Only SNAPFLAG_SERVERCOUNT
is needed to hard-reset the client's time delta.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 04:10:50 +08:00
490fcd9bde Smooth unpause: reset client time delta and entity interpolation
On unpause, toggle SNAPFLAG_SERVERCOUNT and set SNAPFLAG_RESET_ENTITIES.
In CL_ParseSnapshot, detect SERVERCOUNT toggle and hard-reset
cl.serverTimeDelta instead of letting CL_AdjustTimeDelta slowly drift.
During pause, the delta drifted because snapshots had frozen serverTime
while cls.realtime advanced. Without the hard reset, it took 1-2 seconds
of choppy interpolation to re-sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 03:59:44 +08:00
7141d941a3 Demo pause: freeze svs.time, hold demo data consumption
svdemo_pause toggles playback pause. When paused:
- svs.time frozen so entity trajectories freeze correctly
- No demo frames consumed (SVD_PlaybackFrame returns early)
- Game frame still runs at frozen time for spectator movement
- No time jump on unpause — svs.time resumes from where it was

Spectator movement degrades during pause (200ms PmoveSingle cap)
— will be resolved by client-owned camera in a future change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 03:28:32 +08:00
8388292198 Remove stale comment and unused nextFrameTime field
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:21:37 +08:00
c6af63ae42 SNAPFLAG_RESET_ENTITIES: no-interpolation reset without gamestate resend
Replace the gamestate resend approach for map_restart with a custom
snapshot flag (SNAPFLAG_RESET_ENTITIES, bit 4 in snapFlags byte).

Server side (sv_netdemo.c):
- On restart frame, OR the flag into svs.snapFlagServerBit (one-shot)
- Cleared at the start of the next playback frame

Client side (cg_snapshot.c):
- CG_SetNextSnap: clear currentValid for all entities when flag is set,
  making them all "new" — existing interpolation check at line 228
  sets interpolate=qfalse, CG_TransitionEntity calls CG_ResetEntity
- Also set cg.nextFrameTeleport=qtrue to prevent playerstate
  interpolation during follow mode

No loading screen, no lost frames, no gamestate resend. Entities and
playerstate both snap to correct positions on map_restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:19:36 +08:00
0b4eb7b69f Start demo playback only when spectator enters game
Move pause check from SV_Frame (which blocked connection handshake)
into SVD_PlaybackFrame. The server runs frames normally so clients
can connect and load, but demo data isn't consumed until a client
reaches CS_ACTIVE. The spectator sees the demo from the very first
frame without missing any gameplay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 23:46:46 +08:00
fcd5fc6ce8 Fix spectator display and recorded spectator handling
- Set sv_demoplaying before devmap so game module knows during
  ClientConnect/ClientBegin
- Call ClientUserinfoChanged after forcing spectator team so
  CS_PLAYERS configstring has correct team value for cgame
- Record sanitized playerState for spectators (pm_type=PM_SPECTATOR,
  PERS_TEAM=TEAM_SPECTATOR) so they show correctly on scoreboard
  instead of appearing as regular players from follow-mode corruption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 23:44:22 +08:00
d78f5c7aaf Fix pause deadlock: check CS_CONNECTED not CS_ACTIVE
CS_ACTIVE requires a fully connected client, but clients go through
CS_CONNECTED → CS_PRIMED → CS_ACTIVE. If the server is paused
waiting for CS_ACTIVE, the connection handshake never completes.
Check >= CS_CONNECTED to allow connecting clients to unpause.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:28:17 +08:00
300e7c431a Pause demo playback when no spectators are watching
svdemo_pauseEmpty (default: 1) pauses frame processing when no
client is CS_ACTIVE during demo playback. Prevents the demo from
advancing with nobody watching. Time residual is cleared so the
demo resumes from where it paused when a spectator connects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:25:16 +08:00
b79eb77bb3 Remove spectatorClientNum hack
spectatorClientNum was hardcoded to MAX_CLIENTS-1 but the spectator
actually connected at a different slot. All skip checks using it
were no-ops protecting the wrong slot. Removed entirely:

- Zombie slots handle player slot reservation
- G_RunFrame recreates the spectator entity via ClientThink_real
- PlayerState injection writes all slots (spectator's ps gets
  overwritten by ClientThink_real anyway)
- SVD_SpectatorClientNum accessor removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:20:52 +08:00
4d89d35a6d Send gamestate resend on map_restart (matches real server behavior)
A real map_restart sends SV_SendClientGameState to all clients,
which triggers CL_ParseGamestate → CL_ClearState on the client,
wiping all snapshot and entity history. Previously we only forced
non-delta snapshots which doesn't clear cgame's entity state.

Now we call SV_SendClientGameState for all active clients on the
restart frame, exactly matching real server behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:15:31 +08:00
595cf9864b Reset playback delta state on map_restart marker
The recording resets its delta state on map_restart (writes from
zero baseline). The playback must do the same, otherwise the
delta decoder uses stale pre-restart state as baseline, producing
corrupt entity data and preventing EF_TELEPORT_BIT from being
decoded correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:11:08 +08:00
5a18130ba0 Force non-delta snapshot on map_restart during playback
snapFlagServerBit toggle alone doesn't clear client entity
interpolation state. Also reset deltaMessage=-1 for all active
clients, forcing the next snapshot to be full (non-delta). The
client receives deltaNum<=0, clears old entity state, and
renders all entities at their new positions without interpolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:08:34 +08:00
479443513a Toggle snapFlagServerBit on map_restart during demo playback
Record a per-frame restart flag (1 byte) set by SVD_ResetDeltaState
when map_restart occurs. During playback, toggle svs.snapFlagServerBit
so the client treats the next snapshot as a fresh baseline — no
interpolation of entities from pre-restart positions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 06:03:45 +08:00
cc081ddee4 Fix chat capture: capture any target, deduplicate in buffer
Per-client chat is sent to every connected client in a loop.
Capture chat/tchat commands regardless of target clientNum, and
deduplicate by checking if the exact string is already buffered
for the current frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:55:19 +08:00
8b7ec11034 Fix server command replay: broadcast instead of targeting slot 63
spectatorClientNum was hardcoded to MAX_CLIENTS-1 (63) but the
spectator actually connects at the first free slot after zombie
reservations. Broadcasting with SV_SendServerCommand(NULL, ...)
reaches the spectator regardless of their actual slot number.
Zombie clients (< CS_PRIMED) are skipped by the broadcast loop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:50:15 +08:00
0d1f1d515e Restore svs.time sync for trajectory interpolation
Reverting the svs.time change from the bugfix commit — entity
trajectories (rockets, grenades, bobbing items) need svs.time to
match recorded time for client-side interpolation. The zombie
timeout issue is already handled by skipping SV_CheckTimeouts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:47:51 +08:00
4f0d46024b Record and replay broadcast server commands (chat, prints)
Capture broadcast server commands (chat, print, cp, etc.) from
SV_SendServerCommand when cl==NULL. Buffer up to 64 commands per
frame. Written after configstrings in the demo file, replayed to
the spectator client during playback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:41:18 +08:00
330cc30ae7 Optional LZ4 compression for demo files
Per-frame entity and playerState blocks are compressed with LZ4
when svdemo_compress is set (default: 1). The block format writes
[original_size][compressed_size][data] — compressed_size=0 means
uncompressed. Playback auto-detects based on header flags.

Demo format bumped to version 2 with SVDEMO_FLAG_COMPRESSED flag.
Version 1 (uncompressed) demos are no longer compatible.

Uses the lz4.c/lz4.h library already in the server code directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:22:41 +08:00
e58414f564 Auto-record demos with svdemo_autorecord cvar
Set svdemo_autorecord 1 to automatically record a demo on every
map load. Demo files are named <mapname>_YYYYMMDD_HHMMSS.svdm
in the svdemos/ directory.

Refactored SVD_Record_f to use SVD_StartRecording helper so both
manual and auto recording share the same code path. Also fixed
prevPlayers delta state not being cleared on recording start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:11:27 +08:00
5987109014 Filter CS_SERVERINFO/SYSTEMINFO in per-frame configstring changes
The per-frame configstring path in SVD_ReadFrame was not filtering
dangerous configstrings like SVD_ApplyConfigstrings does. When a
recorded map_restart changed CS_SERVERINFO (containing maxclients),
applying it overwrote maxclients=64 with the recorded value,
triggering a latched restart loop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:06:27 +08:00
a1167ff398 Fix 8 netdemo bugs found in code review
1. File handle leak: SVD_Play_f opened file twice, first handle leaked.
   Fix: memset demo state before opening.

2. svdemo_stop now handles both recording and playback via SVD_Stop_f.
   Playback stop disconnects client to return to menu.

3. Zombie client timeout: skip SV_CheckTimeouts during playback so
   reserved player slots aren't freed.

4. Buffer overflow: increase entity buffer to MAX_GENTITIES*300 and
   playerState buffer to MAX_CLIENTS*600 for worst-case first frame.
   Made static to avoid stack overflow.

5. svs.time jump: don't overwrite svs.time with recorded time.
   Server time advances normally, avoiding timeout/heartbeat issues.

6. map_restart: SVD_ResetDeltaState clears entity/player delta state
   so next frame writes full states, preventing corrupt deltas.

7. Demo end and manual stop both disconnect the client.

8. SV_Shutdown cleans up active recording/playback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:03:33 +08:00
b48a0575f1 Proper demo playback cleanup and state reset
- Extract SVD_CleanupPlayback for shared cleanup logic
- Called on demo end (SVD_PlaybackFrame) and manual stop
- Frees zombie client slots, saved configstrings, file handle
- Clears sv_demoplaying cvar so game module exits demo mode
- Hook into SV_Shutdown to clean up on server shutdown/map change
- Allows playing another demo after one finishes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 04:55:12 +08:00
1a186eeb81 Fix player disconnect handling during demo playback
Clear the game module's playerState when a recorded player
disconnects, so G_RunFrame sees commandTime=0 and marks them
as CON_DISCONNECTED. Without this, disconnected players stayed
visible on the scoreboard indefinitely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 04:48:35 +08:00
1f8c8aea1c Record playerStates for scoreboard and future follow mode
Record delta-compressed playerState_t for each active player per
frame using MSG_WriteDeltaPlayerstate. During playback, inject
into game module via SV_GameClientNum and mark players as
CON_CONNECTED with correct team. CalculateRanks and the
scoreboard now show recorded players with scores.

This also lays the groundwork for player-follow spectating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 04:46:40 +08:00
de9863da57 Delta-compress netdemo entity states
Use MSG_WriteDeltaEntity/MSG_ReadDeltaEntity for entity state
serialization. Only changed fields are written per frame. PVS
fields (svFlags, linked, origin, absmin, absmax) also use a
1-bit change flag to skip when unchanged.

Reduces a 10-second demo from ~1400KB to ~52KB (27x smaller).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 04:36:02 +08:00
a8044aad8b Server-side demo recording and playback (netdemo)
Records full entity state array each server frame, enabling
free-camera demo playback from any viewpoint.

Recording:
- svdemo_record <name> / svdemo_stop
- Captures entityState_t + PVS fields (svFlags, linked,
  currentOrigin, absmin, absmax) for all active entities
- Records configstring changes per frame
- File format: svdemos/<name>.svdm

Playback:
- svdemo_play <name>
- Loads map with maxclients=64, reserves recorded player slots
  (CS_ZOMBIE with safe rate/timing) so spectator gets a high slot
- Injects recorded entities into sv.gentities each frame with
  SV_LinkEntity for PVS visibility
- Re-applies demo configstrings (CS_PLAYERS etc.) after map load,
  skipping CS_SERVERINFO/CS_SYSTEMINFO to avoid latch restarts
- Game module runs in demo mode (sv_demoplaying cvar): G_RunFrame
  only processes spectator movement, skips all entity logic
- Spectator forced to TEAM_SPECTATOR on connect (ClientBegin)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 04:28:55 +08:00