diff --git a/code/game/g_main.c b/code/game/g_main.c index b6a93e5..a837e2c 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -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; diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index 10b0532..20ae3dc 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -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++ ) {