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>
This commit is contained in:
serge_shubin 2026-03-23 05:41:18 +08:00
parent 60b50ce224
commit 4f0d46024b
3 changed files with 53 additions and 0 deletions

View file

@ -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 );

View file

@ -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) );

View file

@ -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;
}