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>
This commit is contained in:
serge_shubin 2026-03-23 04:46:40 +08:00
parent de9863da57
commit 1f8c8aea1c
2 changed files with 128 additions and 7 deletions

View file

@ -766,6 +766,10 @@ void CalculateRanks( void ) {
int rank;
int score;
int newScore;
// (demo playback note: this runs normally so the spectator
// appears in sortedClients. Recorded players won't show here
// since they're not connected in the game module.)
gclient_t *cl;
level.follow1 = -1;
@ -1731,14 +1735,35 @@ int start, end;
// get any cvar changes
G_UpdateCvars();
// demo playback: only process spectator client, skip all entity logic
// demo playback: sync recorded player states and process spectator
if ( g_svDemoPlaying.integer ) {
// mark recorded players as connected based on their playerState.
// the server injected playerStates via SV_GameClientNum before
// calling G_RunFrame, so g_clients[i].ps is already populated.
for ( i = 0; i < level.maxclients; i++ ) {
ent = &g_entities[i];
if ( ent->inuse && ent->client &&
ent->client->sess.sessionTeam == TEAM_SPECTATOR &&
ent->client->pers.connected == CON_CONNECTED ) {
ClientThink_real( ent );
gclient_t *cl = &level.clients[i];
gentity_t *e = &g_entities[i];
// skip the spectator slot — it's managed normally
if ( e->client && cl->pers.connected == CON_CONNECTED
&& cl->sess.sessionTeam == TEAM_SPECTATOR ) {
ClientThink_real( e );
continue;
}
// check if server injected a valid playerState for this slot
if ( cl->ps.commandTime > 0 ) {
cl->pers.connected = CON_CONNECTED;
cl->sess.sessionTeam = cl->ps.persistant[PERS_TEAM];
e->inuse = qtrue;
e->client = cl;
e->s.clientNum = i;
e->s.number = i;
} else if ( cl->pers.connected == CON_CONNECTED
&& cl->sess.sessionTeam != TEAM_SPECTATOR ) {
// player left — mark disconnected
cl->pers.connected = CON_DISCONNECTED;
e->inuse = qfalse;
}
}
return;

View file

@ -68,12 +68,19 @@ typedef struct {
qboolean active; // was this entity present last frame?
} svdEntityState_t;
// per-player state for delta compression
typedef struct {
playerState_t ps;
qboolean active;
} svdPlayerState_t;
typedef struct {
// recording
fileHandle_t recordFile;
qboolean recording;
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states
// playback
fileHandle_t playFile;
@ -87,6 +94,7 @@ typedef struct {
qboolean endOfDemo;
qboolean needConfigstrings; // apply saved configstrings on first frame
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
} svDemo_t;
static svDemo_t demo;
@ -237,11 +245,48 @@ static void SVD_WriteFrame( fileHandle_t f ) {
// end of entities marker
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
// write compressed message to file
// write compressed entity message to file
msgLen = msg.cursize;
SVD_WriteInt( f, msgLen );
FS_Write( msg.data, msgLen, f );
// write player states (delta compressed)
{
msg_t psmsg;
byte psBuf[MAX_CLIENTS * 256];
int psCount = 0;
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
for ( i = 0; i < sv_maxclients->integer; i++ ) {
playerState_t *ps = SV_GameClientNum( i );
client_t *cl = &svs.clients[i];
qboolean active = ( cl->state >= CS_ACTIVE );
if ( active ) {
playerState_t *from = demo.prevPlayers[i].active ? &demo.prevPlayers[i].ps : NULL;
MSG_WriteBits( &psmsg, i, 6 ); // client number (0-63)
MSG_WriteBits( &psmsg, 1, 1 ); // active flag
MSG_WriteDeltaPlayerstate( &psmsg, from, ps );
demo.prevPlayers[i].ps = *ps;
demo.prevPlayers[i].active = qtrue;
psCount++;
} else if ( demo.prevPlayers[i].active ) {
MSG_WriteBits( &psmsg, i, 6 );
MSG_WriteBits( &psmsg, 0, 1 ); // inactive flag (player left)
demo.prevPlayers[i].active = qfalse;
psCount++;
}
}
// terminator
MSG_WriteBits( &psmsg, MAX_CLIENTS - 1, 6 );
MSG_WriteBits( &psmsg, 0, 1 );
msgLen = psmsg.cursize;
SVD_WriteInt( f, msgLen );
FS_Write( psmsg.data, msgLen, f );
}
// configstring changes
numChanges = 0;
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
@ -540,6 +585,57 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
sv.num_entities = demo.spectatorClientNum + 1;
}
// read player states (delta compressed)
{
msg_t psmsg;
byte psBuf[MAX_CLIENTS * 256];
int psMsgLen;
psMsgLen = SVD_ReadInt( f );
if ( psMsgLen > 0 && psMsgLen <= (int)sizeof(psBuf) ) {
FS_Read( psBuf, psMsgLen, f );
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
psmsg.cursize = psMsgLen;
while ( 1 ) {
int clientNum = MSG_ReadBits( &psmsg, 6 );
int active = MSG_ReadBits( &psmsg, 1 );
if ( clientNum == MAX_CLIENTS - 1 && !active ) {
break; // terminator
}
if ( psmsg.readcount > psmsg.cursize ) {
break;
}
if ( active ) {
playerState_t newPs;
playerState_t *from;
playerState_t baseline;
if ( demo.playPrevPlayers[clientNum].active ) {
from = &demo.playPrevPlayers[clientNum].ps;
} else {
Com_Memset( &baseline, 0, sizeof(baseline) );
from = &baseline;
}
MSG_ReadDeltaPlayerstate( &psmsg, from, &newPs );
demo.playPrevPlayers[clientNum].ps = newPs;
demo.playPrevPlayers[clientNum].active = qtrue;
// inject into game module's client state
if ( clientNum != demo.spectatorClientNum ) {
playerState_t *gamePs = SV_GameClientNum( clientNum );
*gamePs = newPs;
}
} else {
demo.playPrevPlayers[clientNum].active = qfalse;
}
}
}
}
// read configstring changes
numChanges = SVD_ReadShort( f );
for ( i = 0; i < numChanges; i++ ) {