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:
parent
de9863da57
commit
1f8c8aea1c
2 changed files with 128 additions and 7 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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++ ) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue