diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 4dd5c9d..0c8e98a 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -457,6 +457,10 @@ typedef struct { int clientNum; 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 int deferredPlayerLoading; 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 void trap_SetUserCmdValue( int stateValue, float sensitivityScale ); +void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z ); // aids for VM testing void testPrintInt( char *string, int i ); diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index aa9ec27..97f65a2 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1896,6 +1896,13 @@ void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ) { // get the gamestate from the client system 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 s = CG_ConfigString( CS_GAME_VERSION ); if ( strcmp( s, GAME_VERSION ) ) { diff --git a/code/cgame/cg_predict.c b/code/cgame/cg_predict.c index fec2cfb..ed0d72d 100644 --- a/code/cgame/cg_predict.c +++ b/code/cgame/cg_predict.c @@ -432,6 +432,59 @@ void CG_PredictPlayerState( void ) { // demo playback just copies the moves if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) ) { 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; } diff --git a/code/cgame/cg_public.h b/code/cgame/cg_public.h index 8514143..d667ce2 100644 --- a/code/cgame/cg_public.h +++ b/code/cgame/cg_public.h @@ -164,6 +164,7 @@ typedef enum { CG_R_INPVS, // 1.32 CG_FS_SEEK, + CG_SETCLIENTORIGIN, // set client-owned origin for demo spectator PVS /* CG_LOADCAMERA, diff --git a/code/cgame/cg_snapshot.c b/code/cgame/cg_snapshot.c index 6bc3a0c..2f3dc10 100644 --- a/code/cgame/cg_snapshot.c +++ b/code/cgame/cg_snapshot.c @@ -184,6 +184,22 @@ static void CG_TransitionSnapshot( void ) { || cg_nopredict.integer || cg_synchronousClients.integer ) { 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; + } + } } } diff --git a/code/cgame/cg_syscalls.c b/code/cgame/cg_syscalls.c index a7bf659..2446b4b 100644 --- a/code/cgame/cg_syscalls.c +++ b/code/cgame/cg_syscalls.c @@ -333,6 +333,10 @@ void trap_SetUserCmdValue( int stateValue, float 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 ) { syscall( CG_TESTPRINTINT, string, i ); } diff --git a/code/client/cl_cgame.c b/code/client/cl_cgame.c index 0b259d3..e399cef 100644 --- a/code/client/cl_cgame.c +++ b/code/client/cl_cgame.c @@ -457,6 +457,14 @@ int CL_CgameSystemCalls( int *args ) { return 0; case CG_FS_SEEK: 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: Cbuf_AddText( VMA(1) ); return 0; diff --git a/code/client/cl_input.c b/code/client/cl_input.c index f450ee7..c3b4778 100644 --- a/code/client/cl_input.c +++ b/code/client/cl_input.c @@ -516,6 +516,12 @@ void CL_FinishMove( usercmd_t *cmd ) { for (i=0 ; i<3 ; 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 ); + } } diff --git a/code/client/client.h b/code/client/client.h index 09e02a8..fe36445 100644 --- a/code/client/client.h +++ b/code/client/client.h @@ -106,6 +106,8 @@ typedef struct { // cgame communicates a few values to the client system int cgameUserCmdValue; // current weapon to add to usercmd_t 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 // properly generated command diff --git a/code/game/bg_public.h b/code/game/bg_public.h index 0dd59db..5dc1cf2 100644 --- a/code/game/bg_public.h +++ b/code/game/bg_public.h @@ -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_SHADERSTATE 24 #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 diff --git a/code/game/g_main.c b/code/game/g_main.c index a468854..77b817a 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -418,6 +418,11 @@ void G_InitGame( int levelTime, int randomSeed, int restart ) { 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_InitMemory(); @@ -1746,10 +1751,16 @@ int start, end; gclient_t *cl = &level.clients[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 && 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; continue; } diff --git a/code/game/q_shared.h b/code/game/q_shared.h index 4834c94..32a772f 100644 --- a/code/game/q_shared.h +++ b/code/game/q_shared.h @@ -1253,8 +1253,11 @@ typedef struct usercmd_s { int serverTime; int angles[3]; int buttons; - byte weapon; // weapon + byte weapon; // weapon signed char forwardmove, rightmove, upmove; + // client-owned origin for demo spectator PVS (optional) + qboolean hasOrigin; + float origin[3]; } usercmd_t; //=================================================================== diff --git a/code/qcommon/msg.c b/code/qcommon/msg.c index 97110f8..e4f6696 100644 --- a/code/qcommon/msg.c +++ b/code/qcommon/msg.c @@ -702,18 +702,27 @@ void MSG_WriteDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t * from->weapon == to->weapon) { MSG_WriteBits( msg, 0, 1 ); // no change 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->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; + } } /*