From fcd5fc6ce81b6c7984cac14cf62555e6f2abbd57 Mon Sep 17 00:00:00 2001 From: serge_shubin Date: Mon, 23 Mar 2026 23:44:22 +0800 Subject: [PATCH] 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) --- code/game/g_client.c | 4 ++-- code/server/sv_netdemo.c | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/code/game/g_client.c b/code/game/g_client.c index c48fcb7..719e382 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -1013,10 +1013,10 @@ void ClientBegin( int clientNum ) { client->pers.teamState.state = TEAM_BEGIN; // demo playback: force all clients to spectator + // demo playback: force to spectator and update configstring if ( g_svDemoPlaying.integer ) { client->sess.sessionTeam = TEAM_SPECTATOR; - client->ps.pm_type = PM_SPECTATOR; - client->ps.persistant[PERS_TEAM] = TEAM_SPECTATOR; + ClientUserinfoChanged( ent->client - level.clients ); } // save eflags around this, because changing teams will diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index 8b4fa9c..ba6c411 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -336,6 +336,23 @@ static void SVD_WriteFrame( fileHandle_t f ) { playerState_t *ps = SV_GameClientNum( i ); client_t *cl = &svs.clients[i]; qboolean active = ( cl->state >= CS_ACTIVE ); + qboolean isSpectator; + playerState_t specPs; + + // detect spectators: free cam or follow mode + isSpectator = active && ( ps->pm_type == PM_SPECTATOR || (ps->pm_flags & PMF_FOLLOW) ); + + // for spectators, record a sanitized ps so they appear on + // the scoreboard as spectators (follow mode corrupts their ps + // with the followed player's data) + if ( isSpectator ) { + Com_Memset( &specPs, 0, sizeof(specPs) ); + specPs.commandTime = ps->commandTime; + specPs.pm_type = PM_SPECTATOR; + specPs.persistant[PERS_TEAM] = TEAM_SPECTATOR; + specPs.clientNum = i; + ps = &specPs; + } if ( active ) { playerState_t *from = demo.prevPlayers[i].active ? &demo.prevPlayers[i].ps : NULL; @@ -897,14 +914,15 @@ void SVD_Play_f( void ) { Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n", demo.playMapName, demo.playMaxClients, demo.playFps ); + // Signal demo mode BEFORE map load so the game module knows + // during ClientConnect/ClientBegin to force spectator team. + Cvar_Set( "sv_demoplaying", "1" ); + // Load the map with maxclients = MAX_CLIENTS to avoid entity slot collisions. Cbuf_ExecuteText( EXEC_NOW, va("set sv_maxclients %d\n", MAX_CLIENTS) ); Cbuf_ExecuteText( EXEC_NOW, va("set sv_fps %d\n", demo.playFps) ); Cbuf_ExecuteText( EXEC_NOW, va("devmap %s\n", demo.playMapName) ); - // Signal demo mode to the game module AFTER map load - Cvar_Set( "sv_demoplaying", "1" ); - // Reserve recorded player slots so the connecting spectator // doesn't land in slot 0 (which collides with recorded player 0). // Mark as CS_ZOMBIE (non-free, won't be reused by SV_DirectConnect).