From a1167ff39800df89ded327b1e66630086b2bf51e Mon Sep 17 00:00:00 2001 From: serge_shubin Date: Mon, 23 Mar 2026 05:03:33 +0800 Subject: [PATCH] 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) --- code/server/server.h | 2 ++ code/server/sv_ccmds.c | 5 ++++- code/server/sv_main.c | 6 +++-- code/server/sv_netdemo.c | 48 +++++++++++++++++++++++++++++++--------- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/code/server/server.h b/code/server/server.h index c27ec1e..00ce6bd 100644 --- a/code/server/server.h +++ b/code/server/server.h @@ -347,8 +347,10 @@ void BotImport_DebugPolygonDelete(int id); void SVD_Record_f( void ); void SVD_StopRecord_f( void ); void SVD_RecordFrame( void ); +void SVD_ResetDeltaState( void ); void SVD_Play_f( void ); void SVD_StopPlay_f( void ); +void SVD_Stop_f( void ); qboolean SVD_PlaybackFrame( void ); qboolean SVD_IsRecording( void ); qboolean SVD_IsPlaying( void ); diff --git a/code/server/sv_ccmds.c b/code/server/sv_ccmds.c index 060c030..b55d15f 100644 --- a/code/server/sv_ccmds.c +++ b/code/server/sv_ccmds.c @@ -280,6 +280,9 @@ static void SV_MapRestart_f( void ) { sv.state = SS_GAME; sv.restarting = qfalse; + // reset demo delta state so next frame writes full entities + SVD_ResetDeltaState(); + // connect and begin all the clients for (i=0 ; iinteger ; i++) { client = &svs.clients[i]; @@ -739,7 +742,7 @@ void SV_AddOperatorCommands( void ) { // server-side demo recording/playback Cmd_AddCommand ("svdemo_record", SVD_Record_f); - Cmd_AddCommand ("svdemo_stop", SVD_StopRecord_f); + Cmd_AddCommand ("svdemo_stop", SVD_Stop_f); Cmd_AddCommand ("svdemo_play", SVD_Play_f); } diff --git a/code/server/sv_main.c b/code/server/sv_main.c index 84a9312..da5e89a 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -850,8 +850,10 @@ void SV_Frame( int msec ) { time_game = Sys_Milliseconds () - startTime; } - // check timeouts - SV_CheckTimeouts(); + // check timeouts (skip during demo playback — zombie slots would be freed) + if ( !SVD_IsPlaying() ) { + SV_CheckTimeouts(); + } // send messages back to the clients SV_SendClientMessages(); diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index 2796848..6fe31ea 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -175,7 +175,7 @@ static void SVD_WriteFrame( fileHandle_t f ) { sharedEntity_t *ent; short numChanges; msg_t msg; - byte msgBuf[MAX_GENTITIES * 64]; // delta-compressed entities are small + static byte msgBuf[MAX_GENTITIES * 300]; // worst case: all entities full write from baseline int msgLen; SVD_WriteInt( f, svs.time ); @@ -253,7 +253,7 @@ static void SVD_WriteFrame( fileHandle_t f ) { // write player states (delta compressed) { msg_t psmsg; - byte psBuf[MAX_CLIENTS * 256]; + static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline int psCount = 0; MSG_Init( &psmsg, psBuf, sizeof(psBuf) ); @@ -387,6 +387,18 @@ void SVD_StopRecord_f( void ) { /* Called from SV_Frame() after the game has run its frame. */ +/* +Reset delta compression state. Call on map_restart so the next +recorded frame writes full entity/player states from baseline. +*/ +void SVD_ResetDeltaState( void ) { + if ( !demo.recording ) { + return; + } + Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); + Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); +} + void SVD_RecordFrame( void ) { if ( !demo.recording ) { return; @@ -472,7 +484,7 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) { int i, entNum, msgLen; sharedEntity_t *ent; msg_t msg; - byte msgBuf[MAX_GENTITIES * 64]; + static byte msgBuf[MAX_GENTITIES * 300]; entityState_t newEs; serverTime = SVD_ReadInt( f ); @@ -480,7 +492,9 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) { return qfalse; // end of demo } - svs.time = serverTime; + // don't overwrite svs.time — it advances normally via SV_Frame. + // the recorded serverTime is consumed but not applied, avoiding + // time jumps that break client timeouts and heartbeats. numEnts = SVD_ReadShort( f ); // read compressed message @@ -588,7 +602,7 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) { // read player states (delta compressed) { msg_t psmsg; - byte psBuf[MAX_CLIENTS * 256]; + static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline int psMsgLen; psMsgLen = SVD_ReadInt( f ); @@ -679,17 +693,14 @@ void SVD_Play_f( void ) { Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s ); + memset( &demo, 0, sizeof(demo) ); + len = FS_FOpenFileRead( name, &demo.playFile, qtrue ); if ( !demo.playFile || len <= 0 ) { Com_Printf( "ERROR: couldn't open %s.\n", name ); return; } - // read the header (populates demo.playMapName etc.) - memset( &demo, 0, sizeof(demo) ); - demo.playFile = 0; // re-open after memset - len = FS_FOpenFileRead( name, &demo.playFile, qtrue ); - if ( !SVD_ReadHeader( demo.playFile ) ) { FS_FCloseFile( demo.playFile ); demo.playFile = 0; @@ -770,6 +781,22 @@ void SVD_StopPlay_f( void ) { SVD_CleanupPlayback(); Com_Printf( "Server demo playback stopped.\n" ); + + // disconnect to return to main menu + Cbuf_ExecuteText( EXEC_APPEND, "disconnect\n" ); +} + +/* +Unified stop command: stops recording or playback, whichever is active. +*/ +void SVD_Stop_f( void ) { + if ( demo.recording ) { + SVD_StopRecord_f(); + } else if ( demo.playing ) { + SVD_StopPlay_f(); + } else { + Com_Printf( "Not recording or playing a server demo.\n" ); + } } /* @@ -790,6 +817,7 @@ qboolean SVD_PlaybackFrame( void ) { if ( !SVD_ReadFrame( demo.playFile ) ) { Com_Printf( "Server demo playback finished.\n" ); SVD_CleanupPlayback(); + Cbuf_ExecuteText( EXEC_APPEND, "disconnect\n" ); return qfalse; }