SNAPFLAG_RESET_ENTITIES: no-interpolation reset without gamestate resend

Replace the gamestate resend approach for map_restart with a custom
snapshot flag (SNAPFLAG_RESET_ENTITIES, bit 4 in snapFlags byte).

Server side (sv_netdemo.c):
- On restart frame, OR the flag into svs.snapFlagServerBit (one-shot)
- Cleared at the start of the next playback frame

Client side (cg_snapshot.c):
- CG_SetNextSnap: clear currentValid for all entities when flag is set,
  making them all "new" — existing interpolation check at line 228
  sets interpolate=qfalse, CG_TransitionEntity calls CG_ResetEntity
- Also set cg.nextFrameTeleport=qtrue to prevent playerstate
  interpolation during follow mode

No loading screen, no lost frames, no gamestate resend. Entities and
playerstate both snap to correct positions on map_restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
serge_shubin 2026-03-24 00:19:36 +08:00
parent 0b4eb7b69f
commit c6af63ae42
4 changed files with 22 additions and 13 deletions

View file

@ -203,6 +203,15 @@ static void CG_SetNextSnap( snapshot_t *snap ) {
cg.nextSnap = snap; cg.nextSnap = snap;
// SNAPFLAG_RESET_ENTITIES: invalidate all entities so they are
// treated as new (no interpolation from old positions).
// Must happen before the entity loop below.
if ( snap->snapFlags & SNAPFLAG_RESET_ENTITIES ) {
for ( num = 0 ; num < MAX_GENTITIES ; num++ ) {
cg_entities[ num ].currentValid = qfalse;
}
}
BG_PlayerStateToEntityState( &snap->ps, &cg_entities[ snap->ps.clientNum ].nextState, qfalse ); BG_PlayerStateToEntityState( &snap->ps, &cg_entities[ snap->ps.clientNum ].nextState, qfalse );
cg_entities[ cg.snap->ps.clientNum ].interpolate = qtrue; cg_entities[ cg.snap->ps.clientNum ].interpolate = qtrue;
@ -241,6 +250,11 @@ static void CG_SetNextSnap( snapshot_t *snap ) {
cg.nextFrameTeleport = qtrue; cg.nextFrameTeleport = qtrue;
} }
// entity reset also prevents playerstate interpolation
if ( cg.nextSnap->snapFlags & SNAPFLAG_RESET_ENTITIES ) {
cg.nextFrameTeleport = qtrue;
}
// sort out solid entities // sort out solid entities
CG_BuildSolidList(); CG_BuildSolidList();
} }

View file

@ -1085,6 +1085,7 @@ typedef enum {
#define SNAPFLAG_RATE_DELAYED 1 #define SNAPFLAG_RATE_DELAYED 1
#define SNAPFLAG_NOT_ACTIVE 2 // snapshot used during connection and for zombies #define SNAPFLAG_NOT_ACTIVE 2 // snapshot used during connection and for zombies
#define SNAPFLAG_SERVERCOUNT 4 // toggled every map_restart so transitions can be detected #define SNAPFLAG_SERVERCOUNT 4 // toggled every map_restart so transitions can be detected
#define SNAPFLAG_RESET_ENTITIES 16 // snap all entities to current position, no interpolation
// //
// per-level limits // per-level limits

View file

@ -289,7 +289,6 @@ void SV_AuthorizeIpPacket( netadr_t from );
void SV_ExecuteClientMessage( client_t *cl, msg_t *msg ); void SV_ExecuteClientMessage( client_t *cl, msg_t *msg );
void SV_UserinfoChanged( client_t *cl ); void SV_UserinfoChanged( client_t *cl );
void SV_SendClientGameState( client_t *client );
void SV_ClientEnterWorld( client_t *client, usercmd_t *cmd ); void SV_ClientEnterWorld( client_t *client, usercmd_t *cmd );
void SV_DropClient( client_t *drop, const char *reason ); void SV_DropClient( client_t *drop, const char *reason );

View file

@ -677,22 +677,14 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
byte frameFlags; byte frameFlags;
FS_Read( &frameFlags, 1, f ); FS_Read( &frameFlags, 1, f );
if ( frameFlags & 1 ) { if ( frameFlags & 1 ) {
int j;
// map was restarted — reset playback delta state to match // map was restarted — reset playback delta state to match
// the recording's reset (both now decode from zero baseline). // the recording's reset (both now decode from zero baseline).
// Also toggle server bit and force non-delta snapshot so the
// client clears old entity state and doesn't interpolate.
Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) );
Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) );
svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; // set one-shot SNAPFLAG_RESET_ENTITIES so cgame snaps all
// send a full gamestate to all active clients — this makes // entities to current position without interpolation.
// CL_ParseGamestate → CL_ClearState wipe all snapshot/entity // OR'd into snapFlagServerBit, cleared after one frame.
// history, exactly like a real map_restart does. svs.snapFlagServerBit |= SNAPFLAG_RESET_ENTITIES;
for ( j = 0; j < sv_maxclients->integer; j++ ) {
if ( svs.clients[j].state >= CS_PRIMED ) {
SV_SendClientGameState( &svs.clients[j] );
}
}
} }
} }
@ -1020,6 +1012,9 @@ qboolean SVD_PlaybackFrame( void ) {
demo.needConfigstrings = qfalse; demo.needConfigstrings = qfalse;
} }
// clear one-shot reset flag from previous frame before reading new one
svs.snapFlagServerBit &= ~SNAPFLAG_RESET_ENTITIES;
if ( !SVD_ReadFrame( demo.playFile ) ) { if ( !SVD_ReadFrame( demo.playFile ) ) {
Com_Printf( "Server demo playback finished.\n" ); Com_Printf( "Server demo playback finished.\n" );
SVD_CleanupPlayback(); SVD_CleanupPlayback();