Client-owned camera for demo spectator
cgame runs its own PmoveSingle for the free camera using real time, independent of server time. Solves pause freezing — camera moves smoothly while svs.time is frozen. Changes: - usercmd_t: add optional origin[3] with 1-bit flag (zero cost for normal gameplay, 13 bytes during demo viewing) - msg.c: serialize optional origin in usercmd delta encoding - CG_SETCLIENTORIGIN trap: cgame sends camera origin to engine - cl_input.c: CL_FinishMove writes cgame origin into usercmd - cg_predict.c: CG_PredictPlayerState runs local PmoveSingle for free camera in svDemo mode, sends origin via trap for PVS - cg_snapshot.c: detect follow→free camera transition, init camera from last known position - cg_main.c: detect svDemo mode from CS_SVDEMO configstring, lazy-init camera from first snapshot's spectator spawn - g_main.c: G_InitGame sets CS_SVDEMO configstring, G_RunFrame copies cmd.origin to ps.origin for PVS instead of ClientThink_real - bg_public.h: CS_SVDEMO configstring index (26) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7141d941a3
commit
6e13c747ba
13 changed files with 149 additions and 14 deletions
|
|
@ -457,6 +457,10 @@ typedef struct {
|
||||||
int clientNum;
|
int clientNum;
|
||||||
|
|
||||||
qboolean demoPlayback;
|
qboolean demoPlayback;
|
||||||
|
qboolean svDemoPlayback; // server-side demo playback mode
|
||||||
|
qboolean svDemoFreeCamera; // free camera (client-owned movement)
|
||||||
|
playerState_t svDemoCameraPs; // local playerState for free camera
|
||||||
|
int svDemoCameraTime; // real time of last camera update
|
||||||
qboolean levelShot; // taking a level menu screenshot
|
qboolean levelShot; // taking a level menu screenshot
|
||||||
int deferredPlayerLoading;
|
int deferredPlayerLoading;
|
||||||
qboolean loading; // don't defer players at initial startup
|
qboolean loading; // don't defer players at initial startup
|
||||||
|
|
@ -1618,6 +1622,7 @@ qboolean trap_GetUserCmd( int cmdNumber, usercmd_t *ucmd );
|
||||||
|
|
||||||
// used for the weapon select and zoom
|
// used for the weapon select and zoom
|
||||||
void trap_SetUserCmdValue( int stateValue, float sensitivityScale );
|
void trap_SetUserCmdValue( int stateValue, float sensitivityScale );
|
||||||
|
void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z );
|
||||||
|
|
||||||
// aids for VM testing
|
// aids for VM testing
|
||||||
void testPrintInt( char *string, int i );
|
void testPrintInt( char *string, int i );
|
||||||
|
|
|
||||||
|
|
@ -1896,6 +1896,13 @@ void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ) {
|
||||||
// get the gamestate from the client system
|
// get the gamestate from the client system
|
||||||
trap_GetGameState( &cgs.gameState );
|
trap_GetGameState( &cgs.gameState );
|
||||||
|
|
||||||
|
// detect server-side demo playback from configstring
|
||||||
|
cg.svDemoPlayback = ( atoi( CG_ConfigString( CS_SVDEMO ) ) != 0 );
|
||||||
|
if ( cg.svDemoPlayback ) {
|
||||||
|
cg.svDemoFreeCamera = qtrue;
|
||||||
|
// camera ps will be initialized from first snapshot in CG_PredictPlayerState
|
||||||
|
}
|
||||||
|
|
||||||
// check version
|
// check version
|
||||||
s = CG_ConfigString( CS_GAME_VERSION );
|
s = CG_ConfigString( CS_GAME_VERSION );
|
||||||
if ( strcmp( s, GAME_VERSION ) ) {
|
if ( strcmp( s, GAME_VERSION ) ) {
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,59 @@ void CG_PredictPlayerState( void ) {
|
||||||
// demo playback just copies the moves
|
// demo playback just copies the moves
|
||||||
if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) ) {
|
if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) ) {
|
||||||
CG_InterpolatePlayerState( qfalse );
|
CG_InterpolatePlayerState( qfalse );
|
||||||
|
// send followed player's origin for PVS during server demo follow mode
|
||||||
|
if ( cg.svDemoPlayback && (cg.snap->ps.pm_flags & PMF_FOLLOW) ) {
|
||||||
|
trap_SetClientOrigin( qtrue,
|
||||||
|
cg.snap->ps.origin[0],
|
||||||
|
cg.snap->ps.origin[1],
|
||||||
|
cg.snap->ps.origin[2] );
|
||||||
|
cg.svDemoFreeCamera = qfalse;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// server-side demo free camera: run local PmoveSingle
|
||||||
|
if ( cg.svDemoPlayback && cg.svDemoFreeCamera ) {
|
||||||
|
int realTime = trap_Milliseconds();
|
||||||
|
int dt = realTime - cg.svDemoCameraTime;
|
||||||
|
pmove_t pm;
|
||||||
|
|
||||||
|
// lazy init: place camera at spectator spawn from first snapshot
|
||||||
|
if ( cg.svDemoCameraPs.speed == 0 ) {
|
||||||
|
cg.svDemoCameraPs = cg.snap->ps;
|
||||||
|
cg.svDemoCameraPs.pm_type = PM_SPECTATOR;
|
||||||
|
cg.svDemoCameraPs.speed = 480;
|
||||||
|
cg.svDemoCameraTime = realTime;
|
||||||
|
dt = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( dt < 1 ) dt = 1;
|
||||||
|
if ( dt > 200 ) dt = 200;
|
||||||
|
cg.svDemoCameraTime = realTime;
|
||||||
|
|
||||||
|
// get latest usercmd for input
|
||||||
|
current = trap_GetCurrentCmdNumber();
|
||||||
|
trap_GetUserCmd( current, &latestCmd );
|
||||||
|
|
||||||
|
// set up pmove
|
||||||
|
memset( &pm, 0, sizeof(pm) );
|
||||||
|
pm.ps = &cg.svDemoCameraPs;
|
||||||
|
pm.cmd = latestCmd;
|
||||||
|
pm.cmd.serverTime = cg.svDemoCameraPs.commandTime + dt;
|
||||||
|
pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY;
|
||||||
|
pm.trace = CG_Trace;
|
||||||
|
pm.pointcontents = CG_PointContents;
|
||||||
|
|
||||||
|
Pmove( &pm );
|
||||||
|
|
||||||
|
// use as the predicted state for rendering
|
||||||
|
cg.predictedPlayerState = cg.svDemoCameraPs;
|
||||||
|
|
||||||
|
// send origin for PVS
|
||||||
|
trap_SetClientOrigin( qtrue,
|
||||||
|
cg.svDemoCameraPs.origin[0],
|
||||||
|
cg.svDemoCameraPs.origin[1],
|
||||||
|
cg.svDemoCameraPs.origin[2] );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ typedef enum {
|
||||||
CG_R_INPVS,
|
CG_R_INPVS,
|
||||||
// 1.32
|
// 1.32
|
||||||
CG_FS_SEEK,
|
CG_FS_SEEK,
|
||||||
|
CG_SETCLIENTORIGIN, // set client-owned origin for demo spectator PVS
|
||||||
|
|
||||||
/*
|
/*
|
||||||
CG_LOADCAMERA,
|
CG_LOADCAMERA,
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,22 @@ static void CG_TransitionSnapshot( void ) {
|
||||||
|| cg_nopredict.integer || cg_synchronousClients.integer ) {
|
|| cg_nopredict.integer || cg_synchronousClients.integer ) {
|
||||||
CG_TransitionPlayerState( ps, ops );
|
CG_TransitionPlayerState( ps, ops );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// server demo: detect follow→free camera transition
|
||||||
|
if ( cg.svDemoPlayback ) {
|
||||||
|
qboolean wasFollowing = ( ops->pm_flags & PMF_FOLLOW ) != 0;
|
||||||
|
qboolean isFollowing = ( ps->pm_flags & PMF_FOLLOW ) != 0;
|
||||||
|
if ( wasFollowing && !isFollowing ) {
|
||||||
|
// exiting follow mode — init camera from last known position
|
||||||
|
cg.svDemoFreeCamera = qtrue;
|
||||||
|
cg.svDemoCameraTime = trap_Milliseconds();
|
||||||
|
VectorCopy( ops->origin, cg.svDemoCameraPs.origin );
|
||||||
|
VectorCopy( ops->viewangles, cg.svDemoCameraPs.viewangles );
|
||||||
|
cg.svDemoCameraPs.pm_type = PM_SPECTATOR;
|
||||||
|
cg.svDemoCameraPs.speed = 480;
|
||||||
|
cg.svDemoCameraPs.clientNum = cg.clientNum;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,10 @@ void trap_SetUserCmdValue( int stateValue, float sensitivityScale ) {
|
||||||
syscall( CG_SETUSERCMDVALUE, stateValue, PASSFLOAT(sensitivityScale) );
|
syscall( CG_SETUSERCMDVALUE, stateValue, PASSFLOAT(sensitivityScale) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z ) {
|
||||||
|
syscall( CG_SETCLIENTORIGIN, hasOrigin, PASSFLOAT(x), PASSFLOAT(y), PASSFLOAT(z) );
|
||||||
|
}
|
||||||
|
|
||||||
void testPrintInt( char *string, int i ) {
|
void testPrintInt( char *string, int i ) {
|
||||||
syscall( CG_TESTPRINTINT, string, i );
|
syscall( CG_TESTPRINTINT, string, i );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -457,6 +457,14 @@ int CL_CgameSystemCalls( int *args ) {
|
||||||
return 0;
|
return 0;
|
||||||
case CG_FS_SEEK:
|
case CG_FS_SEEK:
|
||||||
return FS_Seek( args[1], args[2], args[3] );
|
return FS_Seek( args[1], args[2], args[3] );
|
||||||
|
case CG_SETCLIENTORIGIN:
|
||||||
|
cl.cgameHasOrigin = args[1];
|
||||||
|
if ( args[1] ) {
|
||||||
|
cl.cgameOrigin[0] = VMF(2);
|
||||||
|
cl.cgameOrigin[1] = VMF(3);
|
||||||
|
cl.cgameOrigin[2] = VMF(4);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
case CG_SENDCONSOLECOMMAND:
|
case CG_SENDCONSOLECOMMAND:
|
||||||
Cbuf_AddText( VMA(1) );
|
Cbuf_AddText( VMA(1) );
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,12 @@ void CL_FinishMove( usercmd_t *cmd ) {
|
||||||
for (i=0 ; i<3 ; i++) {
|
for (i=0 ; i<3 ; i++) {
|
||||||
cmd->angles[i] = ANGLE2SHORT(cl.viewangles[i]);
|
cmd->angles[i] = ANGLE2SHORT(cl.viewangles[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// client-owned origin for demo spectator PVS
|
||||||
|
cmd->hasOrigin = cl.cgameHasOrigin;
|
||||||
|
if ( cl.cgameHasOrigin ) {
|
||||||
|
VectorCopy( cl.cgameOrigin, cmd->origin );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,8 @@ typedef struct {
|
||||||
// cgame communicates a few values to the client system
|
// cgame communicates a few values to the client system
|
||||||
int cgameUserCmdValue; // current weapon to add to usercmd_t
|
int cgameUserCmdValue; // current weapon to add to usercmd_t
|
||||||
float cgameSensitivity;
|
float cgameSensitivity;
|
||||||
|
qboolean cgameHasOrigin; // client-owned origin for demo spectator PVS
|
||||||
|
vec3_t cgameOrigin; // the origin to send
|
||||||
|
|
||||||
// cmds[cmdNumber] is the predicted command, [cmdNumber-1] is the last
|
// cmds[cmdNumber] is the predicted command, [cmdNumber-1] is the last
|
||||||
// properly generated command
|
// properly generated command
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#define CS_FLAGSTATUS 23 // string indicating flag status in CTF
|
#define CS_FLAGSTATUS 23 // string indicating flag status in CTF
|
||||||
#define CS_SHADERSTATE 24
|
#define CS_SHADERSTATE 24
|
||||||
#define CS_BOTINFO 25
|
#define CS_BOTINFO 25
|
||||||
|
#define CS_SVDEMO 26 // "1" during server-side demo playback
|
||||||
|
|
||||||
#define CS_ITEMS 27 // string of 0's and 1's that tell which items are present
|
#define CS_ITEMS 27 // string of 0's and 1's that tell which items are present
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,11 @@ void G_InitGame( int levelTime, int randomSeed, int restart ) {
|
||||||
|
|
||||||
G_RegisterCvars();
|
G_RegisterCvars();
|
||||||
|
|
||||||
|
// signal server-side demo mode to cgame via configstring
|
||||||
|
if ( trap_Cvar_VariableIntegerValue( "sv_demoplaying" ) ) {
|
||||||
|
trap_SetConfigstring( CS_SVDEMO, "1" );
|
||||||
|
}
|
||||||
|
|
||||||
G_ProcessIPBans();
|
G_ProcessIPBans();
|
||||||
|
|
||||||
G_InitMemory();
|
G_InitMemory();
|
||||||
|
|
@ -1746,10 +1751,16 @@ int start, end;
|
||||||
gclient_t *cl = &level.clients[i];
|
gclient_t *cl = &level.clients[i];
|
||||||
gentity_t *e = &g_entities[i];
|
gentity_t *e = &g_entities[i];
|
||||||
|
|
||||||
// find and process the spectator
|
// find the spectator — use client-owned origin for PVS
|
||||||
if ( e->client && cl->pers.connected == CON_CONNECTED
|
if ( e->client && cl->pers.connected == CON_CONNECTED
|
||||||
&& cl->sess.sessionTeam == TEAM_SPECTATOR ) {
|
&& cl->sess.sessionTeam == TEAM_SPECTATOR ) {
|
||||||
ClientThink_real( e );
|
// copy client-owned origin from usercmd for PVS culling.
|
||||||
|
// cgame runs its own PmoveSingle for camera movement.
|
||||||
|
if ( cl->pers.cmd.hasOrigin ) {
|
||||||
|
VectorCopy( cl->pers.cmd.origin, cl->ps.origin );
|
||||||
|
VectorCopy( cl->pers.cmd.origin, e->s.pos.trBase );
|
||||||
|
VectorCopy( cl->pers.cmd.origin, e->r.currentOrigin );
|
||||||
|
}
|
||||||
specEnt = e;
|
specEnt = e;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1253,8 +1253,11 @@ typedef struct usercmd_s {
|
||||||
int serverTime;
|
int serverTime;
|
||||||
int angles[3];
|
int angles[3];
|
||||||
int buttons;
|
int buttons;
|
||||||
byte weapon; // weapon
|
byte weapon; // weapon
|
||||||
signed char forwardmove, rightmove, upmove;
|
signed char forwardmove, rightmove, upmove;
|
||||||
|
// client-owned origin for demo spectator PVS (optional)
|
||||||
|
qboolean hasOrigin;
|
||||||
|
float origin[3];
|
||||||
} usercmd_t;
|
} usercmd_t;
|
||||||
|
|
||||||
//===================================================================
|
//===================================================================
|
||||||
|
|
|
||||||
|
|
@ -702,18 +702,27 @@ void MSG_WriteDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t *
|
||||||
from->weapon == to->weapon) {
|
from->weapon == to->weapon) {
|
||||||
MSG_WriteBits( msg, 0, 1 ); // no change
|
MSG_WriteBits( msg, 0, 1 ); // no change
|
||||||
oldsize += 7;
|
oldsize += 7;
|
||||||
return;
|
} else {
|
||||||
|
key ^= to->serverTime;
|
||||||
|
MSG_WriteBits( msg, 1, 1 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->angles[0], to->angles[0], 16 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->angles[1], to->angles[1], 16 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->angles[2], to->angles[2], 16 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->forwardmove, to->forwardmove, 8 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->rightmove, to->rightmove, 8 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->upmove, to->upmove, 8 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->buttons, to->buttons, 16 );
|
||||||
|
MSG_WriteDeltaKey( msg, key, from->weapon, to->weapon, 8 );
|
||||||
|
}
|
||||||
|
// optional client-owned origin (always written, independent of field changes)
|
||||||
|
if ( to->hasOrigin ) {
|
||||||
|
MSG_WriteBits( msg, 1, 1 );
|
||||||
|
MSG_WriteFloat( msg, to->origin[0] );
|
||||||
|
MSG_WriteFloat( msg, to->origin[1] );
|
||||||
|
MSG_WriteFloat( msg, to->origin[2] );
|
||||||
|
} else {
|
||||||
|
MSG_WriteBits( msg, 0, 1 );
|
||||||
}
|
}
|
||||||
key ^= to->serverTime;
|
|
||||||
MSG_WriteBits( msg, 1, 1 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->angles[0], to->angles[0], 16 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->angles[1], to->angles[1], 16 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->angles[2], to->angles[2], 16 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->forwardmove, to->forwardmove, 8 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->rightmove, to->rightmove, 8 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->upmove, to->upmove, 8 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->buttons, to->buttons, 16 );
|
|
||||||
MSG_WriteDeltaKey( msg, key, from->weapon, to->weapon, 8 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -748,6 +757,15 @@ void MSG_ReadDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t *t
|
||||||
to->buttons = from->buttons;
|
to->buttons = from->buttons;
|
||||||
to->weapon = from->weapon;
|
to->weapon = from->weapon;
|
||||||
}
|
}
|
||||||
|
// optional client-owned origin
|
||||||
|
if ( MSG_ReadBits( msg, 1 ) ) {
|
||||||
|
to->hasOrigin = qtrue;
|
||||||
|
to->origin[0] = MSG_ReadFloat( msg );
|
||||||
|
to->origin[1] = MSG_ReadFloat( msg );
|
||||||
|
to->origin[2] = MSG_ReadFloat( msg );
|
||||||
|
} else {
|
||||||
|
to->hasOrigin = qfalse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue