From 4f0d46024bd3e76fb017f5f0481ac6daa5f944c3 Mon Sep 17 00:00:00 2001 From: serge_shubin Date: Mon, 23 Mar 2026 05:41:18 +0800 Subject: [PATCH] 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) --- code/server/server.h | 1 + code/server/sv_main.c | 3 +++ code/server/sv_netdemo.c | 49 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/code/server/server.h b/code/server/server.h index ac787c9..3fd1106 100644 --- a/code/server/server.h +++ b/code/server/server.h @@ -349,6 +349,7 @@ void SVD_StopRecord_f( void ); void SVD_RecordFrame( void ); void SVD_ResetDeltaState( void ); void SVD_AutoRecord( void ); +void SVD_CaptureServerCommand( const char *cmd ); void SVD_Play_f( void ); void SVD_StopPlay_f( void ); void SVD_Stop_f( void ); diff --git a/code/server/sv_main.c b/code/server/sv_main.c index da5e89a..7d980b2 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -177,6 +177,9 @@ void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) { return; } + // capture broadcast commands for demo recording + SVD_CaptureServerCommand( (char *)message ); + // hack to echo broadcast prints to console if ( com_dedicated->integer && !strncmp( (char *)message, "print", 5) ) { Com_Printf ("broadcast: %s\n", SV_ExpandNewlines((char *)message) ); diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index 75067ac..0ea122f 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -78,6 +78,9 @@ typedef struct { qboolean active; } svdPlayerState_t; +#define SVD_MAX_SERVERCMDS 64 +#define SVD_MAX_SERVERCMD_LEN 1024 + typedef struct { // recording fileHandle_t recordFile; @@ -87,6 +90,10 @@ typedef struct { svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states + // buffered server commands for current frame + char serverCmds[SVD_MAX_SERVERCMDS][SVD_MAX_SERVERCMD_LEN]; + int numServerCmds; + // playback fileHandle_t playFile; qboolean playing; @@ -368,6 +375,15 @@ static void SVD_WriteFrame( fileHandle_t f ) { } } } + + // write buffered server commands (chat, prints, etc.) + SVD_WriteShort( f, (short)demo.numServerCmds ); + for ( i = 0; i < demo.numServerCmds; i++ ) { + short len = (short)( strlen( demo.serverCmds[i] ) + 1 ); + SVD_WriteShort( f, len ); + FS_Write( demo.serverCmds[i], len, f ); + } + demo.numServerCmds = 0; } // --------------------------------------------------------------- @@ -488,6 +504,21 @@ 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. */ +/* +Capture a broadcast server command for the current frame. +Called from SV_SendServerCommand when cl == NULL (broadcast). +*/ +void SVD_CaptureServerCommand( const char *cmd ) { + if ( !demo.recording ) { + return; + } + if ( demo.numServerCmds >= SVD_MAX_SERVERCMDS ) { + return; // overflow, drop command + } + Q_strncpyz( demo.serverCmds[demo.numServerCmds], cmd, SVD_MAX_SERVERCMD_LEN ); + demo.numServerCmds++; +} + void SVD_ResetDeltaState( void ) { if ( !demo.recording ) { return; @@ -770,6 +801,24 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) { } } + // read server commands (chat, prints, etc.) and replay to spectator + { + short numCmds = SVD_ReadShort( f ); + for ( i = 0; i < numCmds; i++ ) { + short len = SVD_ReadShort( f ); + char buf[SVD_MAX_SERVERCMD_LEN]; + if ( len > 0 && len < (short)sizeof(buf) ) { + FS_Read( buf, len, f ); + buf[len - 1] = '\0'; + // send to the spectator client + if ( demo.spectatorClientNum < sv_maxclients->integer + && svs.clients[demo.spectatorClientNum].state >= CS_PRIMED ) { + SV_SendServerCommand( &svs.clients[demo.spectatorClientNum], "%s", buf ); + } + } + } + } + return qtrue; }