Fix seeking: snapshot timing, backwards time, entity cleanup

Server:
- Reset nextSnapshotTime for active clients after seek so
  SV_SendClientMessages doesn't skip sending (was comparing
  future nextSnapshotTime against past svs.time)
- First frame always a keyframe (beginning seekable)
- Keyframes during normal playback only reset delta state,
  no SNAPFLAG_RESET_ENTITIES (no visual glitch every 5 sec)

Engine client:
- Reset cl.oldFrameServerTime on SERVERCOUNT toggle to prevent
  "time went backwards" error in CL_SetCGameTime

cgame:
- Handle backwards time in CG_ProcessSnapshots for demo seek:
  accept the time jump, reset cg.time, clear all entity state
  (currentValid, interpolate, time-dependent fields), clear
  local entities (particles, gibs, smoke), wait for next snapshot
- Prevents CG_InterpolateEntityPosition NULL nextSnap error

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
serge_shubin 2026-03-24 19:31:47 +08:00
parent 628007ec57
commit a8bfa738b0
3 changed files with 46 additions and 6 deletions

View file

@ -402,8 +402,34 @@ void CG_ProcessSnapshots( void ) {
CG_SetNextSnap( snap ); CG_SetNextSnap( snap );
// if time went backwards, we have a level restart // if time went backwards, we have a level restart or demo seek
if ( cg.nextSnap->serverTime < cg.snap->serverTime ) { if ( cg.nextSnap->serverTime < cg.snap->serverTime ) {
if ( cg.svDemoPlayback ) {
// demo seek — discard old snap, use nextSnap as current,
// and wait for another snapshot before rendering
cg.snap = cg.nextSnap;
cg.nextSnap = NULL;
cg.time = cg.snap->serverTime;
// reset all entity state and time-dependent fields
{
int e;
for ( e = 0; e < MAX_GENTITIES; e++ ) {
cg_entities[e].currentValid = qfalse;
cg_entities[e].interpolate = qfalse;
cg_entities[e].muzzleFlashTime = 0;
cg_entities[e].trailTime = 0;
cg_entities[e].dustTrailTime = 0;
cg_entities[e].miscTime = 0;
cg_entities[e].snapShotTime = 0;
cg_entities[e].previousEvent = 0;
cg_entities[e].teleportFlag = 0;
}
}
// clear local entities (particles, gibs, etc.)
// they reference old times and would render incorrectly
CG_InitLocalEntities();
break; // exit loop, wait for next snapshot
}
CG_Error( "CG_ProcessSnapshots: Server time went backwards" ); CG_Error( "CG_ProcessSnapshots: Server time went backwards" );
} }
} }

View file

@ -308,6 +308,7 @@ void CL_ParseSnapshot( msg_t *msg ) {
cl.serverTimeDelta = cl.snap.serverTime - cls.realtime; cl.serverTimeDelta = cl.snap.serverTime - cls.realtime;
cl.oldServerTime = cl.snap.serverTime; cl.oldServerTime = cl.snap.serverTime;
cl.serverTime = cl.snap.serverTime; cl.serverTime = cl.snap.serverTime;
cl.oldFrameServerTime = cl.snap.serverTime; // prevent backwards time error on seek
} }
} }

View file

@ -396,7 +396,8 @@ static qboolean SVD_StartRecording( const char *demoname ) {
} else { } else {
demo.keyframeInterval = 0; demo.keyframeInterval = 0;
} }
demo.framesSinceKeyframe = 0; // first frame is always a keyframe (makes beginning seekable)
demo.framesSinceKeyframe = demo.keyframeInterval;
demo.numKeyframes = 0; demo.numKeyframes = 0;
} }
@ -1083,13 +1084,24 @@ void SVD_Seek_f( void ) {
// toggle SERVERCOUNT to reset client time delta // toggle SERVERCOUNT to reset client time delta
svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;
// mark that we seeked — next SVD_ReadFrame sets SNAPFLAG_RESET_ENTITIES
demo.seeked = qtrue; demo.seeked = qtrue;
// on the next SV_Frame. SVD_ReadFrame will detect the keyframe flag
// and set SNAPFLAG_RESET_ENTITIES + reset delta state.
demo.endOfDemo = qfalse; demo.endOfDemo = qfalse;
Com_Printf( "Seeked to keyframe at time %d.\n", demo.keyframeTimes[bestKf] ); // reset client snapshot timing so SV_SendClientMessages doesn't
// skip sending (nextSnapshotTime was in the future, now svs.time is past)
{
int j;
for ( j = 0; j < sv_maxclients->integer; j++ ) {
if ( svs.clients[j].state >= CS_ACTIVE ) {
svs.clients[j].nextSnapshotTime = svs.time;
}
}
}
// ensure one frame runs on next SV_Frame
sv.timeResidual = 1000 / sv_fps->integer;
Com_Printf( "Seeked to time %d.\n", svs.time );
} }
/* /*
@ -1101,6 +1113,7 @@ qboolean SVD_PlaybackFrame( void ) {
return qfalse; return qfalse;
} }
// manual pause — don't consume demo data // manual pause — don't consume demo data
if ( demo.paused ) { if ( demo.paused ) {
return qfalse; return qfalse;