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 rank;
|
||||||
int score;
|
int score;
|
||||||
int newScore;
|
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;
|
gclient_t *cl;
|
||||||
|
|
||||||
level.follow1 = -1;
|
level.follow1 = -1;
|
||||||
|
|
@ -1731,14 +1735,35 @@ int start, end;
|
||||||
// get any cvar changes
|
// get any cvar changes
|
||||||
G_UpdateCvars();
|
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 ) {
|
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++ ) {
|
for ( i = 0; i < level.maxclients; i++ ) {
|
||||||
ent = &g_entities[i];
|
gclient_t *cl = &level.clients[i];
|
||||||
if ( ent->inuse && ent->client &&
|
gentity_t *e = &g_entities[i];
|
||||||
ent->client->sess.sessionTeam == TEAM_SPECTATOR &&
|
|
||||||
ent->client->pers.connected == CON_CONNECTED ) {
|
// skip the spectator slot — it's managed normally
|
||||||
ClientThink_real( ent );
|
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;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,19 @@ typedef struct {
|
||||||
qboolean active; // was this entity present last frame?
|
qboolean active; // was this entity present last frame?
|
||||||
} svdEntityState_t;
|
} svdEntityState_t;
|
||||||
|
|
||||||
|
// per-player state for delta compression
|
||||||
|
typedef struct {
|
||||||
|
playerState_t ps;
|
||||||
|
qboolean active;
|
||||||
|
} svdPlayerState_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
// recording
|
// recording
|
||||||
fileHandle_t recordFile;
|
fileHandle_t recordFile;
|
||||||
qboolean recording;
|
qboolean recording;
|
||||||
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
||||||
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
||||||
|
svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states
|
||||||
|
|
||||||
// playback
|
// playback
|
||||||
fileHandle_t playFile;
|
fileHandle_t playFile;
|
||||||
|
|
@ -87,6 +94,7 @@ typedef struct {
|
||||||
qboolean endOfDemo;
|
qboolean endOfDemo;
|
||||||
qboolean needConfigstrings; // apply saved configstrings on first frame
|
qboolean needConfigstrings; // apply saved configstrings on first frame
|
||||||
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
||||||
|
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
|
||||||
} svDemo_t;
|
} svDemo_t;
|
||||||
|
|
||||||
static svDemo_t demo;
|
static svDemo_t demo;
|
||||||
|
|
@ -237,11 +245,48 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
// end of entities marker
|
// end of entities marker
|
||||||
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
||||||
|
|
||||||
// write compressed message to file
|
// write compressed entity message to file
|
||||||
msgLen = msg.cursize;
|
msgLen = msg.cursize;
|
||||||
SVD_WriteInt( f, msgLen );
|
SVD_WriteInt( f, msgLen );
|
||||||
FS_Write( msg.data, msgLen, f );
|
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
|
// configstring changes
|
||||||
numChanges = 0;
|
numChanges = 0;
|
||||||
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
|
@ -540,6 +585,57 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
sv.num_entities = demo.spectatorClientNum + 1;
|
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
|
// read configstring changes
|
||||||
numChanges = SVD_ReadShort( f );
|
numChanges = SVD_ReadShort( f );
|
||||||
for ( i = 0; i < numChanges; i++ ) {
|
for ( i = 0; i < numChanges; i++ ) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue