diff --git a/code/cgame/cg_snapshot.c b/code/cgame/cg_snapshot.c index bce36bb..f10c9ab 100644 --- a/code/cgame/cg_snapshot.c +++ b/code/cgame/cg_snapshot.c @@ -1,459 +1,459 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ -// -// cg_snapshot.c -- things that happen on snapshot transition, -// not necessarily every single rendered frame - -#include "cg_local.h" - - - -/* -================== -CG_ResetEntity -================== -*/ -static void CG_ResetEntity( centity_t *cent ) { - // if the previous snapshot this entity was updated in is at least - // an event window back in time then we can reset the previous event - if ( cent->snapShotTime < cg.time - EVENT_VALID_MSEC ) { - cent->previousEvent = 0; - } - - cent->trailTime = cg.snap->serverTime; - - VectorCopy (cent->currentState.origin, cent->lerpOrigin); - VectorCopy (cent->currentState.angles, cent->lerpAngles); - if ( cent->currentState.eType == ET_PLAYER ) { - CG_ResetPlayerEntity( cent ); - } -} - -/* -=============== -CG_TransitionEntity - -cent->nextState is moved to cent->currentState and events are fired -=============== -*/ -static void CG_TransitionEntity( centity_t *cent ) { - cent->currentState = cent->nextState; - cent->currentValid = qtrue; - - // reset if the entity wasn't in the last frame or was teleported - if ( !cent->interpolate ) { - CG_ResetEntity( cent ); - } - - // clear the next state. if will be set by the next CG_SetNextSnap - cent->interpolate = qfalse; - - // check for events - CG_CheckEvents( cent ); -} - - -/* -================== -CG_SetInitialSnapshot - -This will only happen on the very first snapshot, or -on tourney restarts. All other times will use -CG_TransitionSnapshot instead. - -FIXME: Also called by map_restart? -================== -*/ -void CG_SetInitialSnapshot( snapshot_t *snap ) { - int i; - centity_t *cent; - entityState_t *state; - - cg.snap = snap; - - BG_PlayerStateToEntityState( &snap->ps, &cg_entities[ snap->ps.clientNum ].currentState, qfalse ); - - // sort out solid entities - CG_BuildSolidList(); - - CG_ExecuteNewServerCommands( snap->serverCommandSequence ); - - // set our local weapon selection pointer to - // what the server has indicated the current weapon is - CG_Respawn(); - - for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { - state = &cg.snap->entities[ i ]; - cent = &cg_entities[ state->number ]; - - memcpy(¢->currentState, state, sizeof(entityState_t)); - //cent->currentState = *state; - cent->interpolate = qfalse; - cent->currentValid = qtrue; - - CG_ResetEntity( cent ); - - // check for events - CG_CheckEvents( cent ); - } -} - - -/* -=================== -CG_TransitionSnapshot - -The transition point from snap to nextSnap has passed -=================== -*/ -static void CG_TransitionSnapshot( void ) { - centity_t *cent; - snapshot_t *oldFrame; - int i; - - if ( !cg.snap ) { - CG_Error( "CG_TransitionSnapshot: NULL cg.snap" ); - } - if ( !cg.nextSnap ) { - CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" ); - } - - // execute any server string commands before transitioning entities - CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence ); - - // if we had a map_restart, set everthing with initial - if ( !cg.snap ) { - } - - // clear the currentValid flag for all entities in the existing snapshot - for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { - cent = &cg_entities[ cg.snap->entities[ i ].number ]; - cent->currentValid = qfalse; - } - - // move nextSnap to snap and do the transitions - oldFrame = cg.snap; - cg.snap = cg.nextSnap; - - BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, qfalse ); - cg_entities[ cg.snap->ps.clientNum ].interpolate = qfalse; - - for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { - cent = &cg_entities[ cg.snap->entities[ i ].number ]; - CG_TransitionEntity( cent ); - - // remember time of snapshot this entity was last updated in - cent->snapShotTime = cg.snap->serverTime; - } - - cg.nextSnap = NULL; - - // check for playerstate transition events - if ( oldFrame ) { - playerState_t *ops, *ps; - - ops = &oldFrame->ps; - ps = &cg.snap->ps; - // teleporting checks are irrespective of prediction - if ( ( ps->eFlags ^ ops->eFlags ) & EF_TELEPORT_BIT ) { - cg.thisFrameTeleport = qtrue; // will be cleared by prediction code - } - - // if we are not doing client side movement prediction for any - // reason, then the client events and view changes will be issued now - if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) - || 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; - } - } - } - -} - - -/* -=================== -CG_SetNextSnap - -A new snapshot has just been read in from the client system. -=================== -*/ -static void CG_SetNextSnap( snapshot_t *snap ) { - int num; - entityState_t *es; - centity_t *cent; - - 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 ); - cg_entities[ cg.snap->ps.clientNum ].interpolate = qtrue; - - // check for extrapolation errors - for ( num = 0 ; num < snap->numEntities ; num++ ) { - es = &snap->entities[num]; - cent = &cg_entities[ es->number ]; - - memcpy(¢->nextState, es, sizeof(entityState_t)); - //cent->nextState = *es; - - // if this frame is a teleport, or the entity wasn't in the - // previous frame, don't interpolate - if ( !cent->currentValid || ( ( cent->currentState.eFlags ^ es->eFlags ) & EF_TELEPORT_BIT ) ) { - cent->interpolate = qfalse; - } else { - cent->interpolate = qtrue; - } - } - - // if the next frame is a teleport for the playerstate, we - // can't interpolate during demos - if ( cg.snap && ( ( snap->ps.eFlags ^ cg.snap->ps.eFlags ) & EF_TELEPORT_BIT ) ) { - cg.nextFrameTeleport = qtrue; - } else { - cg.nextFrameTeleport = qfalse; - } - - // if changing follow mode, don't interpolate - if ( cg.nextSnap->ps.clientNum != cg.snap->ps.clientNum ) { - cg.nextFrameTeleport = qtrue; - } - - // if changing server restarts, don't interpolate - if ( ( cg.nextSnap->snapFlags ^ cg.snap->snapFlags ) & SNAPFLAG_SERVERCOUNT ) { - cg.nextFrameTeleport = qtrue; - } - - // entity reset also prevents playerstate interpolation - if ( cg.nextSnap->snapFlags & SNAPFLAG_RESET_ENTITIES ) { - cg.nextFrameTeleport = qtrue; - } - - // sort out solid entities - CG_BuildSolidList(); -} - - -/* -======================== -CG_ReadNextSnapshot - -This is the only place new snapshots are requested -This may increment cgs.processedSnapshotNum multiple -times if the client system fails to return a -valid snapshot. -======================== -*/ -static snapshot_t *CG_ReadNextSnapshot( void ) { - qboolean r; - snapshot_t *dest; - - if ( cg.latestSnapshotNum > cgs.processedSnapshotNum + 1000 ) { - CG_Printf( "WARNING: CG_ReadNextSnapshot: way out of range, %i > %i", - cg.latestSnapshotNum, cgs.processedSnapshotNum ); - } - - while ( cgs.processedSnapshotNum < cg.latestSnapshotNum ) { - // decide which of the two slots to load it into - if ( cg.snap == &cg.activeSnapshots[0] ) { - dest = &cg.activeSnapshots[1]; - } else { - dest = &cg.activeSnapshots[0]; - } - - // try to read the snapshot from the client system - cgs.processedSnapshotNum++; - r = trap_GetSnapshot( cgs.processedSnapshotNum, dest ); - - // FIXME: why would trap_GetSnapshot return a snapshot with the same server time - if ( cg.snap && r && dest->serverTime == cg.snap->serverTime ) { - //continue; - } - - // if it succeeded, return - if ( r ) { - CG_AddLagometerSnapshotInfo( dest ); - return dest; - } - - // a GetSnapshot will return failure if the snapshot - // never arrived, or is so old that its entities - // have been shoved off the end of the circular - // buffer in the client system. - - // record as a dropped packet - CG_AddLagometerSnapshotInfo( NULL ); - - // If there are additional snapshots, continue trying to - // read them. - } - - // nothing left to read - return NULL; -} - - -/* -============ -CG_ProcessSnapshots - -We are trying to set up a renderable view, so determine -what the simulated time is, and try to get snapshots -both before and after that time if available. - -If we don't have a valid cg.snap after exiting this function, -then a 3D game view cannot be rendered. This should only happen -right after the initial connection. After cg.snap has been valid -once, it will never turn invalid. - -Even if cg.snap is valid, cg.nextSnap may not be, if the snapshot -hasn't arrived yet (it becomes an extrapolating situation instead -of an interpolating one) - -============ -*/ -void CG_ProcessSnapshots( void ) { - snapshot_t *snap; - int n; - - // see what the latest snapshot the client system has is - trap_GetCurrentSnapshotNumber( &n, &cg.latestSnapshotTime ); - if ( n != cg.latestSnapshotNum ) { - if ( n < cg.latestSnapshotNum ) { - // this should never happen - CG_Error( "CG_ProcessSnapshots: n < cg.latestSnapshotNum" ); - } - cg.latestSnapshotNum = n; - } - - // If we have yet to receive a snapshot, check for it. - // Once we have gotten the first snapshot, cg.snap will - // always have valid data for the rest of the game - while ( !cg.snap ) { - snap = CG_ReadNextSnapshot(); - if ( !snap ) { - // we can't continue until we get a snapshot - return; - } - - // set our weapon selection to what - // the playerstate is currently using - if ( !( snap->snapFlags & SNAPFLAG_NOT_ACTIVE ) ) { - CG_SetInitialSnapshot( snap ); - } - } - - // loop until we either have a valid nextSnap with a serverTime - // greater than cg.time to interpolate towards, or we run - // out of available snapshots - do { - // if we don't have a nextframe, try and read a new one in - if ( !cg.nextSnap ) { - snap = CG_ReadNextSnapshot(); - - // if we still don't have a nextframe, we will just have to - // extrapolate - if ( !snap ) { - break; - } - - CG_SetNextSnap( snap ); - - - // if time went backwards, we have a level restart or demo seek - 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" ); - } - } - - // if our time is < nextFrame's, we have a nice interpolating state - if ( cg.time >= cg.snap->serverTime && cg.time < cg.nextSnap->serverTime ) { - break; - } - - // we have passed the transition from nextFrame to frame - CG_TransitionSnapshot(); - } while ( 1 ); - - // assert our valid conditions upon exiting - if ( cg.snap == NULL ) { - CG_Error( "CG_ProcessSnapshots: cg.snap == NULL" ); - } - if ( cg.time < cg.snap->serverTime ) { - // this can happen right after a vid_restart - cg.time = cg.snap->serverTime; - } - if ( cg.nextSnap != NULL && cg.nextSnap->serverTime <= cg.time ) { - CG_Error( "CG_ProcessSnapshots: cg.nextSnap->serverTime <= cg.time" ); - } - -} - +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ +// +// cg_snapshot.c -- things that happen on snapshot transition, +// not necessarily every single rendered frame + +#include "cg_local.h" + + + +/* +================== +CG_ResetEntity +================== +*/ +static void CG_ResetEntity( centity_t *cent ) { + // if the previous snapshot this entity was updated in is at least + // an event window back in time then we can reset the previous event + if ( cent->snapShotTime < cg.time - EVENT_VALID_MSEC ) { + cent->previousEvent = 0; + } + + cent->trailTime = cg.snap->serverTime; + + VectorCopy (cent->currentState.origin, cent->lerpOrigin); + VectorCopy (cent->currentState.angles, cent->lerpAngles); + if ( cent->currentState.eType == ET_PLAYER ) { + CG_ResetPlayerEntity( cent ); + } +} + +/* +=============== +CG_TransitionEntity + +cent->nextState is moved to cent->currentState and events are fired +=============== +*/ +static void CG_TransitionEntity( centity_t *cent ) { + cent->currentState = cent->nextState; + cent->currentValid = qtrue; + + // reset if the entity wasn't in the last frame or was teleported + if ( !cent->interpolate ) { + CG_ResetEntity( cent ); + } + + // clear the next state. if will be set by the next CG_SetNextSnap + cent->interpolate = qfalse; + + // check for events + CG_CheckEvents( cent ); +} + + +/* +================== +CG_SetInitialSnapshot + +This will only happen on the very first snapshot, or +on tourney restarts. All other times will use +CG_TransitionSnapshot instead. + +FIXME: Also called by map_restart? +================== +*/ +void CG_SetInitialSnapshot( snapshot_t *snap ) { + int i; + centity_t *cent; + entityState_t *state; + + cg.snap = snap; + + BG_PlayerStateToEntityState( &snap->ps, &cg_entities[ snap->ps.clientNum ].currentState, qfalse ); + + // sort out solid entities + CG_BuildSolidList(); + + CG_ExecuteNewServerCommands( snap->serverCommandSequence ); + + // set our local weapon selection pointer to + // what the server has indicated the current weapon is + CG_Respawn(); + + for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { + state = &cg.snap->entities[ i ]; + cent = &cg_entities[ state->number ]; + + memcpy(¢->currentState, state, sizeof(entityState_t)); + //cent->currentState = *state; + cent->interpolate = qfalse; + cent->currentValid = qtrue; + + CG_ResetEntity( cent ); + + // check for events + CG_CheckEvents( cent ); + } +} + + +/* +=================== +CG_TransitionSnapshot + +The transition point from snap to nextSnap has passed +=================== +*/ +static void CG_TransitionSnapshot( void ) { + centity_t *cent; + snapshot_t *oldFrame; + int i; + + if ( !cg.snap ) { + CG_Error( "CG_TransitionSnapshot: NULL cg.snap" ); + } + if ( !cg.nextSnap ) { + CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" ); + } + + // execute any server string commands before transitioning entities + CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence ); + + // if we had a map_restart, set everthing with initial + if ( !cg.snap ) { + } + + // clear the currentValid flag for all entities in the existing snapshot + for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { + cent = &cg_entities[ cg.snap->entities[ i ].number ]; + cent->currentValid = qfalse; + } + + // move nextSnap to snap and do the transitions + oldFrame = cg.snap; + cg.snap = cg.nextSnap; + + BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, qfalse ); + cg_entities[ cg.snap->ps.clientNum ].interpolate = qfalse; + + for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { + cent = &cg_entities[ cg.snap->entities[ i ].number ]; + CG_TransitionEntity( cent ); + + // remember time of snapshot this entity was last updated in + cent->snapShotTime = cg.snap->serverTime; + } + + cg.nextSnap = NULL; + + // check for playerstate transition events + if ( oldFrame ) { + playerState_t *ops, *ps; + + ops = &oldFrame->ps; + ps = &cg.snap->ps; + // teleporting checks are irrespective of prediction + if ( ( ps->eFlags ^ ops->eFlags ) & EF_TELEPORT_BIT ) { + cg.thisFrameTeleport = qtrue; // will be cleared by prediction code + } + + // if we are not doing client side movement prediction for any + // reason, then the client events and view changes will be issued now + if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) + || 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; + } + } + } + +} + + +/* +=================== +CG_SetNextSnap + +A new snapshot has just been read in from the client system. +=================== +*/ +static void CG_SetNextSnap( snapshot_t *snap ) { + int num; + entityState_t *es; + centity_t *cent; + + 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 ); + cg_entities[ cg.snap->ps.clientNum ].interpolate = qtrue; + + // check for extrapolation errors + for ( num = 0 ; num < snap->numEntities ; num++ ) { + es = &snap->entities[num]; + cent = &cg_entities[ es->number ]; + + memcpy(¢->nextState, es, sizeof(entityState_t)); + //cent->nextState = *es; + + // if this frame is a teleport, or the entity wasn't in the + // previous frame, don't interpolate + if ( !cent->currentValid || ( ( cent->currentState.eFlags ^ es->eFlags ) & EF_TELEPORT_BIT ) ) { + cent->interpolate = qfalse; + } else { + cent->interpolate = qtrue; + } + } + + // if the next frame is a teleport for the playerstate, we + // can't interpolate during demos + if ( cg.snap && ( ( snap->ps.eFlags ^ cg.snap->ps.eFlags ) & EF_TELEPORT_BIT ) ) { + cg.nextFrameTeleport = qtrue; + } else { + cg.nextFrameTeleport = qfalse; + } + + // if changing follow mode, don't interpolate + if ( cg.nextSnap->ps.clientNum != cg.snap->ps.clientNum ) { + cg.nextFrameTeleport = qtrue; + } + + // if changing server restarts, don't interpolate + if ( ( cg.nextSnap->snapFlags ^ cg.snap->snapFlags ) & SNAPFLAG_SERVERCOUNT ) { + cg.nextFrameTeleport = qtrue; + } + + // entity reset also prevents playerstate interpolation + if ( cg.nextSnap->snapFlags & SNAPFLAG_RESET_ENTITIES ) { + cg.nextFrameTeleport = qtrue; + } + + // sort out solid entities + CG_BuildSolidList(); +} + + +/* +======================== +CG_ReadNextSnapshot + +This is the only place new snapshots are requested +This may increment cgs.processedSnapshotNum multiple +times if the client system fails to return a +valid snapshot. +======================== +*/ +static snapshot_t *CG_ReadNextSnapshot( void ) { + qboolean r; + snapshot_t *dest; + + if ( cg.latestSnapshotNum > cgs.processedSnapshotNum + 1000 ) { + CG_Printf( "WARNING: CG_ReadNextSnapshot: way out of range, %i > %i", + cg.latestSnapshotNum, cgs.processedSnapshotNum ); + } + + while ( cgs.processedSnapshotNum < cg.latestSnapshotNum ) { + // decide which of the two slots to load it into + if ( cg.snap == &cg.activeSnapshots[0] ) { + dest = &cg.activeSnapshots[1]; + } else { + dest = &cg.activeSnapshots[0]; + } + + // try to read the snapshot from the client system + cgs.processedSnapshotNum++; + r = trap_GetSnapshot( cgs.processedSnapshotNum, dest ); + + // FIXME: why would trap_GetSnapshot return a snapshot with the same server time + if ( cg.snap && r && dest->serverTime == cg.snap->serverTime ) { + //continue; + } + + // if it succeeded, return + if ( r ) { + CG_AddLagometerSnapshotInfo( dest ); + return dest; + } + + // a GetSnapshot will return failure if the snapshot + // never arrived, or is so old that its entities + // have been shoved off the end of the circular + // buffer in the client system. + + // record as a dropped packet + CG_AddLagometerSnapshotInfo( NULL ); + + // If there are additional snapshots, continue trying to + // read them. + } + + // nothing left to read + return NULL; +} + + +/* +============ +CG_ProcessSnapshots + +We are trying to set up a renderable view, so determine +what the simulated time is, and try to get snapshots +both before and after that time if available. + +If we don't have a valid cg.snap after exiting this function, +then a 3D game view cannot be rendered. This should only happen +right after the initial connection. After cg.snap has been valid +once, it will never turn invalid. + +Even if cg.snap is valid, cg.nextSnap may not be, if the snapshot +hasn't arrived yet (it becomes an extrapolating situation instead +of an interpolating one) + +============ +*/ +void CG_ProcessSnapshots( void ) { + snapshot_t *snap; + int n; + + // see what the latest snapshot the client system has is + trap_GetCurrentSnapshotNumber( &n, &cg.latestSnapshotTime ); + if ( n != cg.latestSnapshotNum ) { + if ( n < cg.latestSnapshotNum ) { + // this should never happen + CG_Error( "CG_ProcessSnapshots: n < cg.latestSnapshotNum" ); + } + cg.latestSnapshotNum = n; + } + + // If we have yet to receive a snapshot, check for it. + // Once we have gotten the first snapshot, cg.snap will + // always have valid data for the rest of the game + while ( !cg.snap ) { + snap = CG_ReadNextSnapshot(); + if ( !snap ) { + // we can't continue until we get a snapshot + return; + } + + // set our weapon selection to what + // the playerstate is currently using + if ( !( snap->snapFlags & SNAPFLAG_NOT_ACTIVE ) ) { + CG_SetInitialSnapshot( snap ); + } + } + + // loop until we either have a valid nextSnap with a serverTime + // greater than cg.time to interpolate towards, or we run + // out of available snapshots + do { + // if we don't have a nextframe, try and read a new one in + if ( !cg.nextSnap ) { + snap = CG_ReadNextSnapshot(); + + // if we still don't have a nextframe, we will just have to + // extrapolate + if ( !snap ) { + break; + } + + CG_SetNextSnap( snap ); + + + // if time went backwards, we have a level restart or demo seek + 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" ); + } + } + + // if our time is < nextFrame's, we have a nice interpolating state + if ( cg.time >= cg.snap->serverTime && cg.time < cg.nextSnap->serverTime ) { + break; + } + + // we have passed the transition from nextFrame to frame + CG_TransitionSnapshot(); + } while ( 1 ); + + // assert our valid conditions upon exiting + if ( cg.snap == NULL ) { + CG_Error( "CG_ProcessSnapshots: cg.snap == NULL" ); + } + if ( cg.time < cg.snap->serverTime ) { + // this can happen right after a vid_restart + cg.time = cg.snap->serverTime; + } + if ( cg.nextSnap != NULL && cg.nextSnap->serverTime <= cg.time ) { + CG_Error( "CG_ProcessSnapshots: cg.nextSnap->serverTime <= cg.time" ); + } + +} + diff --git a/code/game/g_active.c b/code/game/g_active.c index a1e7e82..f78dac8 100644 --- a/code/game/g_active.c +++ b/code/game/g_active.c @@ -1,1197 +1,1197 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ -// - -#include "g_local.h" - - -/* -=============== -G_DamageFeedback - -Called just before a snapshot is sent to the given player. -Totals up all damage and generates both the player_state_t -damage values to that client for pain blends and kicks, and -global pain sound events for all clients. -=============== -*/ -void P_DamageFeedback( gentity_t *player ) { - gclient_t *client; - float count; - vec3_t angles; - - client = player->client; - if ( client->ps.pm_type == PM_DEAD ) { - return; - } - - // total points of damage shot at the player this frame - count = client->damage_blood + client->damage_armor; - if ( count == 0 ) { - return; // didn't take any damage - } - - if ( count > 255 ) { - count = 255; - } - - // send the information to the client - - // world damage (falling, slime, etc) uses a special code - // to make the blend blob centered instead of positional - if ( client->damage_fromWorld ) { - client->ps.damagePitch = 255; - client->ps.damageYaw = 255; - - client->damage_fromWorld = qfalse; - } else { - vectoangles( client->damage_from, angles ); - client->ps.damagePitch = angles[PITCH]/360.0 * 256; - client->ps.damageYaw = angles[YAW]/360.0 * 256; - } - - // play an apropriate pain sound - if ( (level.time > player->pain_debounce_time) && !(player->flags & FL_GODMODE) ) { - player->pain_debounce_time = level.time + 700; - G_AddEvent( player, EV_PAIN, player->health ); - client->ps.damageEvent++; - } - - - client->ps.damageCount = count; - - // - // clear totals - // - client->damage_blood = 0; - client->damage_armor = 0; - client->damage_knockback = 0; -} - - - -/* -============= -P_WorldEffects - -Check for lava / slime contents and drowning -============= -*/ -void P_WorldEffects( gentity_t *ent ) { - qboolean envirosuit; - int waterlevel; - - if ( ent->client->noclip ) { - ent->client->airOutTime = level.time + 12000; // don't need air - return; - } - - waterlevel = ent->waterlevel; - - envirosuit = ent->client->ps.powerups[PW_BATTLESUIT] > level.time; - - // - // check for drowning - // - if ( waterlevel == 3 ) { - // envirosuit give air - if ( envirosuit ) { - ent->client->airOutTime = level.time + 10000; - } - - // if out of air, start drowning - if ( ent->client->airOutTime < level.time) { - // drown! - ent->client->airOutTime += 1000; - if ( ent->health > 0 ) { - // take more damage the longer underwater - ent->damage += 2; - if (ent->damage > 15) - ent->damage = 15; - - // play a gurp sound instead of a normal pain sound - if (ent->health <= ent->damage) { - G_Sound(ent, CHAN_VOICE, G_SoundIndex("*drown.wav")); - } else if (rand()&1) { - G_Sound(ent, CHAN_VOICE, G_SoundIndex("sound/player/gurp1.wav")); - } else { - G_Sound(ent, CHAN_VOICE, G_SoundIndex("sound/player/gurp2.wav")); - } - - // don't play a normal pain sound - ent->pain_debounce_time = level.time + 200; - - G_Damage (ent, NULL, NULL, NULL, NULL, - ent->damage, DAMAGE_NO_ARMOR, MOD_WATER); - } - } - } else { - ent->client->airOutTime = level.time + 12000; - ent->damage = 2; - } - - // - // check for sizzle damage (move to pmove?) - // - if (waterlevel && - (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { - if (ent->health > 0 - && ent->pain_debounce_time <= level.time ) { - - if ( envirosuit ) { - G_AddEvent( ent, EV_POWERUP_BATTLESUIT, 0 ); - } else { - if (ent->watertype & CONTENTS_LAVA) { - G_Damage (ent, NULL, NULL, NULL, NULL, - 30*waterlevel, 0, MOD_LAVA); - } - - if (ent->watertype & CONTENTS_SLIME) { - G_Damage (ent, NULL, NULL, NULL, NULL, - 10*waterlevel, 0, MOD_SLIME); - } - } - } - } -} - - - -/* -=============== -G_SetClientSound -=============== -*/ -void G_SetClientSound( gentity_t *ent ) { -#ifdef MISSIONPACK - if( ent->s.eFlags & EF_TICKING ) { - ent->client->ps.loopSound = G_SoundIndex( "sound/weapons/proxmine/wstbtick.wav"); - } - else -#endif - if (ent->waterlevel && (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { - ent->client->ps.loopSound = level.snd_fry; - } else { - ent->client->ps.loopSound = 0; - } -} - - - -//============================================================== - -/* -============== -ClientImpacts -============== -*/ -void ClientImpacts( gentity_t *ent, pmove_t *pm ) { - int i, j; - trace_t trace; - gentity_t *other; - - memset( &trace, 0, sizeof( trace ) ); - for (i=0 ; inumtouch ; i++) { - for (j=0 ; jtouchents[j] == pm->touchents[i] ) { - break; - } - } - if (j != i) { - continue; // duplicated - } - other = &g_entities[ pm->touchents[i] ]; - - if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) { - ent->touch( ent, other, &trace ); - } - - if ( !other->touch ) { - continue; - } - - other->touch( other, ent, &trace ); - } - -} - -/* -============ -G_TouchTriggers - -Find all trigger entities that ent's current position touches. -Spectators will only interact with teleporters. -============ -*/ -void G_TouchTriggers( gentity_t *ent ) { - int i, num; - int touch[MAX_GENTITIES]; - gentity_t *hit; - trace_t trace; - vec3_t mins, maxs; - static vec3_t range = { 40, 40, 52 }; - - if ( !ent->client ) { - return; - } - - // dead clients don't activate triggers! - if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) { - return; - } - - VectorSubtract( ent->client->ps.origin, range, mins ); - VectorAdd( ent->client->ps.origin, range, maxs ); - - num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); - - // can't use ent->absmin, because that has a one unit pad - VectorAdd( ent->client->ps.origin, ent->r.mins, mins ); - VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs ); - - for ( i=0 ; itouch && !ent->touch ) { - continue; - } - if ( !( hit->r.contents & CONTENTS_TRIGGER ) ) { - continue; - } - - // ignore most entities if a spectator - if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { - if ( hit->s.eType != ET_TELEPORT_TRIGGER && - // this is ugly but adding a new ET_? type will - // most likely cause network incompatibilities - hit->touch != Touch_DoorTrigger) { - continue; - } - } - - // use seperate code for determining if an item is picked up - // so you don't have to actually contact its bounding box - if ( hit->s.eType == ET_ITEM ) { - if ( !BG_PlayerTouchesItem( &ent->client->ps, &hit->s, level.time ) ) { - continue; - } - } else { - if ( !trap_EntityContact( mins, maxs, hit ) ) { - continue; - } - } - - memset( &trace, 0, sizeof(trace) ); - - if ( hit->touch ) { - hit->touch (hit, ent, &trace); - } - - if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) { - ent->touch( ent, hit, &trace ); - } - } - - // if we didn't touch a jump pad this pmove frame - if ( ent->client->ps.jumppad_frame != ent->client->ps.pmove_framecount ) { - ent->client->ps.jumppad_frame = 0; - ent->client->ps.jumppad_ent = 0; - } -} - -/* -================= -SpectatorThink -================= -*/ -void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) { - pmove_t pm; - gclient_t *client; - - client = ent->client; - - if ( client->sess.spectatorState != SPECTATOR_FOLLOW ) { - client->ps.pm_type = PM_SPECTATOR; - client->ps.speed = 400; // faster than normal - - // set up for pmove - memset (&pm, 0, sizeof(pm)); - pm.ps = &client->ps; - pm.cmd = *ucmd; - pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; // spectators can fly through bodies - pm.trace = trap_Trace; - pm.pointcontents = trap_PointContents; - - // perform a pmove - Pmove (&pm); - // save results of pmove - VectorCopy( client->ps.origin, ent->s.origin ); - - G_TouchTriggers( ent ); - trap_UnlinkEntity( ent ); - } - - client->oldbuttons = client->buttons; - client->buttons = ucmd->buttons; - - // attack button cycles through spectators - if ( ( client->buttons & BUTTON_ATTACK ) && ! ( client->oldbuttons & BUTTON_ATTACK ) ) { - Cmd_FollowCycle_f( ent, 1 ); - } -} - - - -/* -================= -ClientInactivityTimer - -Returns qfalse if the client is dropped -================= -*/ -qboolean ClientInactivityTimer( gclient_t *client ) { - if ( ! g_inactivity.integer ) { - // give everyone some time, so if the operator sets g_inactivity during - // gameplay, everyone isn't kicked - client->inactivityTime = level.time + 60 * 1000; - client->inactivityWarning = qfalse; - } else if ( client->pers.cmd.forwardmove || - client->pers.cmd.rightmove || - client->pers.cmd.upmove || - (client->pers.cmd.buttons & BUTTON_ATTACK) ) { - client->inactivityTime = level.time + g_inactivity.integer * 1000; - client->inactivityWarning = qfalse; - } else if ( !client->pers.localClient ) { - if ( level.time > client->inactivityTime ) { - trap_DropClient( client - level.clients, "Dropped due to inactivity" ); - return qfalse; - } - if ( level.time > client->inactivityTime - 10000 && !client->inactivityWarning ) { - client->inactivityWarning = qtrue; - trap_SendServerCommand( client - level.clients, "cp \"Ten seconds until inactivity drop!\n\"" ); - } - } - return qtrue; -} - -/* -================== -ClientTimerActions - -Actions that happen once a second -================== -*/ -void ClientTimerActions( gentity_t *ent, int msec ) { - gclient_t *client; -#ifdef MISSIONPACK - int maxHealth; -#endif - - client = ent->client; - client->timeResidual += msec; - - while ( client->timeResidual >= 1000 ) { - client->timeResidual -= 1000; - - // regenerate -#ifdef MISSIONPACK - if( bg_itemlist[client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_GUARD ) { - maxHealth = client->ps.stats[STAT_MAX_HEALTH] / 2; - } - else if ( client->ps.powerups[PW_REGEN] ) { - maxHealth = client->ps.stats[STAT_MAX_HEALTH]; - } - else { - maxHealth = 0; - } - if( maxHealth ) { - if ( ent->health < maxHealth ) { - ent->health += 15; - if ( ent->health > maxHealth * 1.1 ) { - ent->health = maxHealth * 1.1; - } - G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); - } else if ( ent->health < maxHealth * 2) { - ent->health += 5; - if ( ent->health > maxHealth * 2 ) { - ent->health = maxHealth * 2; - } - G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); - } -#else - if ( client->ps.powerups[PW_REGEN] ) { - if ( ent->health < client->ps.stats[STAT_MAX_HEALTH]) { - ent->health += 15; - if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 1.1 ) { - ent->health = client->ps.stats[STAT_MAX_HEALTH] * 1.1; - } - G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); - } else if ( ent->health < client->ps.stats[STAT_MAX_HEALTH] * 2) { - ent->health += 5; - if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 2 ) { - ent->health = client->ps.stats[STAT_MAX_HEALTH] * 2; - } - G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); - } -#endif - } else { - // count down health when over max - if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] ) { - ent->health--; - } - } - - // count down armor when over max - if ( client->ps.stats[STAT_ARMOR] > client->ps.stats[STAT_MAX_HEALTH] ) { - client->ps.stats[STAT_ARMOR]--; - } - } -#ifdef MISSIONPACK - if( bg_itemlist[client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_AMMOREGEN ) { - int w, max, inc, t, i; - int weapList[]={WP_MACHINEGUN,WP_SHOTGUN,WP_GRENADE_LAUNCHER,WP_ROCKET_LAUNCHER,WP_LIGHTNING,WP_RAILGUN,WP_PLASMAGUN,WP_BFG,WP_NAILGUN,WP_PROX_LAUNCHER,WP_CHAINGUN}; - int weapCount = sizeof(weapList) / sizeof(int); - // - for (i = 0; i < weapCount; i++) { - w = weapList[i]; - - switch(w) { - case WP_MACHINEGUN: max = 50; inc = 4; t = 1000; break; - case WP_SHOTGUN: max = 10; inc = 1; t = 1500; break; - case WP_GRENADE_LAUNCHER: max = 10; inc = 1; t = 2000; break; - case WP_ROCKET_LAUNCHER: max = 10; inc = 1; t = 1750; break; - case WP_LIGHTNING: max = 50; inc = 5; t = 1500; break; - case WP_RAILGUN: max = 10; inc = 1; t = 1750; break; - case WP_PLASMAGUN: max = 50; inc = 5; t = 1500; break; - case WP_BFG: max = 10; inc = 1; t = 4000; break; - case WP_NAILGUN: max = 10; inc = 1; t = 1250; break; - case WP_PROX_LAUNCHER: max = 5; inc = 1; t = 2000; break; - case WP_CHAINGUN: max = 100; inc = 5; t = 1000; break; - default: max = 0; inc = 0; t = 1000; break; - } - client->ammoTimes[w] += msec; - if ( client->ps.ammo[w] >= max ) { - client->ammoTimes[w] = 0; - } - if ( client->ammoTimes[w] >= t ) { - while ( client->ammoTimes[w] >= t ) - client->ammoTimes[w] -= t; - client->ps.ammo[w] += inc; - if ( client->ps.ammo[w] > max ) { - client->ps.ammo[w] = max; - } - } - } - } -#endif -} - -/* -==================== -ClientIntermissionThink -==================== -*/ -void ClientIntermissionThink( gclient_t *client ) { - client->ps.eFlags &= ~EF_TALK; - client->ps.eFlags &= ~EF_FIRING; - - // the level will exit when everyone wants to or after timeouts - - // swap and latch button actions - client->oldbuttons = client->buttons; - client->buttons = client->pers.cmd.buttons; - if ( client->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) & ( client->oldbuttons ^ client->buttons ) ) { - // this used to be an ^1 but once a player says ready, it should stick - client->readyToExit = 1; - } -} - - -/* -================ -ClientEvents - -Events will be passed on to the clients for presentation, -but any server game effects are handled here -================ -*/ -void ClientEvents( gentity_t *ent, int oldEventSequence ) { - int i, j; - int event; - gclient_t *client; - int damage; - vec3_t dir; - vec3_t origin, angles; -// qboolean fired; - gitem_t *item; - gentity_t *drop; - - client = ent->client; - - if ( oldEventSequence < client->ps.eventSequence - MAX_PS_EVENTS ) { - oldEventSequence = client->ps.eventSequence - MAX_PS_EVENTS; - } - for ( i = oldEventSequence ; i < client->ps.eventSequence ; i++ ) { - event = client->ps.events[ i & (MAX_PS_EVENTS-1) ]; - - switch ( event ) { - case EV_FALL_MEDIUM: - case EV_FALL_FAR: - if ( ent->s.eType != ET_PLAYER ) { - break; // not in the player model - } - if ( g_dmflags.integer & DF_NO_FALLING ) { - break; - } - if ( event == EV_FALL_FAR ) { - damage = 10; - } else { - damage = 5; - } - VectorSet (dir, 0, 0, 1); - ent->pain_debounce_time = level.time + 200; // no normal pain sound - G_Damage (ent, NULL, NULL, NULL, NULL, damage, 0, MOD_FALLING); - break; - - case EV_FIRE_WEAPON: - FireWeapon( ent ); - break; - - case EV_USE_ITEM1: // teleporter - // drop flags in CTF - item = NULL; - j = 0; - - if ( ent->client->ps.powerups[ PW_REDFLAG ] ) { - item = BG_FindItemForPowerup( PW_REDFLAG ); - j = PW_REDFLAG; - } else if ( ent->client->ps.powerups[ PW_BLUEFLAG ] ) { - item = BG_FindItemForPowerup( PW_BLUEFLAG ); - j = PW_BLUEFLAG; - } else if ( ent->client->ps.powerups[ PW_NEUTRALFLAG ] ) { - item = BG_FindItemForPowerup( PW_NEUTRALFLAG ); - j = PW_NEUTRALFLAG; - } - - if ( item ) { - drop = Drop_Item( ent, item, 0 ); - // decide how many seconds it has left - drop->count = ( ent->client->ps.powerups[ j ] - level.time ) / 1000; - if ( drop->count < 1 ) { - drop->count = 1; - } - - ent->client->ps.powerups[ j ] = 0; - } - -#ifdef MISSIONPACK - if ( g_gametype.integer == GT_HARVESTER ) { - if ( ent->client->ps.generic1 > 0 ) { - if ( ent->client->sess.sessionTeam == TEAM_RED ) { - item = BG_FindItem( "Blue Cube" ); - } else { - item = BG_FindItem( "Red Cube" ); - } - if ( item ) { - for ( j = 0; j < ent->client->ps.generic1; j++ ) { - drop = Drop_Item( ent, item, 0 ); - if ( ent->client->sess.sessionTeam == TEAM_RED ) { - drop->spawnflags = TEAM_BLUE; - } else { - drop->spawnflags = TEAM_RED; - } - } - } - ent->client->ps.generic1 = 0; - } - } -#endif - SelectSpawnPoint( ent->client->ps.origin, origin, angles ); - TeleportPlayer( ent, origin, angles ); - break; - - case EV_USE_ITEM2: // medkit - ent->health = ent->client->ps.stats[STAT_MAX_HEALTH] + 25; - - break; - -#ifdef MISSIONPACK - case EV_USE_ITEM3: // kamikaze - // make sure the invulnerability is off - ent->client->invulnerabilityTime = 0; - // start the kamikze - G_StartKamikaze( ent ); - break; - - case EV_USE_ITEM4: // portal - if( ent->client->portalID ) { - DropPortalSource( ent ); - } - else { - DropPortalDestination( ent ); - } - break; - case EV_USE_ITEM5: // invulnerability - ent->client->invulnerabilityTime = level.time + 10000; - break; -#endif - - default: - break; - } - } - -} - -#ifdef MISSIONPACK -/* -============== -StuckInOtherClient -============== -*/ -static int StuckInOtherClient(gentity_t *ent) { - int i; - gentity_t *ent2; - - ent2 = &g_entities[0]; - for ( i = 0; i < MAX_CLIENTS; i++, ent2++ ) { - if ( ent2 == ent ) { - continue; - } - if ( !ent2->inuse ) { - continue; - } - if ( !ent2->client ) { - continue; - } - if ( ent2->health <= 0 ) { - continue; - } - // - if (ent2->r.absmin[0] > ent->r.absmax[0]) - continue; - if (ent2->r.absmin[1] > ent->r.absmax[1]) - continue; - if (ent2->r.absmin[2] > ent->r.absmax[2]) - continue; - if (ent2->r.absmax[0] < ent->r.absmin[0]) - continue; - if (ent2->r.absmax[1] < ent->r.absmin[1]) - continue; - if (ent2->r.absmax[2] < ent->r.absmin[2]) - continue; - return qtrue; - } - return qfalse; -} -#endif - -void BotTestSolid(vec3_t origin); - -/* -============== -SendPendingPredictableEvents -============== -*/ -void SendPendingPredictableEvents( playerState_t *ps ) { - gentity_t *t; - int event, seq; - int extEvent, number; - - // if there are still events pending - if ( ps->entityEventSequence < ps->eventSequence ) { - // create a temporary entity for this event which is sent to everyone - // except the client who generated the event - seq = ps->entityEventSequence & (MAX_PS_EVENTS-1); - event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); - // set external event to zero before calling BG_PlayerStateToEntityState - extEvent = ps->externalEvent; - ps->externalEvent = 0; - // create temporary entity for event - t = G_TempEntity( ps->origin, event ); - number = t->s.number; - BG_PlayerStateToEntityState( ps, &t->s, qtrue ); - t->s.number = number; - t->s.eType = ET_EVENTS + event; - t->s.eFlags |= EF_PLAYER_EVENT; - t->s.otherEntityNum = ps->clientNum; - // send to everyone except the client who generated the event - t->r.svFlags |= SVF_NOTSINGLECLIENT; - t->r.singleClient = ps->clientNum; - // set back external event - ps->externalEvent = extEvent; - } -} - -/* -============== -ClientThink - -This will be called once for each client frame, which will -usually be a couple times for each server frame on fast clients. - -If "g_synchronousClients 1" is set, this will be called exactly -once for each server frame, which makes for smooth demo recording. -============== -*/ -void ClientThink_real( gentity_t *ent ) { - gclient_t *client; - pmove_t pm; - int oldEventSequence; - int msec; - usercmd_t *ucmd; - - client = ent->client; - - // don't think if the client is not yet connected (and thus not yet spawned in) - if (client->pers.connected != CON_CONNECTED) { - return; - } - // mark the time, so the connection sprite can be removed - ucmd = &ent->client->pers.cmd; - - // sanity check the command time to prevent speedup cheating - if ( ucmd->serverTime > level.time + 200 ) { - ucmd->serverTime = level.time + 200; -// G_Printf("serverTime <<<<<\n" ); - } - if ( ucmd->serverTime < level.time - 1000 ) { - ucmd->serverTime = level.time - 1000; -// G_Printf("serverTime >>>>>\n" ); - } - - msec = ucmd->serverTime - client->ps.commandTime; - // following others may result in bad times, but we still want - // to check for follow toggles - if ( msec < 1 && client->sess.spectatorState != SPECTATOR_FOLLOW ) { - return; - } - if ( msec > 200 ) { - msec = 200; - } - - if ( pmove_msec.integer < 8 ) { - trap_Cvar_Set("pmove_msec", "8"); - } - else if (pmove_msec.integer > 33) { - trap_Cvar_Set("pmove_msec", "33"); - } - - if ( pmove_fixed.integer || client->pers.pmoveFixed ) { - ucmd->serverTime = ((ucmd->serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; - //if (ucmd->serverTime - client->ps.commandTime <= 0) - // return; - } - - // - // check for exiting intermission - // - if ( level.intermissiontime ) { - ClientIntermissionThink( client ); - return; - } - - // spectators don't do much - if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { - if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { - return; - } - SpectatorThink( ent, ucmd ); - return; - } - - // check for inactivity timer, but never drop the local client of a non-dedicated server - if ( !ClientInactivityTimer( client ) ) { - return; - } - - // clear the rewards if time - if ( level.time > client->rewardTime ) { - client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); - } - - if ( client->noclip ) { - client->ps.pm_type = PM_NOCLIP; - } else if ( client->ps.stats[STAT_HEALTH] <= 0 ) { - client->ps.pm_type = PM_DEAD; - } else { - client->ps.pm_type = PM_NORMAL; - } - - client->ps.gravity = g_gravity.value; - - // set speed - client->ps.speed = g_speed.value; - -#ifdef MISSIONPACK - if( bg_itemlist[client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_SCOUT ) { - client->ps.speed *= 1.5; - } - else -#endif - if ( client->ps.powerups[PW_HASTE] ) { - client->ps.speed *= 1.3; - } - - // Let go of the hook if we aren't firing - if ( client->ps.weapon == WP_GRAPPLING_HOOK && - client->hook && !( ucmd->buttons & BUTTON_ATTACK ) ) { - Weapon_HookFree(client->hook); - } - - // set up for pmove - oldEventSequence = client->ps.eventSequence; - - memset (&pm, 0, sizeof(pm)); - - // check for the hit-scan gauntlet, don't let the action - // go through as an attack unless it actually hits something - if ( client->ps.weapon == WP_GAUNTLET && !( ucmd->buttons & BUTTON_TALK ) && - ( ucmd->buttons & BUTTON_ATTACK ) && client->ps.weaponTime <= 0 ) { - pm.gauntletHit = CheckGauntletAttack( ent ); - } - - if ( ent->flags & FL_FORCE_GESTURE ) { - ent->flags &= ~FL_FORCE_GESTURE; - ent->client->pers.cmd.buttons |= BUTTON_GESTURE; - } - -#ifdef MISSIONPACK - // check for invulnerability expansion before doing the Pmove - if (client->ps.powerups[PW_INVULNERABILITY] ) { - if ( !(client->ps.pm_flags & PMF_INVULEXPAND) ) { - vec3_t mins = { -42, -42, -42 }; - vec3_t maxs = { 42, 42, 42 }; - vec3_t oldmins, oldmaxs; - - VectorCopy (ent->r.mins, oldmins); - VectorCopy (ent->r.maxs, oldmaxs); - // expand - VectorCopy (mins, ent->r.mins); - VectorCopy (maxs, ent->r.maxs); - trap_LinkEntity(ent); - // check if this would get anyone stuck in this player - if ( !StuckInOtherClient(ent) ) { - // set flag so the expanded size will be set in PM_CheckDuck - client->ps.pm_flags |= PMF_INVULEXPAND; - } - // set back - VectorCopy (oldmins, ent->r.mins); - VectorCopy (oldmaxs, ent->r.maxs); - trap_LinkEntity(ent); - } - } -#endif - - pm.ps = &client->ps; - pm.cmd = *ucmd; - if ( pm.ps->pm_type == PM_DEAD ) { - pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; - } - else if ( ent->r.svFlags & SVF_BOT ) { - pm.tracemask = MASK_PLAYERSOLID | CONTENTS_BOTCLIP; - } - else { - pm.tracemask = MASK_PLAYERSOLID; - } - pm.trace = trap_Trace; - pm.pointcontents = trap_PointContents; - pm.debugLevel = g_debugMove.integer; - pm.noFootsteps = ( g_dmflags.integer & DF_NO_FOOTSTEPS ) > 0; - - pm.pmove_fixed = pmove_fixed.integer | client->pers.pmoveFixed; - pm.pmove_msec = pmove_msec.integer; - - VectorCopy( client->ps.origin, client->oldOrigin ); - -#ifdef MISSIONPACK - if (level.intermissionQueued != 0 && g_singlePlayer.integer) { - if ( level.time - level.intermissionQueued >= 1000 ) { - pm.cmd.buttons = 0; - pm.cmd.forwardmove = 0; - pm.cmd.rightmove = 0; - pm.cmd.upmove = 0; - if ( level.time - level.intermissionQueued >= 2000 && level.time - level.intermissionQueued <= 2500 ) { - trap_SendConsoleCommand( EXEC_APPEND, "centerview\n"); - } - ent->client->ps.pm_type = PM_SPINTERMISSION; - } - } - Pmove (&pm); -#else - Pmove (&pm); -#endif - - // save results of pmove - if ( ent->client->ps.eventSequence != oldEventSequence ) { - ent->eventTime = level.time; - } - if (g_smoothClients.integer) { - BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); - } - else { - BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); - } - SendPendingPredictableEvents( &ent->client->ps ); - - if ( !( ent->client->ps.eFlags & EF_FIRING ) ) { - client->fireHeld = qfalse; // for grapple - } - - // use the snapped origin for linking so it matches client predicted versions - VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); - - VectorCopy (pm.mins, ent->r.mins); - VectorCopy (pm.maxs, ent->r.maxs); - - ent->waterlevel = pm.waterlevel; - ent->watertype = pm.watertype; - - // execute client events - ClientEvents( ent, oldEventSequence ); - - // link entity now, after any personal teleporters have been used - trap_LinkEntity (ent); - if ( !ent->client->noclip ) { - G_TouchTriggers( ent ); - } - - // NOTE: now copy the exact origin over otherwise clients can be snapped into solid - VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); - - //test for solid areas in the AAS file - BotTestAAS(ent->r.currentOrigin); - - // touch other objects - ClientImpacts( ent, &pm ); - - // save results of triggers and client events - if (ent->client->ps.eventSequence != oldEventSequence) { - ent->eventTime = level.time; - } - - // swap and latch button actions - client->oldbuttons = client->buttons; - client->buttons = ucmd->buttons; - client->latched_buttons |= client->buttons & ~client->oldbuttons; - - // check for respawning - if ( client->ps.stats[STAT_HEALTH] <= 0 ) { - // wait for the attack button to be pressed - if ( level.time > client->respawnTime ) { - // forcerespawn is to prevent users from waiting out powerups - if ( g_forcerespawn.integer > 0 && - ( level.time - client->respawnTime ) > g_forcerespawn.integer * 1000 ) { - respawn( ent ); - return; - } - - // pressing attack or use is the normal respawn method - if ( ucmd->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) ) { - respawn( ent ); - } - } - return; - } - - // perform once-a-second actions - ClientTimerActions( ent, msec ); -} - -/* -================== -ClientThink - -A new command has arrived from the client -================== -*/ -void ClientThink( int clientNum ) { - gentity_t *ent; - - ent = g_entities + clientNum; - trap_GetUsercmd( clientNum, &ent->client->pers.cmd ); - - // mark the time we got info, so we can display the - // phone jack if they don't get any for a while - ent->client->lastCmdTime = level.time; - - // demo playback: don't run ClientThink_real — cgame owns - // the camera movement, G_RunFrame handles buttons and PVS origin - if ( g_svDemoPlaying.integer ) { - return; - } - - if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { - ClientThink_real( ent ); - } -} - - -void G_RunClient( gentity_t *ent ) { - if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { - return; - } - ent->client->pers.cmd.serverTime = level.time; - ClientThink_real( ent ); -} - - -/* -================== -SpectatorClientEndFrame - -================== -*/ -void SpectatorClientEndFrame( gentity_t *ent ) { - gclient_t *cl; - - // if we are doing a chase cam or a remote view, grab the latest info - if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { - int clientNum, flags; - - clientNum = ent->client->sess.spectatorClient; - - // team follow1 and team follow2 go to whatever clients are playing - if ( clientNum == -1 ) { - clientNum = level.follow1; - } else if ( clientNum == -2 ) { - clientNum = level.follow2; - } - if ( clientNum >= 0 ) { - cl = &level.clients[ clientNum ]; - if ( cl->pers.connected == CON_CONNECTED && cl->sess.sessionTeam != TEAM_SPECTATOR ) { - flags = (cl->ps.eFlags & ~(EF_VOTED | EF_TEAMVOTED)) | (ent->client->ps.eFlags & (EF_VOTED | EF_TEAMVOTED)); - ent->client->ps = cl->ps; - ent->client->ps.pm_flags |= PMF_FOLLOW; - ent->client->ps.eFlags = flags; - return; - } else { - // drop them to free spectators unless they are dedicated camera followers - if ( ent->client->sess.spectatorClient >= 0 ) { - ent->client->sess.spectatorState = SPECTATOR_FREE; - ClientBegin( ent->client - level.clients ); - } - } - } - } - - if ( ent->client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { - ent->client->ps.pm_flags |= PMF_SCOREBOARD; - } else { - ent->client->ps.pm_flags &= ~PMF_SCOREBOARD; - } -} - -/* -============== -ClientEndFrame - -Called at the end of each server frame for each connected client -A fast client will have multiple ClientThink for each ClientEdFrame, -while a slow client may have multiple ClientEndFrame between ClientThink. -============== -*/ -void ClientEndFrame( gentity_t *ent ) { - int i; - clientPersistant_t *pers; - - if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { - SpectatorClientEndFrame( ent ); - return; - } - - pers = &ent->client->pers; - - // turn off any expired powerups - for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { - if ( ent->client->ps.powerups[ i ] < level.time ) { - ent->client->ps.powerups[ i ] = 0; - } - } - -#ifdef MISSIONPACK - // set powerup for player animation - if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_GUARD ) { - ent->client->ps.powerups[PW_GUARD] = level.time; - } - if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_SCOUT ) { - ent->client->ps.powerups[PW_SCOUT] = level.time; - } - if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_DOUBLER ) { - ent->client->ps.powerups[PW_DOUBLER] = level.time; - } - if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_AMMOREGEN ) { - ent->client->ps.powerups[PW_AMMOREGEN] = level.time; - } - if ( ent->client->invulnerabilityTime > level.time ) { - ent->client->ps.powerups[PW_INVULNERABILITY] = level.time; - } -#endif - - // save network bandwidth -#if 0 - if ( !g_synchronousClients->integer && ent->client->ps.pm_type == PM_NORMAL ) { - // FIXME: this must change eventually for non-sync demo recording - VectorClear( ent->client->ps.viewangles ); - } -#endif - - // - // If the end of unit layout is displayed, don't give - // the player any normal movement attributes - // - if ( level.intermissiontime ) { - return; - } - - // burn from lava, etc - P_WorldEffects (ent); - - // apply all the damage taken this frame - P_DamageFeedback (ent); - - // add the EF_CONNECTION flag if we haven't gotten commands recently - if ( level.time - ent->client->lastCmdTime > 1000 ) { - ent->s.eFlags |= EF_CONNECTION; - } else { - ent->s.eFlags &= ~EF_CONNECTION; - } - - ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... - - G_SetClientSound (ent); - - // set the latest infor - if (g_smoothClients.integer) { - BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); - } - else { - BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); - } - SendPendingPredictableEvents( &ent->client->ps ); - - // set the bit for the reachability area the client is currently in -// i = trap_AAS_PointReachabilityAreaIndex( ent->client->ps.origin ); -// ent->client->areabits[i >> 3] |= 1 << (i & 7); -} - - +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ +// + +#include "g_local.h" + + +/* +=============== +G_DamageFeedback + +Called just before a snapshot is sent to the given player. +Totals up all damage and generates both the player_state_t +damage values to that client for pain blends and kicks, and +global pain sound events for all clients. +=============== +*/ +void P_DamageFeedback( gentity_t *player ) { + gclient_t *client; + float count; + vec3_t angles; + + client = player->client; + if ( client->ps.pm_type == PM_DEAD ) { + return; + } + + // total points of damage shot at the player this frame + count = client->damage_blood + client->damage_armor; + if ( count == 0 ) { + return; // didn't take any damage + } + + if ( count > 255 ) { + count = 255; + } + + // send the information to the client + + // world damage (falling, slime, etc) uses a special code + // to make the blend blob centered instead of positional + if ( client->damage_fromWorld ) { + client->ps.damagePitch = 255; + client->ps.damageYaw = 255; + + client->damage_fromWorld = qfalse; + } else { + vectoangles( client->damage_from, angles ); + client->ps.damagePitch = angles[PITCH]/360.0 * 256; + client->ps.damageYaw = angles[YAW]/360.0 * 256; + } + + // play an apropriate pain sound + if ( (level.time > player->pain_debounce_time) && !(player->flags & FL_GODMODE) ) { + player->pain_debounce_time = level.time + 700; + G_AddEvent( player, EV_PAIN, player->health ); + client->ps.damageEvent++; + } + + + client->ps.damageCount = count; + + // + // clear totals + // + client->damage_blood = 0; + client->damage_armor = 0; + client->damage_knockback = 0; +} + + + +/* +============= +P_WorldEffects + +Check for lava / slime contents and drowning +============= +*/ +void P_WorldEffects( gentity_t *ent ) { + qboolean envirosuit; + int waterlevel; + + if ( ent->client->noclip ) { + ent->client->airOutTime = level.time + 12000; // don't need air + return; + } + + waterlevel = ent->waterlevel; + + envirosuit = ent->client->ps.powerups[PW_BATTLESUIT] > level.time; + + // + // check for drowning + // + if ( waterlevel == 3 ) { + // envirosuit give air + if ( envirosuit ) { + ent->client->airOutTime = level.time + 10000; + } + + // if out of air, start drowning + if ( ent->client->airOutTime < level.time) { + // drown! + ent->client->airOutTime += 1000; + if ( ent->health > 0 ) { + // take more damage the longer underwater + ent->damage += 2; + if (ent->damage > 15) + ent->damage = 15; + + // play a gurp sound instead of a normal pain sound + if (ent->health <= ent->damage) { + G_Sound(ent, CHAN_VOICE, G_SoundIndex("*drown.wav")); + } else if (rand()&1) { + G_Sound(ent, CHAN_VOICE, G_SoundIndex("sound/player/gurp1.wav")); + } else { + G_Sound(ent, CHAN_VOICE, G_SoundIndex("sound/player/gurp2.wav")); + } + + // don't play a normal pain sound + ent->pain_debounce_time = level.time + 200; + + G_Damage (ent, NULL, NULL, NULL, NULL, + ent->damage, DAMAGE_NO_ARMOR, MOD_WATER); + } + } + } else { + ent->client->airOutTime = level.time + 12000; + ent->damage = 2; + } + + // + // check for sizzle damage (move to pmove?) + // + if (waterlevel && + (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { + if (ent->health > 0 + && ent->pain_debounce_time <= level.time ) { + + if ( envirosuit ) { + G_AddEvent( ent, EV_POWERUP_BATTLESUIT, 0 ); + } else { + if (ent->watertype & CONTENTS_LAVA) { + G_Damage (ent, NULL, NULL, NULL, NULL, + 30*waterlevel, 0, MOD_LAVA); + } + + if (ent->watertype & CONTENTS_SLIME) { + G_Damage (ent, NULL, NULL, NULL, NULL, + 10*waterlevel, 0, MOD_SLIME); + } + } + } + } +} + + + +/* +=============== +G_SetClientSound +=============== +*/ +void G_SetClientSound( gentity_t *ent ) { +#ifdef MISSIONPACK + if( ent->s.eFlags & EF_TICKING ) { + ent->client->ps.loopSound = G_SoundIndex( "sound/weapons/proxmine/wstbtick.wav"); + } + else +#endif + if (ent->waterlevel && (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { + ent->client->ps.loopSound = level.snd_fry; + } else { + ent->client->ps.loopSound = 0; + } +} + + + +//============================================================== + +/* +============== +ClientImpacts +============== +*/ +void ClientImpacts( gentity_t *ent, pmove_t *pm ) { + int i, j; + trace_t trace; + gentity_t *other; + + memset( &trace, 0, sizeof( trace ) ); + for (i=0 ; inumtouch ; i++) { + for (j=0 ; jtouchents[j] == pm->touchents[i] ) { + break; + } + } + if (j != i) { + continue; // duplicated + } + other = &g_entities[ pm->touchents[i] ]; + + if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) { + ent->touch( ent, other, &trace ); + } + + if ( !other->touch ) { + continue; + } + + other->touch( other, ent, &trace ); + } + +} + +/* +============ +G_TouchTriggers + +Find all trigger entities that ent's current position touches. +Spectators will only interact with teleporters. +============ +*/ +void G_TouchTriggers( gentity_t *ent ) { + int i, num; + int touch[MAX_GENTITIES]; + gentity_t *hit; + trace_t trace; + vec3_t mins, maxs; + static vec3_t range = { 40, 40, 52 }; + + if ( !ent->client ) { + return; + } + + // dead clients don't activate triggers! + if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) { + return; + } + + VectorSubtract( ent->client->ps.origin, range, mins ); + VectorAdd( ent->client->ps.origin, range, maxs ); + + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + // can't use ent->absmin, because that has a one unit pad + VectorAdd( ent->client->ps.origin, ent->r.mins, mins ); + VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs ); + + for ( i=0 ; itouch && !ent->touch ) { + continue; + } + if ( !( hit->r.contents & CONTENTS_TRIGGER ) ) { + continue; + } + + // ignore most entities if a spectator + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( hit->s.eType != ET_TELEPORT_TRIGGER && + // this is ugly but adding a new ET_? type will + // most likely cause network incompatibilities + hit->touch != Touch_DoorTrigger) { + continue; + } + } + + // use seperate code for determining if an item is picked up + // so you don't have to actually contact its bounding box + if ( hit->s.eType == ET_ITEM ) { + if ( !BG_PlayerTouchesItem( &ent->client->ps, &hit->s, level.time ) ) { + continue; + } + } else { + if ( !trap_EntityContact( mins, maxs, hit ) ) { + continue; + } + } + + memset( &trace, 0, sizeof(trace) ); + + if ( hit->touch ) { + hit->touch (hit, ent, &trace); + } + + if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) { + ent->touch( ent, hit, &trace ); + } + } + + // if we didn't touch a jump pad this pmove frame + if ( ent->client->ps.jumppad_frame != ent->client->ps.pmove_framecount ) { + ent->client->ps.jumppad_frame = 0; + ent->client->ps.jumppad_ent = 0; + } +} + +/* +================= +SpectatorThink +================= +*/ +void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) { + pmove_t pm; + gclient_t *client; + + client = ent->client; + + if ( client->sess.spectatorState != SPECTATOR_FOLLOW ) { + client->ps.pm_type = PM_SPECTATOR; + client->ps.speed = 400; // faster than normal + + // set up for pmove + memset (&pm, 0, sizeof(pm)); + pm.ps = &client->ps; + pm.cmd = *ucmd; + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; // spectators can fly through bodies + pm.trace = trap_Trace; + pm.pointcontents = trap_PointContents; + + // perform a pmove + Pmove (&pm); + // save results of pmove + VectorCopy( client->ps.origin, ent->s.origin ); + + G_TouchTriggers( ent ); + trap_UnlinkEntity( ent ); + } + + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + + // attack button cycles through spectators + if ( ( client->buttons & BUTTON_ATTACK ) && ! ( client->oldbuttons & BUTTON_ATTACK ) ) { + Cmd_FollowCycle_f( ent, 1 ); + } +} + + + +/* +================= +ClientInactivityTimer + +Returns qfalse if the client is dropped +================= +*/ +qboolean ClientInactivityTimer( gclient_t *client ) { + if ( ! g_inactivity.integer ) { + // give everyone some time, so if the operator sets g_inactivity during + // gameplay, everyone isn't kicked + client->inactivityTime = level.time + 60 * 1000; + client->inactivityWarning = qfalse; + } else if ( client->pers.cmd.forwardmove || + client->pers.cmd.rightmove || + client->pers.cmd.upmove || + (client->pers.cmd.buttons & BUTTON_ATTACK) ) { + client->inactivityTime = level.time + g_inactivity.integer * 1000; + client->inactivityWarning = qfalse; + } else if ( !client->pers.localClient ) { + if ( level.time > client->inactivityTime ) { + trap_DropClient( client - level.clients, "Dropped due to inactivity" ); + return qfalse; + } + if ( level.time > client->inactivityTime - 10000 && !client->inactivityWarning ) { + client->inactivityWarning = qtrue; + trap_SendServerCommand( client - level.clients, "cp \"Ten seconds until inactivity drop!\n\"" ); + } + } + return qtrue; +} + +/* +================== +ClientTimerActions + +Actions that happen once a second +================== +*/ +void ClientTimerActions( gentity_t *ent, int msec ) { + gclient_t *client; +#ifdef MISSIONPACK + int maxHealth; +#endif + + client = ent->client; + client->timeResidual += msec; + + while ( client->timeResidual >= 1000 ) { + client->timeResidual -= 1000; + + // regenerate +#ifdef MISSIONPACK + if( bg_itemlist[client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_GUARD ) { + maxHealth = client->ps.stats[STAT_MAX_HEALTH] / 2; + } + else if ( client->ps.powerups[PW_REGEN] ) { + maxHealth = client->ps.stats[STAT_MAX_HEALTH]; + } + else { + maxHealth = 0; + } + if( maxHealth ) { + if ( ent->health < maxHealth ) { + ent->health += 15; + if ( ent->health > maxHealth * 1.1 ) { + ent->health = maxHealth * 1.1; + } + G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); + } else if ( ent->health < maxHealth * 2) { + ent->health += 5; + if ( ent->health > maxHealth * 2 ) { + ent->health = maxHealth * 2; + } + G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); + } +#else + if ( client->ps.powerups[PW_REGEN] ) { + if ( ent->health < client->ps.stats[STAT_MAX_HEALTH]) { + ent->health += 15; + if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 1.1 ) { + ent->health = client->ps.stats[STAT_MAX_HEALTH] * 1.1; + } + G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); + } else if ( ent->health < client->ps.stats[STAT_MAX_HEALTH] * 2) { + ent->health += 5; + if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 2 ) { + ent->health = client->ps.stats[STAT_MAX_HEALTH] * 2; + } + G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); + } +#endif + } else { + // count down health when over max + if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] ) { + ent->health--; + } + } + + // count down armor when over max + if ( client->ps.stats[STAT_ARMOR] > client->ps.stats[STAT_MAX_HEALTH] ) { + client->ps.stats[STAT_ARMOR]--; + } + } +#ifdef MISSIONPACK + if( bg_itemlist[client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_AMMOREGEN ) { + int w, max, inc, t, i; + int weapList[]={WP_MACHINEGUN,WP_SHOTGUN,WP_GRENADE_LAUNCHER,WP_ROCKET_LAUNCHER,WP_LIGHTNING,WP_RAILGUN,WP_PLASMAGUN,WP_BFG,WP_NAILGUN,WP_PROX_LAUNCHER,WP_CHAINGUN}; + int weapCount = sizeof(weapList) / sizeof(int); + // + for (i = 0; i < weapCount; i++) { + w = weapList[i]; + + switch(w) { + case WP_MACHINEGUN: max = 50; inc = 4; t = 1000; break; + case WP_SHOTGUN: max = 10; inc = 1; t = 1500; break; + case WP_GRENADE_LAUNCHER: max = 10; inc = 1; t = 2000; break; + case WP_ROCKET_LAUNCHER: max = 10; inc = 1; t = 1750; break; + case WP_LIGHTNING: max = 50; inc = 5; t = 1500; break; + case WP_RAILGUN: max = 10; inc = 1; t = 1750; break; + case WP_PLASMAGUN: max = 50; inc = 5; t = 1500; break; + case WP_BFG: max = 10; inc = 1; t = 4000; break; + case WP_NAILGUN: max = 10; inc = 1; t = 1250; break; + case WP_PROX_LAUNCHER: max = 5; inc = 1; t = 2000; break; + case WP_CHAINGUN: max = 100; inc = 5; t = 1000; break; + default: max = 0; inc = 0; t = 1000; break; + } + client->ammoTimes[w] += msec; + if ( client->ps.ammo[w] >= max ) { + client->ammoTimes[w] = 0; + } + if ( client->ammoTimes[w] >= t ) { + while ( client->ammoTimes[w] >= t ) + client->ammoTimes[w] -= t; + client->ps.ammo[w] += inc; + if ( client->ps.ammo[w] > max ) { + client->ps.ammo[w] = max; + } + } + } + } +#endif +} + +/* +==================== +ClientIntermissionThink +==================== +*/ +void ClientIntermissionThink( gclient_t *client ) { + client->ps.eFlags &= ~EF_TALK; + client->ps.eFlags &= ~EF_FIRING; + + // the level will exit when everyone wants to or after timeouts + + // swap and latch button actions + client->oldbuttons = client->buttons; + client->buttons = client->pers.cmd.buttons; + if ( client->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) & ( client->oldbuttons ^ client->buttons ) ) { + // this used to be an ^1 but once a player says ready, it should stick + client->readyToExit = 1; + } +} + + +/* +================ +ClientEvents + +Events will be passed on to the clients for presentation, +but any server game effects are handled here +================ +*/ +void ClientEvents( gentity_t *ent, int oldEventSequence ) { + int i, j; + int event; + gclient_t *client; + int damage; + vec3_t dir; + vec3_t origin, angles; +// qboolean fired; + gitem_t *item; + gentity_t *drop; + + client = ent->client; + + if ( oldEventSequence < client->ps.eventSequence - MAX_PS_EVENTS ) { + oldEventSequence = client->ps.eventSequence - MAX_PS_EVENTS; + } + for ( i = oldEventSequence ; i < client->ps.eventSequence ; i++ ) { + event = client->ps.events[ i & (MAX_PS_EVENTS-1) ]; + + switch ( event ) { + case EV_FALL_MEDIUM: + case EV_FALL_FAR: + if ( ent->s.eType != ET_PLAYER ) { + break; // not in the player model + } + if ( g_dmflags.integer & DF_NO_FALLING ) { + break; + } + if ( event == EV_FALL_FAR ) { + damage = 10; + } else { + damage = 5; + } + VectorSet (dir, 0, 0, 1); + ent->pain_debounce_time = level.time + 200; // no normal pain sound + G_Damage (ent, NULL, NULL, NULL, NULL, damage, 0, MOD_FALLING); + break; + + case EV_FIRE_WEAPON: + FireWeapon( ent ); + break; + + case EV_USE_ITEM1: // teleporter + // drop flags in CTF + item = NULL; + j = 0; + + if ( ent->client->ps.powerups[ PW_REDFLAG ] ) { + item = BG_FindItemForPowerup( PW_REDFLAG ); + j = PW_REDFLAG; + } else if ( ent->client->ps.powerups[ PW_BLUEFLAG ] ) { + item = BG_FindItemForPowerup( PW_BLUEFLAG ); + j = PW_BLUEFLAG; + } else if ( ent->client->ps.powerups[ PW_NEUTRALFLAG ] ) { + item = BG_FindItemForPowerup( PW_NEUTRALFLAG ); + j = PW_NEUTRALFLAG; + } + + if ( item ) { + drop = Drop_Item( ent, item, 0 ); + // decide how many seconds it has left + drop->count = ( ent->client->ps.powerups[ j ] - level.time ) / 1000; + if ( drop->count < 1 ) { + drop->count = 1; + } + + ent->client->ps.powerups[ j ] = 0; + } + +#ifdef MISSIONPACK + if ( g_gametype.integer == GT_HARVESTER ) { + if ( ent->client->ps.generic1 > 0 ) { + if ( ent->client->sess.sessionTeam == TEAM_RED ) { + item = BG_FindItem( "Blue Cube" ); + } else { + item = BG_FindItem( "Red Cube" ); + } + if ( item ) { + for ( j = 0; j < ent->client->ps.generic1; j++ ) { + drop = Drop_Item( ent, item, 0 ); + if ( ent->client->sess.sessionTeam == TEAM_RED ) { + drop->spawnflags = TEAM_BLUE; + } else { + drop->spawnflags = TEAM_RED; + } + } + } + ent->client->ps.generic1 = 0; + } + } +#endif + SelectSpawnPoint( ent->client->ps.origin, origin, angles ); + TeleportPlayer( ent, origin, angles ); + break; + + case EV_USE_ITEM2: // medkit + ent->health = ent->client->ps.stats[STAT_MAX_HEALTH] + 25; + + break; + +#ifdef MISSIONPACK + case EV_USE_ITEM3: // kamikaze + // make sure the invulnerability is off + ent->client->invulnerabilityTime = 0; + // start the kamikze + G_StartKamikaze( ent ); + break; + + case EV_USE_ITEM4: // portal + if( ent->client->portalID ) { + DropPortalSource( ent ); + } + else { + DropPortalDestination( ent ); + } + break; + case EV_USE_ITEM5: // invulnerability + ent->client->invulnerabilityTime = level.time + 10000; + break; +#endif + + default: + break; + } + } + +} + +#ifdef MISSIONPACK +/* +============== +StuckInOtherClient +============== +*/ +static int StuckInOtherClient(gentity_t *ent) { + int i; + gentity_t *ent2; + + ent2 = &g_entities[0]; + for ( i = 0; i < MAX_CLIENTS; i++, ent2++ ) { + if ( ent2 == ent ) { + continue; + } + if ( !ent2->inuse ) { + continue; + } + if ( !ent2->client ) { + continue; + } + if ( ent2->health <= 0 ) { + continue; + } + // + if (ent2->r.absmin[0] > ent->r.absmax[0]) + continue; + if (ent2->r.absmin[1] > ent->r.absmax[1]) + continue; + if (ent2->r.absmin[2] > ent->r.absmax[2]) + continue; + if (ent2->r.absmax[0] < ent->r.absmin[0]) + continue; + if (ent2->r.absmax[1] < ent->r.absmin[1]) + continue; + if (ent2->r.absmax[2] < ent->r.absmin[2]) + continue; + return qtrue; + } + return qfalse; +} +#endif + +void BotTestSolid(vec3_t origin); + +/* +============== +SendPendingPredictableEvents +============== +*/ +void SendPendingPredictableEvents( playerState_t *ps ) { + gentity_t *t; + int event, seq; + int extEvent, number; + + // if there are still events pending + if ( ps->entityEventSequence < ps->eventSequence ) { + // create a temporary entity for this event which is sent to everyone + // except the client who generated the event + seq = ps->entityEventSequence & (MAX_PS_EVENTS-1); + event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); + // set external event to zero before calling BG_PlayerStateToEntityState + extEvent = ps->externalEvent; + ps->externalEvent = 0; + // create temporary entity for event + t = G_TempEntity( ps->origin, event ); + number = t->s.number; + BG_PlayerStateToEntityState( ps, &t->s, qtrue ); + t->s.number = number; + t->s.eType = ET_EVENTS + event; + t->s.eFlags |= EF_PLAYER_EVENT; + t->s.otherEntityNum = ps->clientNum; + // send to everyone except the client who generated the event + t->r.svFlags |= SVF_NOTSINGLECLIENT; + t->r.singleClient = ps->clientNum; + // set back external event + ps->externalEvent = extEvent; + } +} + +/* +============== +ClientThink + +This will be called once for each client frame, which will +usually be a couple times for each server frame on fast clients. + +If "g_synchronousClients 1" is set, this will be called exactly +once for each server frame, which makes for smooth demo recording. +============== +*/ +void ClientThink_real( gentity_t *ent ) { + gclient_t *client; + pmove_t pm; + int oldEventSequence; + int msec; + usercmd_t *ucmd; + + client = ent->client; + + // don't think if the client is not yet connected (and thus not yet spawned in) + if (client->pers.connected != CON_CONNECTED) { + return; + } + // mark the time, so the connection sprite can be removed + ucmd = &ent->client->pers.cmd; + + // sanity check the command time to prevent speedup cheating + if ( ucmd->serverTime > level.time + 200 ) { + ucmd->serverTime = level.time + 200; +// G_Printf("serverTime <<<<<\n" ); + } + if ( ucmd->serverTime < level.time - 1000 ) { + ucmd->serverTime = level.time - 1000; +// G_Printf("serverTime >>>>>\n" ); + } + + msec = ucmd->serverTime - client->ps.commandTime; + // following others may result in bad times, but we still want + // to check for follow toggles + if ( msec < 1 && client->sess.spectatorState != SPECTATOR_FOLLOW ) { + return; + } + if ( msec > 200 ) { + msec = 200; + } + + if ( pmove_msec.integer < 8 ) { + trap_Cvar_Set("pmove_msec", "8"); + } + else if (pmove_msec.integer > 33) { + trap_Cvar_Set("pmove_msec", "33"); + } + + if ( pmove_fixed.integer || client->pers.pmoveFixed ) { + ucmd->serverTime = ((ucmd->serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; + //if (ucmd->serverTime - client->ps.commandTime <= 0) + // return; + } + + // + // check for exiting intermission + // + if ( level.intermissiontime ) { + ClientIntermissionThink( client ); + return; + } + + // spectators don't do much + if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { + return; + } + SpectatorThink( ent, ucmd ); + return; + } + + // check for inactivity timer, but never drop the local client of a non-dedicated server + if ( !ClientInactivityTimer( client ) ) { + return; + } + + // clear the rewards if time + if ( level.time > client->rewardTime ) { + client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + } + + if ( client->noclip ) { + client->ps.pm_type = PM_NOCLIP; + } else if ( client->ps.stats[STAT_HEALTH] <= 0 ) { + client->ps.pm_type = PM_DEAD; + } else { + client->ps.pm_type = PM_NORMAL; + } + + client->ps.gravity = g_gravity.value; + + // set speed + client->ps.speed = g_speed.value; + +#ifdef MISSIONPACK + if( bg_itemlist[client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_SCOUT ) { + client->ps.speed *= 1.5; + } + else +#endif + if ( client->ps.powerups[PW_HASTE] ) { + client->ps.speed *= 1.3; + } + + // Let go of the hook if we aren't firing + if ( client->ps.weapon == WP_GRAPPLING_HOOK && + client->hook && !( ucmd->buttons & BUTTON_ATTACK ) ) { + Weapon_HookFree(client->hook); + } + + // set up for pmove + oldEventSequence = client->ps.eventSequence; + + memset (&pm, 0, sizeof(pm)); + + // check for the hit-scan gauntlet, don't let the action + // go through as an attack unless it actually hits something + if ( client->ps.weapon == WP_GAUNTLET && !( ucmd->buttons & BUTTON_TALK ) && + ( ucmd->buttons & BUTTON_ATTACK ) && client->ps.weaponTime <= 0 ) { + pm.gauntletHit = CheckGauntletAttack( ent ); + } + + if ( ent->flags & FL_FORCE_GESTURE ) { + ent->flags &= ~FL_FORCE_GESTURE; + ent->client->pers.cmd.buttons |= BUTTON_GESTURE; + } + +#ifdef MISSIONPACK + // check for invulnerability expansion before doing the Pmove + if (client->ps.powerups[PW_INVULNERABILITY] ) { + if ( !(client->ps.pm_flags & PMF_INVULEXPAND) ) { + vec3_t mins = { -42, -42, -42 }; + vec3_t maxs = { 42, 42, 42 }; + vec3_t oldmins, oldmaxs; + + VectorCopy (ent->r.mins, oldmins); + VectorCopy (ent->r.maxs, oldmaxs); + // expand + VectorCopy (mins, ent->r.mins); + VectorCopy (maxs, ent->r.maxs); + trap_LinkEntity(ent); + // check if this would get anyone stuck in this player + if ( !StuckInOtherClient(ent) ) { + // set flag so the expanded size will be set in PM_CheckDuck + client->ps.pm_flags |= PMF_INVULEXPAND; + } + // set back + VectorCopy (oldmins, ent->r.mins); + VectorCopy (oldmaxs, ent->r.maxs); + trap_LinkEntity(ent); + } + } +#endif + + pm.ps = &client->ps; + pm.cmd = *ucmd; + if ( pm.ps->pm_type == PM_DEAD ) { + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; + } + else if ( ent->r.svFlags & SVF_BOT ) { + pm.tracemask = MASK_PLAYERSOLID | CONTENTS_BOTCLIP; + } + else { + pm.tracemask = MASK_PLAYERSOLID; + } + pm.trace = trap_Trace; + pm.pointcontents = trap_PointContents; + pm.debugLevel = g_debugMove.integer; + pm.noFootsteps = ( g_dmflags.integer & DF_NO_FOOTSTEPS ) > 0; + + pm.pmove_fixed = pmove_fixed.integer | client->pers.pmoveFixed; + pm.pmove_msec = pmove_msec.integer; + + VectorCopy( client->ps.origin, client->oldOrigin ); + +#ifdef MISSIONPACK + if (level.intermissionQueued != 0 && g_singlePlayer.integer) { + if ( level.time - level.intermissionQueued >= 1000 ) { + pm.cmd.buttons = 0; + pm.cmd.forwardmove = 0; + pm.cmd.rightmove = 0; + pm.cmd.upmove = 0; + if ( level.time - level.intermissionQueued >= 2000 && level.time - level.intermissionQueued <= 2500 ) { + trap_SendConsoleCommand( EXEC_APPEND, "centerview\n"); + } + ent->client->ps.pm_type = PM_SPINTERMISSION; + } + } + Pmove (&pm); +#else + Pmove (&pm); +#endif + + // save results of pmove + if ( ent->client->ps.eventSequence != oldEventSequence ) { + ent->eventTime = level.time; + } + if (g_smoothClients.integer) { + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + } + else { + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + } + SendPendingPredictableEvents( &ent->client->ps ); + + if ( !( ent->client->ps.eFlags & EF_FIRING ) ) { + client->fireHeld = qfalse; // for grapple + } + + // use the snapped origin for linking so it matches client predicted versions + VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); + + VectorCopy (pm.mins, ent->r.mins); + VectorCopy (pm.maxs, ent->r.maxs); + + ent->waterlevel = pm.waterlevel; + ent->watertype = pm.watertype; + + // execute client events + ClientEvents( ent, oldEventSequence ); + + // link entity now, after any personal teleporters have been used + trap_LinkEntity (ent); + if ( !ent->client->noclip ) { + G_TouchTriggers( ent ); + } + + // NOTE: now copy the exact origin over otherwise clients can be snapped into solid + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + + //test for solid areas in the AAS file + BotTestAAS(ent->r.currentOrigin); + + // touch other objects + ClientImpacts( ent, &pm ); + + // save results of triggers and client events + if (ent->client->ps.eventSequence != oldEventSequence) { + ent->eventTime = level.time; + } + + // swap and latch button actions + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + client->latched_buttons |= client->buttons & ~client->oldbuttons; + + // check for respawning + if ( client->ps.stats[STAT_HEALTH] <= 0 ) { + // wait for the attack button to be pressed + if ( level.time > client->respawnTime ) { + // forcerespawn is to prevent users from waiting out powerups + if ( g_forcerespawn.integer > 0 && + ( level.time - client->respawnTime ) > g_forcerespawn.integer * 1000 ) { + respawn( ent ); + return; + } + + // pressing attack or use is the normal respawn method + if ( ucmd->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) ) { + respawn( ent ); + } + } + return; + } + + // perform once-a-second actions + ClientTimerActions( ent, msec ); +} + +/* +================== +ClientThink + +A new command has arrived from the client +================== +*/ +void ClientThink( int clientNum ) { + gentity_t *ent; + + ent = g_entities + clientNum; + trap_GetUsercmd( clientNum, &ent->client->pers.cmd ); + + // mark the time we got info, so we can display the + // phone jack if they don't get any for a while + ent->client->lastCmdTime = level.time; + + // demo playback: don't run ClientThink_real -- cgame owns + // the camera movement, G_RunFrame handles buttons and PVS origin + if ( g_svDemoPlaying.integer ) { + return; + } + + if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { + ClientThink_real( ent ); + } +} + + +void G_RunClient( gentity_t *ent ) { + if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { + return; + } + ent->client->pers.cmd.serverTime = level.time; + ClientThink_real( ent ); +} + + +/* +================== +SpectatorClientEndFrame + +================== +*/ +void SpectatorClientEndFrame( gentity_t *ent ) { + gclient_t *cl; + + // if we are doing a chase cam or a remote view, grab the latest info + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + int clientNum, flags; + + clientNum = ent->client->sess.spectatorClient; + + // team follow1 and team follow2 go to whatever clients are playing + if ( clientNum == -1 ) { + clientNum = level.follow1; + } else if ( clientNum == -2 ) { + clientNum = level.follow2; + } + if ( clientNum >= 0 ) { + cl = &level.clients[ clientNum ]; + if ( cl->pers.connected == CON_CONNECTED && cl->sess.sessionTeam != TEAM_SPECTATOR ) { + flags = (cl->ps.eFlags & ~(EF_VOTED | EF_TEAMVOTED)) | (ent->client->ps.eFlags & (EF_VOTED | EF_TEAMVOTED)); + ent->client->ps = cl->ps; + ent->client->ps.pm_flags |= PMF_FOLLOW; + ent->client->ps.eFlags = flags; + return; + } else { + // drop them to free spectators unless they are dedicated camera followers + if ( ent->client->sess.spectatorClient >= 0 ) { + ent->client->sess.spectatorState = SPECTATOR_FREE; + ClientBegin( ent->client - level.clients ); + } + } + } + } + + if ( ent->client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { + ent->client->ps.pm_flags |= PMF_SCOREBOARD; + } else { + ent->client->ps.pm_flags &= ~PMF_SCOREBOARD; + } +} + +/* +============== +ClientEndFrame + +Called at the end of each server frame for each connected client +A fast client will have multiple ClientThink for each ClientEdFrame, +while a slow client may have multiple ClientEndFrame between ClientThink. +============== +*/ +void ClientEndFrame( gentity_t *ent ) { + int i; + clientPersistant_t *pers; + + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + SpectatorClientEndFrame( ent ); + return; + } + + pers = &ent->client->pers; + + // turn off any expired powerups + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + if ( ent->client->ps.powerups[ i ] < level.time ) { + ent->client->ps.powerups[ i ] = 0; + } + } + +#ifdef MISSIONPACK + // set powerup for player animation + if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_GUARD ) { + ent->client->ps.powerups[PW_GUARD] = level.time; + } + if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_SCOUT ) { + ent->client->ps.powerups[PW_SCOUT] = level.time; + } + if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_DOUBLER ) { + ent->client->ps.powerups[PW_DOUBLER] = level.time; + } + if( bg_itemlist[ent->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_AMMOREGEN ) { + ent->client->ps.powerups[PW_AMMOREGEN] = level.time; + } + if ( ent->client->invulnerabilityTime > level.time ) { + ent->client->ps.powerups[PW_INVULNERABILITY] = level.time; + } +#endif + + // save network bandwidth +#if 0 + if ( !g_synchronousClients->integer && ent->client->ps.pm_type == PM_NORMAL ) { + // FIXME: this must change eventually for non-sync demo recording + VectorClear( ent->client->ps.viewangles ); + } +#endif + + // + // If the end of unit layout is displayed, don't give + // the player any normal movement attributes + // + if ( level.intermissiontime ) { + return; + } + + // burn from lava, etc + P_WorldEffects (ent); + + // apply all the damage taken this frame + P_DamageFeedback (ent); + + // add the EF_CONNECTION flag if we haven't gotten commands recently + if ( level.time - ent->client->lastCmdTime > 1000 ) { + ent->s.eFlags |= EF_CONNECTION; + } else { + ent->s.eFlags &= ~EF_CONNECTION; + } + + ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... + + G_SetClientSound (ent); + + // set the latest infor + if (g_smoothClients.integer) { + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + } + else { + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + } + SendPendingPredictableEvents( &ent->client->ps ); + + // set the bit for the reachability area the client is currently in +// i = trap_AAS_PointReachabilityAreaIndex( ent->client->ps.origin ); +// ent->client->areabits[i >> 3] |= 1 << (i & 7); +} + + diff --git a/code/game/g_main.c b/code/game/g_main.c index 117f6d7..b6303f1 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -1,1926 +1,1926 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ -// - -#include "g_local.h" - -level_locals_t level; - -typedef struct { - vmCvar_t *vmCvar; - char *cvarName; - char *defaultString; - int cvarFlags; - int modificationCount; // for tracking changes - qboolean trackChange; // track this variable, and announce if changed - qboolean teamShader; // track and if changed, update shader state -} cvarTable_t; - -gentity_t g_entities[MAX_GENTITIES]; -gclient_t g_clients[MAX_CLIENTS]; - -vmCvar_t g_gametype; -vmCvar_t g_svDemoPlaying; -vmCvar_t g_dmflags; -vmCvar_t g_fraglimit; -vmCvar_t g_timelimit; -vmCvar_t g_capturelimit; -vmCvar_t g_friendlyFire; -vmCvar_t g_password; -vmCvar_t g_needpass; -vmCvar_t g_maxclients; -vmCvar_t g_maxGameClients; -vmCvar_t g_dedicated; -vmCvar_t g_speed; -vmCvar_t g_gravity; -vmCvar_t g_cheats; -vmCvar_t g_knockback; -vmCvar_t g_quadfactor; -vmCvar_t g_forcerespawn; -vmCvar_t g_inactivity; -vmCvar_t g_debugMove; -vmCvar_t g_debugDamage; -vmCvar_t g_debugAlloc; -vmCvar_t g_weaponRespawn; -vmCvar_t g_weaponTeamRespawn; -vmCvar_t g_motd; -vmCvar_t g_synchronousClients; -vmCvar_t g_warmup; -vmCvar_t g_doWarmup; -vmCvar_t g_restarted; -vmCvar_t g_log; -vmCvar_t g_logSync; -vmCvar_t g_blood; -vmCvar_t g_podiumDist; -vmCvar_t g_podiumDrop; -vmCvar_t g_allowVote; -vmCvar_t g_teamAutoJoin; -vmCvar_t g_teamForceBalance; -vmCvar_t g_banIPs; -vmCvar_t g_filterBan; -vmCvar_t g_smoothClients; -vmCvar_t pmove_fixed; -vmCvar_t pmove_msec; -vmCvar_t g_rankings; -vmCvar_t g_listEntity; -#ifdef MISSIONPACK -vmCvar_t g_obeliskHealth; -vmCvar_t g_obeliskRegenPeriod; -vmCvar_t g_obeliskRegenAmount; -vmCvar_t g_obeliskRespawnDelay; -vmCvar_t g_cubeTimeout; -vmCvar_t g_redteam; -vmCvar_t g_blueteam; -vmCvar_t g_singlePlayer; -vmCvar_t g_enableDust; -vmCvar_t g_enableBreath; -vmCvar_t g_proxMineTimeout; -#endif - -// bk001129 - made static to avoid aliasing -static cvarTable_t gameCvarTable[] = { - // don't override the cheat state set by the system - { &g_cheats, "sv_cheats", "", 0, 0, qfalse }, - - // noset vars - { NULL, "gamename", GAMEVERSION , CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, - { NULL, "gamedate", __DATE__ , CVAR_ROM, 0, qfalse }, - { &g_restarted, "g_restarted", "0", CVAR_ROM, 0, qfalse }, - { NULL, "sv_mapname", "", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, - - // latched vars - { &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_USERINFO | CVAR_LATCH, 0, qfalse }, - { &g_svDemoPlaying, "sv_demoplaying", "0", CVAR_ROM, 0, qfalse }, - - { &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, - { &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, - - // change anytime vars - { &g_dmflags, "dmflags", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, - { &g_fraglimit, "fraglimit", "20", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, - { &g_timelimit, "timelimit", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, - { &g_capturelimit, "capturelimit", "8", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, - - { &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, qfalse }, - - { &g_friendlyFire, "g_friendlyFire", "0", CVAR_ARCHIVE, 0, qtrue }, - - { &g_teamAutoJoin, "g_teamAutoJoin", "0", CVAR_ARCHIVE }, - { &g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE }, - - { &g_warmup, "g_warmup", "20", CVAR_ARCHIVE, 0, qtrue }, - { &g_doWarmup, "g_doWarmup", "0", 0, 0, qtrue }, - { &g_log, "g_log", "games.log", CVAR_ARCHIVE, 0, qfalse }, - { &g_logSync, "g_logSync", "0", CVAR_ARCHIVE, 0, qfalse }, - - { &g_password, "g_password", "", CVAR_USERINFO, 0, qfalse }, - - { &g_banIPs, "g_banIPs", "", CVAR_ARCHIVE, 0, qfalse }, - { &g_filterBan, "g_filterBan", "1", CVAR_ARCHIVE, 0, qfalse }, - - { &g_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, - - { &g_dedicated, "dedicated", "0", 0, 0, qfalse }, - - { &g_speed, "g_speed", "320", 0, 0, qtrue }, - { &g_gravity, "g_gravity", "800", 0, 0, qtrue }, - { &g_knockback, "g_knockback", "1000", 0, 0, qtrue }, - { &g_quadfactor, "g_quadfactor", "3", 0, 0, qtrue }, - { &g_weaponRespawn, "g_weaponrespawn", "5", 0, 0, qtrue }, - { &g_weaponTeamRespawn, "g_weaponTeamRespawn", "30", 0, 0, qtrue }, - { &g_forcerespawn, "g_forcerespawn", "20", 0, 0, qtrue }, - { &g_inactivity, "g_inactivity", "0", 0, 0, qtrue }, - { &g_debugMove, "g_debugMove", "0", 0, 0, qfalse }, - { &g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse }, - { &g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse }, - { &g_motd, "g_motd", "", 0, 0, qfalse }, - { &g_blood, "com_blood", "1", 0, 0, qfalse }, - - { &g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse }, - { &g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse }, - - { &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse }, - { &g_listEntity, "g_listEntity", "0", 0, 0, qfalse }, - -#ifdef MISSIONPACK - { &g_obeliskHealth, "g_obeliskHealth", "2500", 0, 0, qfalse }, - { &g_obeliskRegenPeriod, "g_obeliskRegenPeriod", "1", 0, 0, qfalse }, - { &g_obeliskRegenAmount, "g_obeliskRegenAmount", "15", 0, 0, qfalse }, - { &g_obeliskRespawnDelay, "g_obeliskRespawnDelay", "10", CVAR_SERVERINFO, 0, qfalse }, - - { &g_cubeTimeout, "g_cubeTimeout", "30", 0, 0, qfalse }, - { &g_redteam, "g_redteam", "Stroggs", CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_USERINFO , 0, qtrue, qtrue }, - { &g_blueteam, "g_blueteam", "Pagans", CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_USERINFO , 0, qtrue, qtrue }, - { &g_singlePlayer, "ui_singlePlayerActive", "", 0, 0, qfalse, qfalse }, - - { &g_enableDust, "g_enableDust", "0", CVAR_SERVERINFO, 0, qtrue, qfalse }, - { &g_enableBreath, "g_enableBreath", "0", CVAR_SERVERINFO, 0, qtrue, qfalse }, - { &g_proxMineTimeout, "g_proxMineTimeout", "20000", 0, 0, qfalse }, -#endif - { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, - { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, - { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, - - { &g_rankings, "g_rankings", "0", 0, 0, qfalse} - -}; - -// bk001129 - made static to avoid aliasing -static int gameCvarTableSize = sizeof( gameCvarTable ) / sizeof( gameCvarTable[0] ); - - -void G_InitGame( int levelTime, int randomSeed, int restart ); -void G_RunFrame( int levelTime ); -void G_ShutdownGame( int restart ); -void CheckExitRules( void ); - - -/* -================ -vmMain - -This is the only way control passes into the module. -This must be the very first function compiled into the .q3vm file -================ -*/ -int vmMain( int command, int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10, int arg11 ) { - switch ( command ) { - case GAME_INIT: - G_InitGame( arg0, arg1, arg2 ); - return 0; - case GAME_SHUTDOWN: - G_ShutdownGame( arg0 ); - return 0; - case GAME_CLIENT_CONNECT: - return (int)ClientConnect( arg0, arg1, arg2 ); - case GAME_CLIENT_THINK: - ClientThink( arg0 ); - return 0; - case GAME_CLIENT_USERINFO_CHANGED: - ClientUserinfoChanged( arg0 ); - return 0; - case GAME_CLIENT_DISCONNECT: - ClientDisconnect( arg0 ); - return 0; - case GAME_CLIENT_BEGIN: - ClientBegin( arg0 ); - return 0; - case GAME_CLIENT_COMMAND: - ClientCommand( arg0 ); - return 0; - case GAME_RUN_FRAME: - G_RunFrame( arg0 ); - return 0; - case GAME_CONSOLE_COMMAND: - return ConsoleCommand(); - case BOTAI_START_FRAME: - return BotAIStartFrame( arg0 ); - } - - return -1; -} - - -void QDECL G_Printf( const char *fmt, ... ) { - va_list argptr; - char text[1024]; - - va_start (argptr, fmt); - vsprintf (text, fmt, argptr); - va_end (argptr); - - trap_Printf( text ); -} - -void QDECL G_Error( const char *fmt, ... ) { - va_list argptr; - char text[1024]; - - va_start (argptr, fmt); - vsprintf (text, fmt, argptr); - va_end (argptr); - - trap_Error( text ); -} - -/* -================ -G_FindTeams - -Chain together all entities with a matching team field. -Entity teams are used for item groups and multi-entity mover groups. - -All but the first will have the FL_TEAMSLAVE flag set and teammaster field set -All but the last will have the teamchain field set to the next one -================ -*/ -void G_FindTeams( void ) { - gentity_t *e, *e2; - int i, j; - int c, c2; - - c = 0; - c2 = 0; - for ( i=1, e=g_entities+i ; i < level.num_entities ; i++,e++ ){ - if (!e->inuse) - continue; - if (!e->team) - continue; - if (e->flags & FL_TEAMSLAVE) - continue; - e->teammaster = e; - c++; - c2++; - for (j=i+1, e2=e+1 ; j < level.num_entities ; j++,e2++) - { - if (!e2->inuse) - continue; - if (!e2->team) - continue; - if (e2->flags & FL_TEAMSLAVE) - continue; - if (!strcmp(e->team, e2->team)) - { - c2++; - e2->teamchain = e->teamchain; - e->teamchain = e2; - e2->teammaster = e; - e2->flags |= FL_TEAMSLAVE; - - // make sure that targets only point at the master - if ( e2->targetname ) { - e->targetname = e2->targetname; - e2->targetname = NULL; - } - } - } - } - - G_Printf ("%i teams with %i entities\n", c, c2); -} - -void G_RemapTeamShaders() { -#ifdef MISSIONPACK - char string[1024]; - float f = level.time * 0.001; - Com_sprintf( string, sizeof(string), "team_icon/%s_red", g_redteam.string ); - AddRemap("textures/ctf2/redteam01", string, f); - AddRemap("textures/ctf2/redteam02", string, f); - Com_sprintf( string, sizeof(string), "team_icon/%s_blue", g_blueteam.string ); - AddRemap("textures/ctf2/blueteam01", string, f); - AddRemap("textures/ctf2/blueteam02", string, f); - trap_SetConfigstring(CS_SHADERSTATE, BuildShaderStateConfig()); -#endif -} - - -/* -================= -G_RegisterCvars -================= -*/ -void G_RegisterCvars( void ) { - int i; - cvarTable_t *cv; - qboolean remapped = qfalse; - - for ( i = 0, cv = gameCvarTable ; i < gameCvarTableSize ; i++, cv++ ) { - trap_Cvar_Register( cv->vmCvar, cv->cvarName, - cv->defaultString, cv->cvarFlags ); - if ( cv->vmCvar ) - cv->modificationCount = cv->vmCvar->modificationCount; - - if (cv->teamShader) { - remapped = qtrue; - } - } - - if (remapped) { - G_RemapTeamShaders(); - } - - // check some things - if ( g_gametype.integer < 0 || g_gametype.integer >= GT_MAX_GAME_TYPE ) { - G_Printf( "g_gametype %i is out of range, defaulting to 0\n", g_gametype.integer ); - trap_Cvar_Set( "g_gametype", "0" ); - } - - level.warmupModificationCount = g_warmup.modificationCount; -} - -/* -================= -G_UpdateCvars -================= -*/ -void G_UpdateCvars( void ) { - int i; - cvarTable_t *cv; - qboolean remapped = qfalse; - - for ( i = 0, cv = gameCvarTable ; i < gameCvarTableSize ; i++, cv++ ) { - if ( cv->vmCvar ) { - trap_Cvar_Update( cv->vmCvar ); - - if ( cv->modificationCount != cv->vmCvar->modificationCount ) { - cv->modificationCount = cv->vmCvar->modificationCount; - - if ( cv->trackChange ) { - trap_SendServerCommand( -1, va("print \"Server: %s changed to %s\n\"", - cv->cvarName, cv->vmCvar->string ) ); - } - - if (cv->teamShader) { - remapped = qtrue; - } - } - } - } - - if (remapped) { - G_RemapTeamShaders(); - } -} - -/* -============ -G_InitGame - -============ -*/ -void G_InitGame( int levelTime, int randomSeed, int restart ) { - int i; - - G_Printf ("------- Game Initialization -------\n"); - G_Printf ("gamename: %s\n", GAMEVERSION); - G_Printf ("gamedate: %s\n", __DATE__); - - srand( randomSeed ); - - G_RegisterCvars(); - - // signal server-side demo mode to cgame via configstring - if ( g_svDemoPlaying.integer ) { - trap_SetConfigstring( CS_SVDEMO, "1" ); - } - - G_ProcessIPBans(); - - G_InitMemory(); - - // set some level globals - memset( &level, 0, sizeof( level ) ); - level.time = levelTime; - level.startTime = levelTime; - - level.snd_fry = G_SoundIndex("sound/player/fry.wav"); // FIXME standing in lava / slime - - if ( g_gametype.integer != GT_SINGLE_PLAYER && g_log.string[0] ) { - if ( g_logSync.integer ) { - trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND_SYNC ); - } else { - trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND ); - } - if ( !level.logFile ) { - G_Printf( "WARNING: Couldn't open logfile: %s\n", g_log.string ); - } else { - char serverinfo[MAX_INFO_STRING]; - - trap_GetServerinfo( serverinfo, sizeof( serverinfo ) ); - - G_LogPrintf("------------------------------------------------------------\n" ); - G_LogPrintf("InitGame: %s\n", serverinfo ); - } - } else { - G_Printf( "Not logging to disk.\n" ); - } - - G_InitWorldSession(); - - // initialize all entities for this game - memset( g_entities, 0, MAX_GENTITIES * sizeof(g_entities[0]) ); - level.gentities = g_entities; - - // initialize all clients for this game - level.maxclients = g_maxclients.integer; - memset( g_clients, 0, MAX_CLIENTS * sizeof(g_clients[0]) ); - level.clients = g_clients; - - // set client fields on player ents - for ( i=0 ; i= GT_TEAM ) { - G_CheckTeamItems(); - } - - SaveRegisteredItems(); - - G_Printf ("-----------------------------------\n"); - - if( g_gametype.integer == GT_SINGLE_PLAYER || trap_Cvar_VariableIntegerValue( "com_buildScript" ) ) { - G_ModelIndex( SP_PODIUM_MODEL ); - G_SoundIndex( "sound/player/gurp1.wav" ); - G_SoundIndex( "sound/player/gurp2.wav" ); - } - - if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) ) { - BotAISetup( restart ); - BotAILoadMap( restart ); - G_InitBots( restart ); - } - - G_RemapTeamShaders(); - -} - - - -/* -================= -G_ShutdownGame -================= -*/ -void G_ShutdownGame( int restart ) { - G_Printf ("==== ShutdownGame ====\n"); - - if ( level.logFile ) { - G_LogPrintf("ShutdownGame:\n" ); - G_LogPrintf("------------------------------------------------------------\n" ); - trap_FS_FCloseFile( level.logFile ); - } - - // write all the client session data so we can get it back - G_WriteSessionData(); - - if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) ) { - BotAIShutdown( restart ); - } -} - - - -//=================================================================== - -#ifndef GAME_HARD_LINKED -// this is only here so the functions in q_shared.c and bg_*.c can link - -void QDECL Com_Error ( int level, const char *error, ... ) { - va_list argptr; - char text[1024]; - - va_start (argptr, error); - vsprintf (text, error, argptr); - va_end (argptr); - - G_Error( "%s", text); -} - -void QDECL Com_Printf( const char *msg, ... ) { - va_list argptr; - char text[1024]; - - va_start (argptr, msg); - vsprintf (text, msg, argptr); - va_end (argptr); - - G_Printf ("%s", text); -} - -#endif - -/* -======================================================================== - -PLAYER COUNTING / SCORE SORTING - -======================================================================== -*/ - -/* -============= -AddTournamentPlayer - -If there are less than two tournament players, put a -spectator in the game and restart -============= -*/ -void AddTournamentPlayer( void ) { - int i; - gclient_t *client; - gclient_t *nextInLine; - - if ( level.numPlayingClients >= 2 ) { - return; - } - - // never change during intermission - if ( level.intermissiontime ) { - return; - } - - nextInLine = NULL; - - for ( i = 0 ; i < level.maxclients ; i++ ) { - client = &level.clients[i]; - if ( client->pers.connected != CON_CONNECTED ) { - continue; - } - if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { - continue; - } - // never select the dedicated follow or scoreboard clients - if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD || - client->sess.spectatorClient < 0 ) { - continue; - } - - if ( !nextInLine || client->sess.spectatorTime < nextInLine->sess.spectatorTime ) { - nextInLine = client; - } - } - - if ( !nextInLine ) { - return; - } - - level.warmupTime = -1; - - // set them to free-for-all team - SetTeam( &g_entities[ nextInLine - level.clients ], "f" ); -} - -/* -======================= -RemoveTournamentLoser - -Make the loser a spectator at the back of the line -======================= -*/ -void RemoveTournamentLoser( void ) { - int clientNum; - - if ( level.numPlayingClients != 2 ) { - return; - } - - clientNum = level.sortedClients[1]; - - if ( level.clients[ clientNum ].pers.connected != CON_CONNECTED ) { - return; - } - - // make them a spectator - SetTeam( &g_entities[ clientNum ], "s" ); -} - -/* -======================= -RemoveTournamentWinner -======================= -*/ -void RemoveTournamentWinner( void ) { - int clientNum; - - if ( level.numPlayingClients != 2 ) { - return; - } - - clientNum = level.sortedClients[0]; - - if ( level.clients[ clientNum ].pers.connected != CON_CONNECTED ) { - return; - } - - // make them a spectator - SetTeam( &g_entities[ clientNum ], "s" ); -} - -/* -======================= -AdjustTournamentScores -======================= -*/ -void AdjustTournamentScores( void ) { - int clientNum; - - clientNum = level.sortedClients[0]; - if ( level.clients[ clientNum ].pers.connected == CON_CONNECTED ) { - level.clients[ clientNum ].sess.wins++; - ClientUserinfoChanged( clientNum ); - } - - clientNum = level.sortedClients[1]; - if ( level.clients[ clientNum ].pers.connected == CON_CONNECTED ) { - level.clients[ clientNum ].sess.losses++; - ClientUserinfoChanged( clientNum ); - } - -} - -/* -============= -SortRanks - -============= -*/ -int QDECL SortRanks( const void *a, const void *b ) { - gclient_t *ca, *cb; - - ca = &level.clients[*(int *)a]; - cb = &level.clients[*(int *)b]; - - // sort special clients last - if ( ca->sess.spectatorState == SPECTATOR_SCOREBOARD || ca->sess.spectatorClient < 0 ) { - return 1; - } - if ( cb->sess.spectatorState == SPECTATOR_SCOREBOARD || cb->sess.spectatorClient < 0 ) { - return -1; - } - - // then connecting clients - if ( ca->pers.connected == CON_CONNECTING ) { - return 1; - } - if ( cb->pers.connected == CON_CONNECTING ) { - return -1; - } - - - // then spectators - if ( ca->sess.sessionTeam == TEAM_SPECTATOR && cb->sess.sessionTeam == TEAM_SPECTATOR ) { - if ( ca->sess.spectatorTime < cb->sess.spectatorTime ) { - return -1; - } - if ( ca->sess.spectatorTime > cb->sess.spectatorTime ) { - return 1; - } - return 0; - } - if ( ca->sess.sessionTeam == TEAM_SPECTATOR ) { - return 1; - } - if ( cb->sess.sessionTeam == TEAM_SPECTATOR ) { - return -1; - } - - // then sort by score - if ( ca->ps.persistant[PERS_SCORE] - > cb->ps.persistant[PERS_SCORE] ) { - return -1; - } - if ( ca->ps.persistant[PERS_SCORE] - < cb->ps.persistant[PERS_SCORE] ) { - return 1; - } - return 0; -} - -/* -============ -CalculateRanks - -Recalculates the score ranks of all players -This will be called on every client connect, begin, disconnect, death, -and team change. -============ -*/ -void CalculateRanks( void ) { - int i; - 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; - level.follow2 = -1; - level.numConnectedClients = 0; - level.numNonSpectatorClients = 0; - level.numPlayingClients = 0; - level.numVotingClients = 0; // don't count bots - for ( i = 0; i < TEAM_NUM_TEAMS; i++ ) { - level.numteamVotingClients[i] = 0; - } - for ( i = 0 ; i < level.maxclients ; i++ ) { - if ( level.clients[i].pers.connected != CON_DISCONNECTED ) { - level.sortedClients[level.numConnectedClients] = i; - level.numConnectedClients++; - - if ( level.clients[i].sess.sessionTeam != TEAM_SPECTATOR ) { - level.numNonSpectatorClients++; - - // decide if this should be auto-followed - if ( level.clients[i].pers.connected == CON_CONNECTED ) { - level.numPlayingClients++; - if ( !(g_entities[i].r.svFlags & SVF_BOT) ) { - level.numVotingClients++; - if ( level.clients[i].sess.sessionTeam == TEAM_RED ) - level.numteamVotingClients[0]++; - else if ( level.clients[i].sess.sessionTeam == TEAM_BLUE ) - level.numteamVotingClients[1]++; - } - if ( level.follow1 == -1 ) { - level.follow1 = i; - } else if ( level.follow2 == -1 ) { - level.follow2 = i; - } - } - } - } - } - - qsort( level.sortedClients, level.numConnectedClients, - sizeof(level.sortedClients[0]), SortRanks ); - - // set the rank value for all clients that are connected and not spectators - if ( g_gametype.integer >= GT_TEAM ) { - // in team games, rank is just the order of the teams, 0=red, 1=blue, 2=tied - for ( i = 0; i < level.numConnectedClients; i++ ) { - cl = &level.clients[ level.sortedClients[i] ]; - if ( level.teamScores[TEAM_RED] == level.teamScores[TEAM_BLUE] ) { - cl->ps.persistant[PERS_RANK] = 2; - } else if ( level.teamScores[TEAM_RED] > level.teamScores[TEAM_BLUE] ) { - cl->ps.persistant[PERS_RANK] = 0; - } else { - cl->ps.persistant[PERS_RANK] = 1; - } - } - } else { - rank = -1; - score = 0; - for ( i = 0; i < level.numPlayingClients; i++ ) { - cl = &level.clients[ level.sortedClients[i] ]; - newScore = cl->ps.persistant[PERS_SCORE]; - if ( i == 0 || newScore != score ) { - rank = i; - // assume we aren't tied until the next client is checked - level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank; - } else { - // we are tied with the previous client - level.clients[ level.sortedClients[i-1] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; - level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; - } - score = newScore; - if ( g_gametype.integer == GT_SINGLE_PLAYER && level.numPlayingClients == 1 ) { - level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; - } - } - } - - // set the CS_SCORES1/2 configstrings, which will be visible to everyone - if ( g_gametype.integer >= GT_TEAM ) { - trap_SetConfigstring( CS_SCORES1, va("%i", level.teamScores[TEAM_RED] ) ); - trap_SetConfigstring( CS_SCORES2, va("%i", level.teamScores[TEAM_BLUE] ) ); - } else { - if ( level.numConnectedClients == 0 ) { - trap_SetConfigstring( CS_SCORES1, va("%i", SCORE_NOT_PRESENT) ); - trap_SetConfigstring( CS_SCORES2, va("%i", SCORE_NOT_PRESENT) ); - } else if ( level.numConnectedClients == 1 ) { - trap_SetConfigstring( CS_SCORES1, va("%i", level.clients[ level.sortedClients[0] ].ps.persistant[PERS_SCORE] ) ); - trap_SetConfigstring( CS_SCORES2, va("%i", SCORE_NOT_PRESENT) ); - } else { - trap_SetConfigstring( CS_SCORES1, va("%i", level.clients[ level.sortedClients[0] ].ps.persistant[PERS_SCORE] ) ); - trap_SetConfigstring( CS_SCORES2, va("%i", level.clients[ level.sortedClients[1] ].ps.persistant[PERS_SCORE] ) ); - } - } - - // see if it is time to end the level - CheckExitRules(); - - // if we are at the intermission, send the new info to everyone - if ( level.intermissiontime ) { - SendScoreboardMessageToAllClients(); - } -} - - -/* -======================================================================== - -MAP CHANGING - -======================================================================== -*/ - -/* -======================== -SendScoreboardMessageToAllClients - -Do this at BeginIntermission time and whenever ranks are recalculated -due to enters/exits/forced team changes -======================== -*/ -void SendScoreboardMessageToAllClients( void ) { - int i; - - for ( i = 0 ; i < level.maxclients ; i++ ) { - if ( level.clients[ i ].pers.connected == CON_CONNECTED ) { - DeathmatchScoreboardMessage( g_entities + i ); - } - } -} - -/* -======================== -MoveClientToIntermission - -When the intermission starts, this will be called for all players. -If a new client connects, this will be called after the spawn function. -======================== -*/ -void MoveClientToIntermission( gentity_t *ent ) { - // take out of follow mode if needed - if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { - StopFollowing( ent ); - } - - - // move to the spot - VectorCopy( level.intermission_origin, ent->s.origin ); - VectorCopy( level.intermission_origin, ent->client->ps.origin ); - VectorCopy (level.intermission_angle, ent->client->ps.viewangles); - ent->client->ps.pm_type = PM_INTERMISSION; - - // clean up powerup info - memset( ent->client->ps.powerups, 0, sizeof(ent->client->ps.powerups) ); - - ent->client->ps.eFlags = 0; - ent->s.eFlags = 0; - ent->s.eType = ET_GENERAL; - ent->s.modelindex = 0; - ent->s.loopSound = 0; - ent->s.event = 0; - ent->r.contents = 0; -} - -/* -================== -FindIntermissionPoint - -This is also used for spectator spawns -================== -*/ -void FindIntermissionPoint( void ) { - gentity_t *ent, *target; - vec3_t dir; - - // find the intermission spot - ent = G_Find (NULL, FOFS(classname), "info_player_intermission"); - if ( !ent ) { // the map creator forgot to put in an intermission point... - SelectSpawnPoint ( vec3_origin, level.intermission_origin, level.intermission_angle ); - } else { - VectorCopy (ent->s.origin, level.intermission_origin); - VectorCopy (ent->s.angles, level.intermission_angle); - // if it has a target, look towards it - if ( ent->target ) { - target = G_PickTarget( ent->target ); - if ( target ) { - VectorSubtract( target->s.origin, level.intermission_origin, dir ); - vectoangles( dir, level.intermission_angle ); - } - } - } - -} - -/* -================== -BeginIntermission -================== -*/ -void BeginIntermission( void ) { - int i; - gentity_t *client; - - if ( level.intermissiontime ) { - return; // already active - } - - // if in tournement mode, change the wins / losses - if ( g_gametype.integer == GT_TOURNAMENT ) { - AdjustTournamentScores(); - } - - level.intermissiontime = level.time; - FindIntermissionPoint(); - -#ifdef MISSIONPACK - if (g_singlePlayer.integer) { - trap_Cvar_Set("ui_singlePlayerActive", "0"); - UpdateTournamentInfo(); - } -#else - // if single player game - if ( g_gametype.integer == GT_SINGLE_PLAYER ) { - UpdateTournamentInfo(); - SpawnModelsOnVictoryPads(); - } -#endif - - // move all clients to the intermission point - for (i=0 ; i< level.maxclients ; i++) { - client = g_entities + i; - if (!client->inuse) - continue; - // respawn if dead - if (client->health <= 0) { - respawn(client); - } - MoveClientToIntermission( client ); - } - - // send the current scoring to all clients - SendScoreboardMessageToAllClients(); - -} - - -/* -============= -ExitLevel - -When the intermission has been exited, the server is either killed -or moved to a new level based on the "nextmap" cvar - -============= -*/ -void ExitLevel (void) { - int i; - gclient_t *cl; - - //bot interbreeding - BotInterbreedEndMatch(); - - // if we are running a tournement map, kick the loser to spectator status, - // which will automatically grab the next spectator and restart - if ( g_gametype.integer == GT_TOURNAMENT ) { - if ( !level.restarted ) { - RemoveTournamentLoser(); - trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); - level.restarted = qtrue; - level.changemap = NULL; - level.intermissiontime = 0; - } - return; - } - - - trap_SendConsoleCommand( EXEC_APPEND, "vstr nextmap\n" ); - level.changemap = NULL; - level.intermissiontime = 0; - - // reset all the scores so we don't enter the intermission again - level.teamScores[TEAM_RED] = 0; - level.teamScores[TEAM_BLUE] = 0; - for ( i=0 ; i< g_maxclients.integer ; i++ ) { - cl = level.clients + i; - if ( cl->pers.connected != CON_CONNECTED ) { - continue; - } - cl->ps.persistant[PERS_SCORE] = 0; - } - - // we need to do this here before chaning to CON_CONNECTING - G_WriteSessionData(); - - // change all client states to connecting, so the early players into the - // next level will know the others aren't done reconnecting - for (i=0 ; i< g_maxclients.integer ; i++) { - if ( level.clients[i].pers.connected == CON_CONNECTED ) { - level.clients[i].pers.connected = CON_CONNECTING; - } - } - -} - -/* -================= -G_LogPrintf - -Print to the logfile with a time stamp if it is open -================= -*/ -void QDECL G_LogPrintf( const char *fmt, ... ) { - va_list argptr; - char string[1024]; - int min, tens, sec; - - sec = level.time / 1000; - - min = sec / 60; - sec -= min * 60; - tens = sec / 10; - sec -= tens * 10; - - Com_sprintf( string, sizeof(string), "%3i:%i%i ", min, tens, sec ); - - va_start( argptr, fmt ); - vsprintf( string +7 , fmt,argptr ); - va_end( argptr ); - - if ( g_dedicated.integer ) { - G_Printf( "%s", string + 7 ); - } - - if ( !level.logFile ) { - return; - } - - trap_FS_Write( string, strlen( string ), level.logFile ); -} - -/* -================ -LogExit - -Append information about this game to the log file -================ -*/ -void LogExit( const char *string ) { - int i, numSorted; - gclient_t *cl; -#ifdef MISSIONPACK // bk001205 - qboolean won = qtrue; -#endif - G_LogPrintf( "Exit: %s\n", string ); - - level.intermissionQueued = level.time; - - // this will keep the clients from playing any voice sounds - // that will get cut off when the queued intermission starts - trap_SetConfigstring( CS_INTERMISSION, "1" ); - - // don't send more than 32 scores (FIXME?) - numSorted = level.numConnectedClients; - if ( numSorted > 32 ) { - numSorted = 32; - } - - if ( g_gametype.integer >= GT_TEAM ) { - G_LogPrintf( "red:%i blue:%i\n", - level.teamScores[TEAM_RED], level.teamScores[TEAM_BLUE] ); - } - - for (i=0 ; i < numSorted ; i++) { - int ping; - - cl = &level.clients[level.sortedClients[i]]; - - if ( cl->sess.sessionTeam == TEAM_SPECTATOR ) { - continue; - } - if ( cl->pers.connected == CON_CONNECTING ) { - continue; - } - - ping = cl->ps.ping < 999 ? cl->ps.ping : 999; - - G_LogPrintf( "score: %i ping: %i client: %i %s\n", cl->ps.persistant[PERS_SCORE], ping, level.sortedClients[i], cl->pers.netname ); -#ifdef MISSIONPACK - if (g_singlePlayer.integer && g_gametype.integer == GT_TOURNAMENT) { - if (g_entities[cl - level.clients].r.svFlags & SVF_BOT && cl->ps.persistant[PERS_RANK] == 0) { - won = qfalse; - } - } -#endif - - } - -#ifdef MISSIONPACK - if (g_singlePlayer.integer) { - if (g_gametype.integer >= GT_CTF) { - won = level.teamScores[TEAM_RED] > level.teamScores[TEAM_BLUE]; - } - trap_SendConsoleCommand( EXEC_APPEND, (won) ? "spWin\n" : "spLose\n" ); - } -#endif - - -} - - -/* -================= -CheckIntermissionExit - -The level will stay at the intermission for a minimum of 5 seconds -If all players wish to continue, the level will then exit. -If one or more players have not acknowledged the continue, the game will -wait 10 seconds before going on. -================= -*/ -void CheckIntermissionExit( void ) { - int ready, notReady; - int i; - gclient_t *cl; - int readyMask; - - if ( g_gametype.integer == GT_SINGLE_PLAYER ) { - return; - } - - // see which players are ready - ready = 0; - notReady = 0; - readyMask = 0; - for (i=0 ; i< g_maxclients.integer ; i++) { - cl = level.clients + i; - if ( cl->pers.connected != CON_CONNECTED ) { - continue; - } - if ( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) { - continue; - } - - if ( cl->readyToExit ) { - ready++; - if ( i < 16 ) { - readyMask |= 1 << i; - } - } else { - notReady++; - } - } - - // copy the readyMask to each player's stats so - // it can be displayed on the scoreboard - for (i=0 ; i< g_maxclients.integer ; i++) { - cl = level.clients + i; - if ( cl->pers.connected != CON_CONNECTED ) { - continue; - } - cl->ps.stats[STAT_CLIENTS_READY] = readyMask; - } - - // never exit in less than five seconds - if ( level.time < level.intermissiontime + 5000 ) { - return; - } - - // if nobody wants to go, clear timer - if ( !ready ) { - level.readyToExit = qfalse; - return; - } - - // if everyone wants to go, go now - if ( !notReady ) { - ExitLevel(); - return; - } - - // the first person to ready starts the ten second timeout - if ( !level.readyToExit ) { - level.readyToExit = qtrue; - level.exitTime = level.time; - } - - // if we have waited ten seconds since at least one player - // wanted to exit, go ahead - if ( level.time < level.exitTime + 10000 ) { - return; - } - - ExitLevel(); -} - -/* -============= -ScoreIsTied -============= -*/ -qboolean ScoreIsTied( void ) { - int a, b; - - if ( level.numPlayingClients < 2 ) { - return qfalse; - } - - if ( g_gametype.integer >= GT_TEAM ) { - return level.teamScores[TEAM_RED] == level.teamScores[TEAM_BLUE]; - } - - a = level.clients[level.sortedClients[0]].ps.persistant[PERS_SCORE]; - b = level.clients[level.sortedClients[1]].ps.persistant[PERS_SCORE]; - - return a == b; -} - -/* -================= -CheckExitRules - -There will be a delay between the time the exit is qualified for -and the time everyone is moved to the intermission spot, so you -can see the last frag. -================= -*/ -void CheckExitRules( void ) { - int i; - gclient_t *cl; - // if at the intermission, wait for all non-bots to - // signal ready, then go to next level - if ( level.intermissiontime ) { - CheckIntermissionExit (); - return; - } - - if ( level.intermissionQueued ) { -#ifdef MISSIONPACK - int time = (g_singlePlayer.integer) ? SP_INTERMISSION_DELAY_TIME : INTERMISSION_DELAY_TIME; - if ( level.time - level.intermissionQueued >= time ) { - level.intermissionQueued = 0; - BeginIntermission(); - } -#else - if ( level.time - level.intermissionQueued >= INTERMISSION_DELAY_TIME ) { - level.intermissionQueued = 0; - BeginIntermission(); - } -#endif - return; - } - - // check for sudden death - if ( ScoreIsTied() ) { - // always wait for sudden death - return; - } - - if ( g_timelimit.integer && !level.warmupTime ) { - if ( level.time - level.startTime >= g_timelimit.integer*60000 ) { - trap_SendServerCommand( -1, "print \"Timelimit hit.\n\""); - LogExit( "Timelimit hit." ); - return; - } - } - - if ( level.numPlayingClients < 2 ) { - return; - } - - if ( g_gametype.integer < GT_CTF && g_fraglimit.integer ) { - if ( level.teamScores[TEAM_RED] >= g_fraglimit.integer ) { - trap_SendServerCommand( -1, "print \"Red hit the fraglimit.\n\"" ); - LogExit( "Fraglimit hit." ); - return; - } - - if ( level.teamScores[TEAM_BLUE] >= g_fraglimit.integer ) { - trap_SendServerCommand( -1, "print \"Blue hit the fraglimit.\n\"" ); - LogExit( "Fraglimit hit." ); - return; - } - - for ( i=0 ; i< g_maxclients.integer ; i++ ) { - cl = level.clients + i; - if ( cl->pers.connected != CON_CONNECTED ) { - continue; - } - if ( cl->sess.sessionTeam != TEAM_FREE ) { - continue; - } - - if ( cl->ps.persistant[PERS_SCORE] >= g_fraglimit.integer ) { - LogExit( "Fraglimit hit." ); - trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " hit the fraglimit.\n\"", - cl->pers.netname ) ); - return; - } - } - } - - if ( g_gametype.integer >= GT_CTF && g_capturelimit.integer ) { - - if ( level.teamScores[TEAM_RED] >= g_capturelimit.integer ) { - trap_SendServerCommand( -1, "print \"Red hit the capturelimit.\n\"" ); - LogExit( "Capturelimit hit." ); - return; - } - - if ( level.teamScores[TEAM_BLUE] >= g_capturelimit.integer ) { - trap_SendServerCommand( -1, "print \"Blue hit the capturelimit.\n\"" ); - LogExit( "Capturelimit hit." ); - return; - } - } -} - - - -/* -======================================================================== - -FUNCTIONS CALLED EVERY FRAME - -======================================================================== -*/ - - -/* -============= -CheckTournament - -Once a frame, check for changes in tournement player state -============= -*/ -void CheckTournament( void ) { - // check because we run 3 game frames before calling Connect and/or ClientBegin - // for clients on a map_restart - if ( level.numPlayingClients == 0 ) { - return; - } - - if ( g_gametype.integer == GT_TOURNAMENT ) { - - // pull in a spectator if needed - if ( level.numPlayingClients < 2 ) { - AddTournamentPlayer(); - } - - // if we don't have two players, go back to "waiting for players" - if ( level.numPlayingClients != 2 ) { - if ( level.warmupTime != -1 ) { - level.warmupTime = -1; - trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); - G_LogPrintf( "Warmup:\n" ); - } - return; - } - - if ( level.warmupTime == 0 ) { - return; - } - - // if the warmup is changed at the console, restart it - if ( g_warmup.modificationCount != level.warmupModificationCount ) { - level.warmupModificationCount = g_warmup.modificationCount; - level.warmupTime = -1; - } - - // if all players have arrived, start the countdown - if ( level.warmupTime < 0 ) { - if ( level.numPlayingClients == 2 ) { - // fudge by -1 to account for extra delays - level.warmupTime = level.time + ( g_warmup.integer - 1 ) * 1000; - trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); - } - return; - } - - // if the warmup time has counted down, restart - if ( level.time > level.warmupTime ) { - level.warmupTime += 10000; - trap_Cvar_Set( "g_restarted", "1" ); - trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); - level.restarted = qtrue; - return; - } - } else if ( g_gametype.integer != GT_SINGLE_PLAYER && level.warmupTime != 0 ) { - int counts[TEAM_NUM_TEAMS]; - qboolean notEnough = qfalse; - - if ( g_gametype.integer > GT_TEAM ) { - counts[TEAM_BLUE] = TeamCount( -1, TEAM_BLUE ); - counts[TEAM_RED] = TeamCount( -1, TEAM_RED ); - - if (counts[TEAM_RED] < 1 || counts[TEAM_BLUE] < 1) { - notEnough = qtrue; - } - } else if ( level.numPlayingClients < 2 ) { - notEnough = qtrue; - } - - if ( notEnough ) { - if ( level.warmupTime != -1 ) { - level.warmupTime = -1; - trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); - G_LogPrintf( "Warmup:\n" ); - } - return; // still waiting for team members - } - - if ( level.warmupTime == 0 ) { - return; - } - - // if the warmup is changed at the console, restart it - if ( g_warmup.modificationCount != level.warmupModificationCount ) { - level.warmupModificationCount = g_warmup.modificationCount; - level.warmupTime = -1; - } - - // if all players have arrived, start the countdown - if ( level.warmupTime < 0 ) { - // fudge by -1 to account for extra delays - level.warmupTime = level.time + ( g_warmup.integer - 1 ) * 1000; - trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); - return; - } - - // if the warmup time has counted down, restart - if ( level.time > level.warmupTime ) { - level.warmupTime += 10000; - trap_Cvar_Set( "g_restarted", "1" ); - trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); - level.restarted = qtrue; - return; - } - } -} - - -/* -================== -CheckVote -================== -*/ -void CheckVote( void ) { - if ( level.voteExecuteTime && level.voteExecuteTime < level.time ) { - level.voteExecuteTime = 0; - trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.voteString ) ); - } - if ( !level.voteTime ) { - return; - } - if ( level.time - level.voteTime >= VOTE_TIME ) { - trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); - } else { - // ATVI Q3 1.32 Patch #9, WNF - if ( level.voteYes > level.numVotingClients/2 ) { - // execute the command, then remove the vote - trap_SendServerCommand( -1, "print \"Vote passed.\n\"" ); - level.voteExecuteTime = level.time + 3000; - } else if ( level.voteNo >= level.numVotingClients/2 ) { - // same behavior as a timeout - trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); - } else { - // still waiting for a majority - return; - } - } - level.voteTime = 0; - trap_SetConfigstring( CS_VOTE_TIME, "" ); - -} - -/* -================== -PrintTeam -================== -*/ -void PrintTeam(int team, char *message) { - int i; - - for ( i = 0 ; i < level.maxclients ; i++ ) { - if (level.clients[i].sess.sessionTeam != team) - continue; - trap_SendServerCommand( i, message ); - } -} - -/* -================== -SetLeader -================== -*/ -void SetLeader(int team, int client) { - int i; - - if ( level.clients[client].pers.connected == CON_DISCONNECTED ) { - PrintTeam(team, va("print \"%s is not connected\n\"", level.clients[client].pers.netname) ); - return; - } - if (level.clients[client].sess.sessionTeam != team) { - PrintTeam(team, va("print \"%s is not on the team anymore\n\"", level.clients[client].pers.netname) ); - return; - } - for ( i = 0 ; i < level.maxclients ; i++ ) { - if (level.clients[i].sess.sessionTeam != team) - continue; - if (level.clients[i].sess.teamLeader) { - level.clients[i].sess.teamLeader = qfalse; - ClientUserinfoChanged(i); - } - } - level.clients[client].sess.teamLeader = qtrue; - ClientUserinfoChanged( client ); - PrintTeam(team, va("print \"%s is the new team leader\n\"", level.clients[client].pers.netname) ); -} - -/* -================== -CheckTeamLeader -================== -*/ -void CheckTeamLeader( int team ) { - int i; - - for ( i = 0 ; i < level.maxclients ; i++ ) { - if (level.clients[i].sess.sessionTeam != team) - continue; - if (level.clients[i].sess.teamLeader) - break; - } - if (i >= level.maxclients) { - for ( i = 0 ; i < level.maxclients ; i++ ) { - if (level.clients[i].sess.sessionTeam != team) - continue; - if (!(g_entities[i].r.svFlags & SVF_BOT)) { - level.clients[i].sess.teamLeader = qtrue; - break; - } - } - for ( i = 0 ; i < level.maxclients ; i++ ) { - if (level.clients[i].sess.sessionTeam != team) - continue; - level.clients[i].sess.teamLeader = qtrue; - break; - } - } -} - -/* -================== -CheckTeamVote -================== -*/ -void CheckTeamVote( int team ) { - int cs_offset; - - if ( team == TEAM_RED ) - cs_offset = 0; - else if ( team == TEAM_BLUE ) - cs_offset = 1; - else - return; - - if ( !level.teamVoteTime[cs_offset] ) { - return; - } - if ( level.time - level.teamVoteTime[cs_offset] >= VOTE_TIME ) { - trap_SendServerCommand( -1, "print \"Team vote failed.\n\"" ); - } else { - if ( level.teamVoteYes[cs_offset] > level.numteamVotingClients[cs_offset]/2 ) { - // execute the command, then remove the vote - trap_SendServerCommand( -1, "print \"Team vote passed.\n\"" ); - // - if ( !Q_strncmp( "leader", level.teamVoteString[cs_offset], 6) ) { - //set the team leader - SetLeader(team, atoi(level.teamVoteString[cs_offset] + 7)); - } - else { - trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.teamVoteString[cs_offset] ) ); - } - } else if ( level.teamVoteNo[cs_offset] >= level.numteamVotingClients[cs_offset]/2 ) { - // same behavior as a timeout - trap_SendServerCommand( -1, "print \"Team vote failed.\n\"" ); - } else { - // still waiting for a majority - return; - } - } - level.teamVoteTime[cs_offset] = 0; - trap_SetConfigstring( CS_TEAMVOTE_TIME + cs_offset, "" ); - -} - - -/* -================== -CheckCvars -================== -*/ -void CheckCvars( void ) { - static int lastMod = -1; - - if ( g_password.modificationCount != lastMod ) { - lastMod = g_password.modificationCount; - if ( *g_password.string && Q_stricmp( g_password.string, "none" ) ) { - trap_Cvar_Set( "g_needpass", "1" ); - } else { - trap_Cvar_Set( "g_needpass", "0" ); - } - } -} - -/* -============= -G_RunThink - -Runs thinking code for this frame if necessary -============= -*/ -void G_RunThink (gentity_t *ent) { - float thinktime; - - thinktime = ent->nextthink; - if (thinktime <= 0) { - return; - } - if (thinktime > level.time) { - return; - } - - ent->nextthink = 0; - if (!ent->think) { - G_Error ( "NULL ent->think"); - } - ent->think (ent); -} - -/* -================ -G_RunFrame - -Advances the non-player objects in the world -================ -*/ -void G_RunFrame( int levelTime ) { - int i; - gentity_t *ent; - int msec; -int start, end; - - // if we are waiting for the level to restart, do nothing - if ( level.restarted ) { - return; - } - - level.framenum++; - level.previousTime = level.time; - level.time = levelTime; - msec = level.time - level.previousTime; - - // get any cvar changes - G_UpdateCvars(); - - // demo playback: sync recorded player states and process spectator - if ( g_svDemoPlaying.integer ) { - gentity_t *specEnt = NULL; - - // 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++ ) { - gclient_t *cl = &level.clients[i]; - gentity_t *e = &g_entities[i]; - - // find the spectator — use client-owned origin for PVS - if ( e->client && cl->pers.connected == CON_CONNECTED - && cl->sess.sessionTeam == TEAM_SPECTATOR ) { - // 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 ); - } - // process spectator buttons for follow mode switching - // (pers.cmd is updated by ClientThink which still runs) - { - int oldButtons = cl->buttons; - cl->oldbuttons = cl->buttons; - cl->buttons = cl->pers.cmd.buttons; - // attack cycles follow targets - if ( ( cl->buttons & BUTTON_ATTACK ) && !( oldButtons & BUTTON_ATTACK ) ) { - Cmd_FollowCycle_f( e, 1 ); - } - } - specEnt = 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; - } - } - - // evaluate trajectories and link all recorded entities for PVS. - // SVD_ReadFrame injected entityState_t but didn't link because - // the server doesn't have BG_EvaluateTrajectory. - // scan up to MAX_GENTITIES because recorded entities may exceed - // level.num_entities (which only counts game-module-spawned entities). - for ( i = 0; i < MAX_GENTITIES; i++ ) { - ent = &g_entities[i]; - if ( ent->s.eType == 0 && !ent->inuse ) { - continue; - } - // skip spectator slot - if ( ent->client && ent->client->pers.connected == CON_CONNECTED - && ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { - continue; - } - // compute currentOrigin from trajectory - BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin ); - ent->r.linked = qtrue; - trap_LinkEntity( ent ); - } - - // update rankings so follow mode can cycle through players - CalculateRanks(); - - // run end-of-frame for spectator (handles follow mode PS copy) - if ( specEnt ) { - ClientEndFrame( specEnt ); - } - return; - } - - // - // go through all allocated objects - // - start = trap_Milliseconds(); - ent = &g_entities[0]; - for (i=0 ; iinuse ) { - continue; - } - - // clear events that are too old - if ( level.time - ent->eventTime > EVENT_VALID_MSEC ) { - if ( ent->s.event ) { - ent->s.event = 0; // &= EV_EVENT_BITS; - if ( ent->client ) { - ent->client->ps.externalEvent = 0; - // predicted events should never be set to zero - //ent->client->ps.events[0] = 0; - //ent->client->ps.events[1] = 0; - } - } - if ( ent->freeAfterEvent ) { - // tempEntities or dropped items completely go away after their event - G_FreeEntity( ent ); - continue; - } else if ( ent->unlinkAfterEvent ) { - // items that will respawn will hide themselves after their pickup event - ent->unlinkAfterEvent = qfalse; - trap_UnlinkEntity( ent ); - } - } - - // temporary entities don't think - if ( ent->freeAfterEvent ) { - continue; - } - - if ( !ent->r.linked && ent->neverFree ) { - continue; - } - - if ( ent->s.eType == ET_MISSILE ) { - G_RunMissile( ent ); - continue; - } - - if ( ent->s.eType == ET_ITEM || ent->physicsObject ) { - G_RunItem( ent ); - continue; - } - - if ( ent->s.eType == ET_MOVER ) { - G_RunMover( ent ); - continue; - } - - if ( i < MAX_CLIENTS ) { - G_RunClient( ent ); - continue; - } - - G_RunThink( ent ); - } -end = trap_Milliseconds(); - -start = trap_Milliseconds(); - // perform final fixups on the players - ent = &g_entities[0]; - for (i=0 ; i < level.maxclients ; i++, ent++ ) { - if ( ent->inuse ) { - ClientEndFrame( ent ); - } - } -end = trap_Milliseconds(); - - // see if it is time to do a tournement restart - CheckTournament(); - - // see if it is time to end the level - CheckExitRules(); - - // update to team status? - CheckTeamStatus(); - - // cancel vote if timed out - CheckVote(); - - // check team votes - CheckTeamVote( TEAM_RED ); - CheckTeamVote( TEAM_BLUE ); - - // for tracking changes - CheckCvars(); - - if (g_listEntity.integer) { - for (i = 0; i < MAX_GENTITIES; i++) { - G_Printf("%4i: %s\n", i, g_entities[i].classname); - } - trap_Cvar_Set("g_listEntity", "0"); - } -} +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ +// + +#include "g_local.h" + +level_locals_t level; + +typedef struct { + vmCvar_t *vmCvar; + char *cvarName; + char *defaultString; + int cvarFlags; + int modificationCount; // for tracking changes + qboolean trackChange; // track this variable, and announce if changed + qboolean teamShader; // track and if changed, update shader state +} cvarTable_t; + +gentity_t g_entities[MAX_GENTITIES]; +gclient_t g_clients[MAX_CLIENTS]; + +vmCvar_t g_gametype; +vmCvar_t g_svDemoPlaying; +vmCvar_t g_dmflags; +vmCvar_t g_fraglimit; +vmCvar_t g_timelimit; +vmCvar_t g_capturelimit; +vmCvar_t g_friendlyFire; +vmCvar_t g_password; +vmCvar_t g_needpass; +vmCvar_t g_maxclients; +vmCvar_t g_maxGameClients; +vmCvar_t g_dedicated; +vmCvar_t g_speed; +vmCvar_t g_gravity; +vmCvar_t g_cheats; +vmCvar_t g_knockback; +vmCvar_t g_quadfactor; +vmCvar_t g_forcerespawn; +vmCvar_t g_inactivity; +vmCvar_t g_debugMove; +vmCvar_t g_debugDamage; +vmCvar_t g_debugAlloc; +vmCvar_t g_weaponRespawn; +vmCvar_t g_weaponTeamRespawn; +vmCvar_t g_motd; +vmCvar_t g_synchronousClients; +vmCvar_t g_warmup; +vmCvar_t g_doWarmup; +vmCvar_t g_restarted; +vmCvar_t g_log; +vmCvar_t g_logSync; +vmCvar_t g_blood; +vmCvar_t g_podiumDist; +vmCvar_t g_podiumDrop; +vmCvar_t g_allowVote; +vmCvar_t g_teamAutoJoin; +vmCvar_t g_teamForceBalance; +vmCvar_t g_banIPs; +vmCvar_t g_filterBan; +vmCvar_t g_smoothClients; +vmCvar_t pmove_fixed; +vmCvar_t pmove_msec; +vmCvar_t g_rankings; +vmCvar_t g_listEntity; +#ifdef MISSIONPACK +vmCvar_t g_obeliskHealth; +vmCvar_t g_obeliskRegenPeriod; +vmCvar_t g_obeliskRegenAmount; +vmCvar_t g_obeliskRespawnDelay; +vmCvar_t g_cubeTimeout; +vmCvar_t g_redteam; +vmCvar_t g_blueteam; +vmCvar_t g_singlePlayer; +vmCvar_t g_enableDust; +vmCvar_t g_enableBreath; +vmCvar_t g_proxMineTimeout; +#endif + +// bk001129 - made static to avoid aliasing +static cvarTable_t gameCvarTable[] = { + // don't override the cheat state set by the system + { &g_cheats, "sv_cheats", "", 0, 0, qfalse }, + + // noset vars + { NULL, "gamename", GAMEVERSION , CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + { NULL, "gamedate", __DATE__ , CVAR_ROM, 0, qfalse }, + { &g_restarted, "g_restarted", "0", CVAR_ROM, 0, qfalse }, + { NULL, "sv_mapname", "", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + // latched vars + { &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_USERINFO | CVAR_LATCH, 0, qfalse }, + { &g_svDemoPlaying, "sv_demoplaying", "0", CVAR_ROM, 0, qfalse }, + + { &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + { &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + + // change anytime vars + { &g_dmflags, "dmflags", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + { &g_fraglimit, "fraglimit", "20", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_timelimit, "timelimit", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_capturelimit, "capturelimit", "8", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + + { &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_friendlyFire, "g_friendlyFire", "0", CVAR_ARCHIVE, 0, qtrue }, + + { &g_teamAutoJoin, "g_teamAutoJoin", "0", CVAR_ARCHIVE }, + { &g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE }, + + { &g_warmup, "g_warmup", "20", CVAR_ARCHIVE, 0, qtrue }, + { &g_doWarmup, "g_doWarmup", "0", 0, 0, qtrue }, + { &g_log, "g_log", "games.log", CVAR_ARCHIVE, 0, qfalse }, + { &g_logSync, "g_logSync", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_password, "g_password", "", CVAR_USERINFO, 0, qfalse }, + + { &g_banIPs, "g_banIPs", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_filterBan, "g_filterBan", "1", CVAR_ARCHIVE, 0, qfalse }, + + { &g_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + { &g_dedicated, "dedicated", "0", 0, 0, qfalse }, + + { &g_speed, "g_speed", "320", 0, 0, qtrue }, + { &g_gravity, "g_gravity", "800", 0, 0, qtrue }, + { &g_knockback, "g_knockback", "1000", 0, 0, qtrue }, + { &g_quadfactor, "g_quadfactor", "3", 0, 0, qtrue }, + { &g_weaponRespawn, "g_weaponrespawn", "5", 0, 0, qtrue }, + { &g_weaponTeamRespawn, "g_weaponTeamRespawn", "30", 0, 0, qtrue }, + { &g_forcerespawn, "g_forcerespawn", "20", 0, 0, qtrue }, + { &g_inactivity, "g_inactivity", "0", 0, 0, qtrue }, + { &g_debugMove, "g_debugMove", "0", 0, 0, qfalse }, + { &g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse }, + { &g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse }, + { &g_motd, "g_motd", "", 0, 0, qfalse }, + { &g_blood, "com_blood", "1", 0, 0, qfalse }, + + { &g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse }, + { &g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse }, + + { &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_listEntity, "g_listEntity", "0", 0, 0, qfalse }, + +#ifdef MISSIONPACK + { &g_obeliskHealth, "g_obeliskHealth", "2500", 0, 0, qfalse }, + { &g_obeliskRegenPeriod, "g_obeliskRegenPeriod", "1", 0, 0, qfalse }, + { &g_obeliskRegenAmount, "g_obeliskRegenAmount", "15", 0, 0, qfalse }, + { &g_obeliskRespawnDelay, "g_obeliskRespawnDelay", "10", CVAR_SERVERINFO, 0, qfalse }, + + { &g_cubeTimeout, "g_cubeTimeout", "30", 0, 0, qfalse }, + { &g_redteam, "g_redteam", "Stroggs", CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_USERINFO , 0, qtrue, qtrue }, + { &g_blueteam, "g_blueteam", "Pagans", CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_USERINFO , 0, qtrue, qtrue }, + { &g_singlePlayer, "ui_singlePlayerActive", "", 0, 0, qfalse, qfalse }, + + { &g_enableDust, "g_enableDust", "0", CVAR_SERVERINFO, 0, qtrue, qfalse }, + { &g_enableBreath, "g_enableBreath", "0", CVAR_SERVERINFO, 0, qtrue, qfalse }, + { &g_proxMineTimeout, "g_proxMineTimeout", "20000", 0, 0, qfalse }, +#endif + { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, + { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, + { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, + + { &g_rankings, "g_rankings", "0", 0, 0, qfalse} + +}; + +// bk001129 - made static to avoid aliasing +static int gameCvarTableSize = sizeof( gameCvarTable ) / sizeof( gameCvarTable[0] ); + + +void G_InitGame( int levelTime, int randomSeed, int restart ); +void G_RunFrame( int levelTime ); +void G_ShutdownGame( int restart ); +void CheckExitRules( void ); + + +/* +================ +vmMain + +This is the only way control passes into the module. +This must be the very first function compiled into the .q3vm file +================ +*/ +int vmMain( int command, int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10, int arg11 ) { + switch ( command ) { + case GAME_INIT: + G_InitGame( arg0, arg1, arg2 ); + return 0; + case GAME_SHUTDOWN: + G_ShutdownGame( arg0 ); + return 0; + case GAME_CLIENT_CONNECT: + return (int)ClientConnect( arg0, arg1, arg2 ); + case GAME_CLIENT_THINK: + ClientThink( arg0 ); + return 0; + case GAME_CLIENT_USERINFO_CHANGED: + ClientUserinfoChanged( arg0 ); + return 0; + case GAME_CLIENT_DISCONNECT: + ClientDisconnect( arg0 ); + return 0; + case GAME_CLIENT_BEGIN: + ClientBegin( arg0 ); + return 0; + case GAME_CLIENT_COMMAND: + ClientCommand( arg0 ); + return 0; + case GAME_RUN_FRAME: + G_RunFrame( arg0 ); + return 0; + case GAME_CONSOLE_COMMAND: + return ConsoleCommand(); + case BOTAI_START_FRAME: + return BotAIStartFrame( arg0 ); + } + + return -1; +} + + +void QDECL G_Printf( const char *fmt, ... ) { + va_list argptr; + char text[1024]; + + va_start (argptr, fmt); + vsprintf (text, fmt, argptr); + va_end (argptr); + + trap_Printf( text ); +} + +void QDECL G_Error( const char *fmt, ... ) { + va_list argptr; + char text[1024]; + + va_start (argptr, fmt); + vsprintf (text, fmt, argptr); + va_end (argptr); + + trap_Error( text ); +} + +/* +================ +G_FindTeams + +Chain together all entities with a matching team field. +Entity teams are used for item groups and multi-entity mover groups. + +All but the first will have the FL_TEAMSLAVE flag set and teammaster field set +All but the last will have the teamchain field set to the next one +================ +*/ +void G_FindTeams( void ) { + gentity_t *e, *e2; + int i, j; + int c, c2; + + c = 0; + c2 = 0; + for ( i=1, e=g_entities+i ; i < level.num_entities ; i++,e++ ){ + if (!e->inuse) + continue; + if (!e->team) + continue; + if (e->flags & FL_TEAMSLAVE) + continue; + e->teammaster = e; + c++; + c2++; + for (j=i+1, e2=e+1 ; j < level.num_entities ; j++,e2++) + { + if (!e2->inuse) + continue; + if (!e2->team) + continue; + if (e2->flags & FL_TEAMSLAVE) + continue; + if (!strcmp(e->team, e2->team)) + { + c2++; + e2->teamchain = e->teamchain; + e->teamchain = e2; + e2->teammaster = e; + e2->flags |= FL_TEAMSLAVE; + + // make sure that targets only point at the master + if ( e2->targetname ) { + e->targetname = e2->targetname; + e2->targetname = NULL; + } + } + } + } + + G_Printf ("%i teams with %i entities\n", c, c2); +} + +void G_RemapTeamShaders() { +#ifdef MISSIONPACK + char string[1024]; + float f = level.time * 0.001; + Com_sprintf( string, sizeof(string), "team_icon/%s_red", g_redteam.string ); + AddRemap("textures/ctf2/redteam01", string, f); + AddRemap("textures/ctf2/redteam02", string, f); + Com_sprintf( string, sizeof(string), "team_icon/%s_blue", g_blueteam.string ); + AddRemap("textures/ctf2/blueteam01", string, f); + AddRemap("textures/ctf2/blueteam02", string, f); + trap_SetConfigstring(CS_SHADERSTATE, BuildShaderStateConfig()); +#endif +} + + +/* +================= +G_RegisterCvars +================= +*/ +void G_RegisterCvars( void ) { + int i; + cvarTable_t *cv; + qboolean remapped = qfalse; + + for ( i = 0, cv = gameCvarTable ; i < gameCvarTableSize ; i++, cv++ ) { + trap_Cvar_Register( cv->vmCvar, cv->cvarName, + cv->defaultString, cv->cvarFlags ); + if ( cv->vmCvar ) + cv->modificationCount = cv->vmCvar->modificationCount; + + if (cv->teamShader) { + remapped = qtrue; + } + } + + if (remapped) { + G_RemapTeamShaders(); + } + + // check some things + if ( g_gametype.integer < 0 || g_gametype.integer >= GT_MAX_GAME_TYPE ) { + G_Printf( "g_gametype %i is out of range, defaulting to 0\n", g_gametype.integer ); + trap_Cvar_Set( "g_gametype", "0" ); + } + + level.warmupModificationCount = g_warmup.modificationCount; +} + +/* +================= +G_UpdateCvars +================= +*/ +void G_UpdateCvars( void ) { + int i; + cvarTable_t *cv; + qboolean remapped = qfalse; + + for ( i = 0, cv = gameCvarTable ; i < gameCvarTableSize ; i++, cv++ ) { + if ( cv->vmCvar ) { + trap_Cvar_Update( cv->vmCvar ); + + if ( cv->modificationCount != cv->vmCvar->modificationCount ) { + cv->modificationCount = cv->vmCvar->modificationCount; + + if ( cv->trackChange ) { + trap_SendServerCommand( -1, va("print \"Server: %s changed to %s\n\"", + cv->cvarName, cv->vmCvar->string ) ); + } + + if (cv->teamShader) { + remapped = qtrue; + } + } + } + } + + if (remapped) { + G_RemapTeamShaders(); + } +} + +/* +============ +G_InitGame + +============ +*/ +void G_InitGame( int levelTime, int randomSeed, int restart ) { + int i; + + G_Printf ("------- Game Initialization -------\n"); + G_Printf ("gamename: %s\n", GAMEVERSION); + G_Printf ("gamedate: %s\n", __DATE__); + + srand( randomSeed ); + + G_RegisterCvars(); + + // signal server-side demo mode to cgame via configstring + if ( g_svDemoPlaying.integer ) { + trap_SetConfigstring( CS_SVDEMO, "1" ); + } + + G_ProcessIPBans(); + + G_InitMemory(); + + // set some level globals + memset( &level, 0, sizeof( level ) ); + level.time = levelTime; + level.startTime = levelTime; + + level.snd_fry = G_SoundIndex("sound/player/fry.wav"); // FIXME standing in lava / slime + + if ( g_gametype.integer != GT_SINGLE_PLAYER && g_log.string[0] ) { + if ( g_logSync.integer ) { + trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND_SYNC ); + } else { + trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND ); + } + if ( !level.logFile ) { + G_Printf( "WARNING: Couldn't open logfile: %s\n", g_log.string ); + } else { + char serverinfo[MAX_INFO_STRING]; + + trap_GetServerinfo( serverinfo, sizeof( serverinfo ) ); + + G_LogPrintf("------------------------------------------------------------\n" ); + G_LogPrintf("InitGame: %s\n", serverinfo ); + } + } else { + G_Printf( "Not logging to disk.\n" ); + } + + G_InitWorldSession(); + + // initialize all entities for this game + memset( g_entities, 0, MAX_GENTITIES * sizeof(g_entities[0]) ); + level.gentities = g_entities; + + // initialize all clients for this game + level.maxclients = g_maxclients.integer; + memset( g_clients, 0, MAX_CLIENTS * sizeof(g_clients[0]) ); + level.clients = g_clients; + + // set client fields on player ents + for ( i=0 ; i= GT_TEAM ) { + G_CheckTeamItems(); + } + + SaveRegisteredItems(); + + G_Printf ("-----------------------------------\n"); + + if( g_gametype.integer == GT_SINGLE_PLAYER || trap_Cvar_VariableIntegerValue( "com_buildScript" ) ) { + G_ModelIndex( SP_PODIUM_MODEL ); + G_SoundIndex( "sound/player/gurp1.wav" ); + G_SoundIndex( "sound/player/gurp2.wav" ); + } + + if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) ) { + BotAISetup( restart ); + BotAILoadMap( restart ); + G_InitBots( restart ); + } + + G_RemapTeamShaders(); + +} + + + +/* +================= +G_ShutdownGame +================= +*/ +void G_ShutdownGame( int restart ) { + G_Printf ("==== ShutdownGame ====\n"); + + if ( level.logFile ) { + G_LogPrintf("ShutdownGame:\n" ); + G_LogPrintf("------------------------------------------------------------\n" ); + trap_FS_FCloseFile( level.logFile ); + } + + // write all the client session data so we can get it back + G_WriteSessionData(); + + if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) ) { + BotAIShutdown( restart ); + } +} + + + +//=================================================================== + +#ifndef GAME_HARD_LINKED +// this is only here so the functions in q_shared.c and bg_*.c can link + +void QDECL Com_Error ( int level, const char *error, ... ) { + va_list argptr; + char text[1024]; + + va_start (argptr, error); + vsprintf (text, error, argptr); + va_end (argptr); + + G_Error( "%s", text); +} + +void QDECL Com_Printf( const char *msg, ... ) { + va_list argptr; + char text[1024]; + + va_start (argptr, msg); + vsprintf (text, msg, argptr); + va_end (argptr); + + G_Printf ("%s", text); +} + +#endif + +/* +======================================================================== + +PLAYER COUNTING / SCORE SORTING + +======================================================================== +*/ + +/* +============= +AddTournamentPlayer + +If there are less than two tournament players, put a +spectator in the game and restart +============= +*/ +void AddTournamentPlayer( void ) { + int i; + gclient_t *client; + gclient_t *nextInLine; + + if ( level.numPlayingClients >= 2 ) { + return; + } + + // never change during intermission + if ( level.intermissiontime ) { + return; + } + + nextInLine = NULL; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + client = &level.clients[i]; + if ( client->pers.connected != CON_CONNECTED ) { + continue; + } + if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { + continue; + } + // never select the dedicated follow or scoreboard clients + if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD || + client->sess.spectatorClient < 0 ) { + continue; + } + + if ( !nextInLine || client->sess.spectatorTime < nextInLine->sess.spectatorTime ) { + nextInLine = client; + } + } + + if ( !nextInLine ) { + return; + } + + level.warmupTime = -1; + + // set them to free-for-all team + SetTeam( &g_entities[ nextInLine - level.clients ], "f" ); +} + +/* +======================= +RemoveTournamentLoser + +Make the loser a spectator at the back of the line +======================= +*/ +void RemoveTournamentLoser( void ) { + int clientNum; + + if ( level.numPlayingClients != 2 ) { + return; + } + + clientNum = level.sortedClients[1]; + + if ( level.clients[ clientNum ].pers.connected != CON_CONNECTED ) { + return; + } + + // make them a spectator + SetTeam( &g_entities[ clientNum ], "s" ); +} + +/* +======================= +RemoveTournamentWinner +======================= +*/ +void RemoveTournamentWinner( void ) { + int clientNum; + + if ( level.numPlayingClients != 2 ) { + return; + } + + clientNum = level.sortedClients[0]; + + if ( level.clients[ clientNum ].pers.connected != CON_CONNECTED ) { + return; + } + + // make them a spectator + SetTeam( &g_entities[ clientNum ], "s" ); +} + +/* +======================= +AdjustTournamentScores +======================= +*/ +void AdjustTournamentScores( void ) { + int clientNum; + + clientNum = level.sortedClients[0]; + if ( level.clients[ clientNum ].pers.connected == CON_CONNECTED ) { + level.clients[ clientNum ].sess.wins++; + ClientUserinfoChanged( clientNum ); + } + + clientNum = level.sortedClients[1]; + if ( level.clients[ clientNum ].pers.connected == CON_CONNECTED ) { + level.clients[ clientNum ].sess.losses++; + ClientUserinfoChanged( clientNum ); + } + +} + +/* +============= +SortRanks + +============= +*/ +int QDECL SortRanks( const void *a, const void *b ) { + gclient_t *ca, *cb; + + ca = &level.clients[*(int *)a]; + cb = &level.clients[*(int *)b]; + + // sort special clients last + if ( ca->sess.spectatorState == SPECTATOR_SCOREBOARD || ca->sess.spectatorClient < 0 ) { + return 1; + } + if ( cb->sess.spectatorState == SPECTATOR_SCOREBOARD || cb->sess.spectatorClient < 0 ) { + return -1; + } + + // then connecting clients + if ( ca->pers.connected == CON_CONNECTING ) { + return 1; + } + if ( cb->pers.connected == CON_CONNECTING ) { + return -1; + } + + + // then spectators + if ( ca->sess.sessionTeam == TEAM_SPECTATOR && cb->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( ca->sess.spectatorTime < cb->sess.spectatorTime ) { + return -1; + } + if ( ca->sess.spectatorTime > cb->sess.spectatorTime ) { + return 1; + } + return 0; + } + if ( ca->sess.sessionTeam == TEAM_SPECTATOR ) { + return 1; + } + if ( cb->sess.sessionTeam == TEAM_SPECTATOR ) { + return -1; + } + + // then sort by score + if ( ca->ps.persistant[PERS_SCORE] + > cb->ps.persistant[PERS_SCORE] ) { + return -1; + } + if ( ca->ps.persistant[PERS_SCORE] + < cb->ps.persistant[PERS_SCORE] ) { + return 1; + } + return 0; +} + +/* +============ +CalculateRanks + +Recalculates the score ranks of all players +This will be called on every client connect, begin, disconnect, death, +and team change. +============ +*/ +void CalculateRanks( void ) { + int i; + 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; + level.follow2 = -1; + level.numConnectedClients = 0; + level.numNonSpectatorClients = 0; + level.numPlayingClients = 0; + level.numVotingClients = 0; // don't count bots + for ( i = 0; i < TEAM_NUM_TEAMS; i++ ) { + level.numteamVotingClients[i] = 0; + } + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected != CON_DISCONNECTED ) { + level.sortedClients[level.numConnectedClients] = i; + level.numConnectedClients++; + + if ( level.clients[i].sess.sessionTeam != TEAM_SPECTATOR ) { + level.numNonSpectatorClients++; + + // decide if this should be auto-followed + if ( level.clients[i].pers.connected == CON_CONNECTED ) { + level.numPlayingClients++; + if ( !(g_entities[i].r.svFlags & SVF_BOT) ) { + level.numVotingClients++; + if ( level.clients[i].sess.sessionTeam == TEAM_RED ) + level.numteamVotingClients[0]++; + else if ( level.clients[i].sess.sessionTeam == TEAM_BLUE ) + level.numteamVotingClients[1]++; + } + if ( level.follow1 == -1 ) { + level.follow1 = i; + } else if ( level.follow2 == -1 ) { + level.follow2 = i; + } + } + } + } + } + + qsort( level.sortedClients, level.numConnectedClients, + sizeof(level.sortedClients[0]), SortRanks ); + + // set the rank value for all clients that are connected and not spectators + if ( g_gametype.integer >= GT_TEAM ) { + // in team games, rank is just the order of the teams, 0=red, 1=blue, 2=tied + for ( i = 0; i < level.numConnectedClients; i++ ) { + cl = &level.clients[ level.sortedClients[i] ]; + if ( level.teamScores[TEAM_RED] == level.teamScores[TEAM_BLUE] ) { + cl->ps.persistant[PERS_RANK] = 2; + } else if ( level.teamScores[TEAM_RED] > level.teamScores[TEAM_BLUE] ) { + cl->ps.persistant[PERS_RANK] = 0; + } else { + cl->ps.persistant[PERS_RANK] = 1; + } + } + } else { + rank = -1; + score = 0; + for ( i = 0; i < level.numPlayingClients; i++ ) { + cl = &level.clients[ level.sortedClients[i] ]; + newScore = cl->ps.persistant[PERS_SCORE]; + if ( i == 0 || newScore != score ) { + rank = i; + // assume we aren't tied until the next client is checked + level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank; + } else { + // we are tied with the previous client + level.clients[ level.sortedClients[i-1] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; + level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; + } + score = newScore; + if ( g_gametype.integer == GT_SINGLE_PLAYER && level.numPlayingClients == 1 ) { + level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; + } + } + } + + // set the CS_SCORES1/2 configstrings, which will be visible to everyone + if ( g_gametype.integer >= GT_TEAM ) { + trap_SetConfigstring( CS_SCORES1, va("%i", level.teamScores[TEAM_RED] ) ); + trap_SetConfigstring( CS_SCORES2, va("%i", level.teamScores[TEAM_BLUE] ) ); + } else { + if ( level.numConnectedClients == 0 ) { + trap_SetConfigstring( CS_SCORES1, va("%i", SCORE_NOT_PRESENT) ); + trap_SetConfigstring( CS_SCORES2, va("%i", SCORE_NOT_PRESENT) ); + } else if ( level.numConnectedClients == 1 ) { + trap_SetConfigstring( CS_SCORES1, va("%i", level.clients[ level.sortedClients[0] ].ps.persistant[PERS_SCORE] ) ); + trap_SetConfigstring( CS_SCORES2, va("%i", SCORE_NOT_PRESENT) ); + } else { + trap_SetConfigstring( CS_SCORES1, va("%i", level.clients[ level.sortedClients[0] ].ps.persistant[PERS_SCORE] ) ); + trap_SetConfigstring( CS_SCORES2, va("%i", level.clients[ level.sortedClients[1] ].ps.persistant[PERS_SCORE] ) ); + } + } + + // see if it is time to end the level + CheckExitRules(); + + // if we are at the intermission, send the new info to everyone + if ( level.intermissiontime ) { + SendScoreboardMessageToAllClients(); + } +} + + +/* +======================================================================== + +MAP CHANGING + +======================================================================== +*/ + +/* +======================== +SendScoreboardMessageToAllClients + +Do this at BeginIntermission time and whenever ranks are recalculated +due to enters/exits/forced team changes +======================== +*/ +void SendScoreboardMessageToAllClients( void ) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[ i ].pers.connected == CON_CONNECTED ) { + DeathmatchScoreboardMessage( g_entities + i ); + } + } +} + +/* +======================== +MoveClientToIntermission + +When the intermission starts, this will be called for all players. +If a new client connects, this will be called after the spawn function. +======================== +*/ +void MoveClientToIntermission( gentity_t *ent ) { + // take out of follow mode if needed + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + StopFollowing( ent ); + } + + + // move to the spot + VectorCopy( level.intermission_origin, ent->s.origin ); + VectorCopy( level.intermission_origin, ent->client->ps.origin ); + VectorCopy (level.intermission_angle, ent->client->ps.viewangles); + ent->client->ps.pm_type = PM_INTERMISSION; + + // clean up powerup info + memset( ent->client->ps.powerups, 0, sizeof(ent->client->ps.powerups) ); + + ent->client->ps.eFlags = 0; + ent->s.eFlags = 0; + ent->s.eType = ET_GENERAL; + ent->s.modelindex = 0; + ent->s.loopSound = 0; + ent->s.event = 0; + ent->r.contents = 0; +} + +/* +================== +FindIntermissionPoint + +This is also used for spectator spawns +================== +*/ +void FindIntermissionPoint( void ) { + gentity_t *ent, *target; + vec3_t dir; + + // find the intermission spot + ent = G_Find (NULL, FOFS(classname), "info_player_intermission"); + if ( !ent ) { // the map creator forgot to put in an intermission point... + SelectSpawnPoint ( vec3_origin, level.intermission_origin, level.intermission_angle ); + } else { + VectorCopy (ent->s.origin, level.intermission_origin); + VectorCopy (ent->s.angles, level.intermission_angle); + // if it has a target, look towards it + if ( ent->target ) { + target = G_PickTarget( ent->target ); + if ( target ) { + VectorSubtract( target->s.origin, level.intermission_origin, dir ); + vectoangles( dir, level.intermission_angle ); + } + } + } + +} + +/* +================== +BeginIntermission +================== +*/ +void BeginIntermission( void ) { + int i; + gentity_t *client; + + if ( level.intermissiontime ) { + return; // already active + } + + // if in tournement mode, change the wins / losses + if ( g_gametype.integer == GT_TOURNAMENT ) { + AdjustTournamentScores(); + } + + level.intermissiontime = level.time; + FindIntermissionPoint(); + +#ifdef MISSIONPACK + if (g_singlePlayer.integer) { + trap_Cvar_Set("ui_singlePlayerActive", "0"); + UpdateTournamentInfo(); + } +#else + // if single player game + if ( g_gametype.integer == GT_SINGLE_PLAYER ) { + UpdateTournamentInfo(); + SpawnModelsOnVictoryPads(); + } +#endif + + // move all clients to the intermission point + for (i=0 ; i< level.maxclients ; i++) { + client = g_entities + i; + if (!client->inuse) + continue; + // respawn if dead + if (client->health <= 0) { + respawn(client); + } + MoveClientToIntermission( client ); + } + + // send the current scoring to all clients + SendScoreboardMessageToAllClients(); + +} + + +/* +============= +ExitLevel + +When the intermission has been exited, the server is either killed +or moved to a new level based on the "nextmap" cvar + +============= +*/ +void ExitLevel (void) { + int i; + gclient_t *cl; + + //bot interbreeding + BotInterbreedEndMatch(); + + // if we are running a tournement map, kick the loser to spectator status, + // which will automatically grab the next spectator and restart + if ( g_gametype.integer == GT_TOURNAMENT ) { + if ( !level.restarted ) { + RemoveTournamentLoser(); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); + level.restarted = qtrue; + level.changemap = NULL; + level.intermissiontime = 0; + } + return; + } + + + trap_SendConsoleCommand( EXEC_APPEND, "vstr nextmap\n" ); + level.changemap = NULL; + level.intermissiontime = 0; + + // reset all the scores so we don't enter the intermission again + level.teamScores[TEAM_RED] = 0; + level.teamScores[TEAM_BLUE] = 0; + for ( i=0 ; i< g_maxclients.integer ; i++ ) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + cl->ps.persistant[PERS_SCORE] = 0; + } + + // we need to do this here before chaning to CON_CONNECTING + G_WriteSessionData(); + + // change all client states to connecting, so the early players into the + // next level will know the others aren't done reconnecting + for (i=0 ; i< g_maxclients.integer ; i++) { + if ( level.clients[i].pers.connected == CON_CONNECTED ) { + level.clients[i].pers.connected = CON_CONNECTING; + } + } + +} + +/* +================= +G_LogPrintf + +Print to the logfile with a time stamp if it is open +================= +*/ +void QDECL G_LogPrintf( const char *fmt, ... ) { + va_list argptr; + char string[1024]; + int min, tens, sec; + + sec = level.time / 1000; + + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + Com_sprintf( string, sizeof(string), "%3i:%i%i ", min, tens, sec ); + + va_start( argptr, fmt ); + vsprintf( string +7 , fmt,argptr ); + va_end( argptr ); + + if ( g_dedicated.integer ) { + G_Printf( "%s", string + 7 ); + } + + if ( !level.logFile ) { + return; + } + + trap_FS_Write( string, strlen( string ), level.logFile ); +} + +/* +================ +LogExit + +Append information about this game to the log file +================ +*/ +void LogExit( const char *string ) { + int i, numSorted; + gclient_t *cl; +#ifdef MISSIONPACK // bk001205 + qboolean won = qtrue; +#endif + G_LogPrintf( "Exit: %s\n", string ); + + level.intermissionQueued = level.time; + + // this will keep the clients from playing any voice sounds + // that will get cut off when the queued intermission starts + trap_SetConfigstring( CS_INTERMISSION, "1" ); + + // don't send more than 32 scores (FIXME?) + numSorted = level.numConnectedClients; + if ( numSorted > 32 ) { + numSorted = 32; + } + + if ( g_gametype.integer >= GT_TEAM ) { + G_LogPrintf( "red:%i blue:%i\n", + level.teamScores[TEAM_RED], level.teamScores[TEAM_BLUE] ); + } + + for (i=0 ; i < numSorted ; i++) { + int ping; + + cl = &level.clients[level.sortedClients[i]]; + + if ( cl->sess.sessionTeam == TEAM_SPECTATOR ) { + continue; + } + if ( cl->pers.connected == CON_CONNECTING ) { + continue; + } + + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + G_LogPrintf( "score: %i ping: %i client: %i %s\n", cl->ps.persistant[PERS_SCORE], ping, level.sortedClients[i], cl->pers.netname ); +#ifdef MISSIONPACK + if (g_singlePlayer.integer && g_gametype.integer == GT_TOURNAMENT) { + if (g_entities[cl - level.clients].r.svFlags & SVF_BOT && cl->ps.persistant[PERS_RANK] == 0) { + won = qfalse; + } + } +#endif + + } + +#ifdef MISSIONPACK + if (g_singlePlayer.integer) { + if (g_gametype.integer >= GT_CTF) { + won = level.teamScores[TEAM_RED] > level.teamScores[TEAM_BLUE]; + } + trap_SendConsoleCommand( EXEC_APPEND, (won) ? "spWin\n" : "spLose\n" ); + } +#endif + + +} + + +/* +================= +CheckIntermissionExit + +The level will stay at the intermission for a minimum of 5 seconds +If all players wish to continue, the level will then exit. +If one or more players have not acknowledged the continue, the game will +wait 10 seconds before going on. +================= +*/ +void CheckIntermissionExit( void ) { + int ready, notReady; + int i; + gclient_t *cl; + int readyMask; + + if ( g_gametype.integer == GT_SINGLE_PLAYER ) { + return; + } + + // see which players are ready + ready = 0; + notReady = 0; + readyMask = 0; + for (i=0 ; i< g_maxclients.integer ; i++) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + if ( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) { + continue; + } + + if ( cl->readyToExit ) { + ready++; + if ( i < 16 ) { + readyMask |= 1 << i; + } + } else { + notReady++; + } + } + + // copy the readyMask to each player's stats so + // it can be displayed on the scoreboard + for (i=0 ; i< g_maxclients.integer ; i++) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + cl->ps.stats[STAT_CLIENTS_READY] = readyMask; + } + + // never exit in less than five seconds + if ( level.time < level.intermissiontime + 5000 ) { + return; + } + + // if nobody wants to go, clear timer + if ( !ready ) { + level.readyToExit = qfalse; + return; + } + + // if everyone wants to go, go now + if ( !notReady ) { + ExitLevel(); + return; + } + + // the first person to ready starts the ten second timeout + if ( !level.readyToExit ) { + level.readyToExit = qtrue; + level.exitTime = level.time; + } + + // if we have waited ten seconds since at least one player + // wanted to exit, go ahead + if ( level.time < level.exitTime + 10000 ) { + return; + } + + ExitLevel(); +} + +/* +============= +ScoreIsTied +============= +*/ +qboolean ScoreIsTied( void ) { + int a, b; + + if ( level.numPlayingClients < 2 ) { + return qfalse; + } + + if ( g_gametype.integer >= GT_TEAM ) { + return level.teamScores[TEAM_RED] == level.teamScores[TEAM_BLUE]; + } + + a = level.clients[level.sortedClients[0]].ps.persistant[PERS_SCORE]; + b = level.clients[level.sortedClients[1]].ps.persistant[PERS_SCORE]; + + return a == b; +} + +/* +================= +CheckExitRules + +There will be a delay between the time the exit is qualified for +and the time everyone is moved to the intermission spot, so you +can see the last frag. +================= +*/ +void CheckExitRules( void ) { + int i; + gclient_t *cl; + // if at the intermission, wait for all non-bots to + // signal ready, then go to next level + if ( level.intermissiontime ) { + CheckIntermissionExit (); + return; + } + + if ( level.intermissionQueued ) { +#ifdef MISSIONPACK + int time = (g_singlePlayer.integer) ? SP_INTERMISSION_DELAY_TIME : INTERMISSION_DELAY_TIME; + if ( level.time - level.intermissionQueued >= time ) { + level.intermissionQueued = 0; + BeginIntermission(); + } +#else + if ( level.time - level.intermissionQueued >= INTERMISSION_DELAY_TIME ) { + level.intermissionQueued = 0; + BeginIntermission(); + } +#endif + return; + } + + // check for sudden death + if ( ScoreIsTied() ) { + // always wait for sudden death + return; + } + + if ( g_timelimit.integer && !level.warmupTime ) { + if ( level.time - level.startTime >= g_timelimit.integer*60000 ) { + trap_SendServerCommand( -1, "print \"Timelimit hit.\n\""); + LogExit( "Timelimit hit." ); + return; + } + } + + if ( level.numPlayingClients < 2 ) { + return; + } + + if ( g_gametype.integer < GT_CTF && g_fraglimit.integer ) { + if ( level.teamScores[TEAM_RED] >= g_fraglimit.integer ) { + trap_SendServerCommand( -1, "print \"Red hit the fraglimit.\n\"" ); + LogExit( "Fraglimit hit." ); + return; + } + + if ( level.teamScores[TEAM_BLUE] >= g_fraglimit.integer ) { + trap_SendServerCommand( -1, "print \"Blue hit the fraglimit.\n\"" ); + LogExit( "Fraglimit hit." ); + return; + } + + for ( i=0 ; i< g_maxclients.integer ; i++ ) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + if ( cl->sess.sessionTeam != TEAM_FREE ) { + continue; + } + + if ( cl->ps.persistant[PERS_SCORE] >= g_fraglimit.integer ) { + LogExit( "Fraglimit hit." ); + trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " hit the fraglimit.\n\"", + cl->pers.netname ) ); + return; + } + } + } + + if ( g_gametype.integer >= GT_CTF && g_capturelimit.integer ) { + + if ( level.teamScores[TEAM_RED] >= g_capturelimit.integer ) { + trap_SendServerCommand( -1, "print \"Red hit the capturelimit.\n\"" ); + LogExit( "Capturelimit hit." ); + return; + } + + if ( level.teamScores[TEAM_BLUE] >= g_capturelimit.integer ) { + trap_SendServerCommand( -1, "print \"Blue hit the capturelimit.\n\"" ); + LogExit( "Capturelimit hit." ); + return; + } + } +} + + + +/* +======================================================================== + +FUNCTIONS CALLED EVERY FRAME + +======================================================================== +*/ + + +/* +============= +CheckTournament + +Once a frame, check for changes in tournement player state +============= +*/ +void CheckTournament( void ) { + // check because we run 3 game frames before calling Connect and/or ClientBegin + // for clients on a map_restart + if ( level.numPlayingClients == 0 ) { + return; + } + + if ( g_gametype.integer == GT_TOURNAMENT ) { + + // pull in a spectator if needed + if ( level.numPlayingClients < 2 ) { + AddTournamentPlayer(); + } + + // if we don't have two players, go back to "waiting for players" + if ( level.numPlayingClients != 2 ) { + if ( level.warmupTime != -1 ) { + level.warmupTime = -1; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + G_LogPrintf( "Warmup:\n" ); + } + return; + } + + if ( level.warmupTime == 0 ) { + return; + } + + // if the warmup is changed at the console, restart it + if ( g_warmup.modificationCount != level.warmupModificationCount ) { + level.warmupModificationCount = g_warmup.modificationCount; + level.warmupTime = -1; + } + + // if all players have arrived, start the countdown + if ( level.warmupTime < 0 ) { + if ( level.numPlayingClients == 2 ) { + // fudge by -1 to account for extra delays + level.warmupTime = level.time + ( g_warmup.integer - 1 ) * 1000; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + } + return; + } + + // if the warmup time has counted down, restart + if ( level.time > level.warmupTime ) { + level.warmupTime += 10000; + trap_Cvar_Set( "g_restarted", "1" ); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); + level.restarted = qtrue; + return; + } + } else if ( g_gametype.integer != GT_SINGLE_PLAYER && level.warmupTime != 0 ) { + int counts[TEAM_NUM_TEAMS]; + qboolean notEnough = qfalse; + + if ( g_gametype.integer > GT_TEAM ) { + counts[TEAM_BLUE] = TeamCount( -1, TEAM_BLUE ); + counts[TEAM_RED] = TeamCount( -1, TEAM_RED ); + + if (counts[TEAM_RED] < 1 || counts[TEAM_BLUE] < 1) { + notEnough = qtrue; + } + } else if ( level.numPlayingClients < 2 ) { + notEnough = qtrue; + } + + if ( notEnough ) { + if ( level.warmupTime != -1 ) { + level.warmupTime = -1; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + G_LogPrintf( "Warmup:\n" ); + } + return; // still waiting for team members + } + + if ( level.warmupTime == 0 ) { + return; + } + + // if the warmup is changed at the console, restart it + if ( g_warmup.modificationCount != level.warmupModificationCount ) { + level.warmupModificationCount = g_warmup.modificationCount; + level.warmupTime = -1; + } + + // if all players have arrived, start the countdown + if ( level.warmupTime < 0 ) { + // fudge by -1 to account for extra delays + level.warmupTime = level.time + ( g_warmup.integer - 1 ) * 1000; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + return; + } + + // if the warmup time has counted down, restart + if ( level.time > level.warmupTime ) { + level.warmupTime += 10000; + trap_Cvar_Set( "g_restarted", "1" ); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); + level.restarted = qtrue; + return; + } + } +} + + +/* +================== +CheckVote +================== +*/ +void CheckVote( void ) { + if ( level.voteExecuteTime && level.voteExecuteTime < level.time ) { + level.voteExecuteTime = 0; + trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.voteString ) ); + } + if ( !level.voteTime ) { + return; + } + if ( level.time - level.voteTime >= VOTE_TIME ) { + trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + } else { + // ATVI Q3 1.32 Patch #9, WNF + if ( level.voteYes > level.numVotingClients/2 ) { + // execute the command, then remove the vote + trap_SendServerCommand( -1, "print \"Vote passed.\n\"" ); + level.voteExecuteTime = level.time + 3000; + } else if ( level.voteNo >= level.numVotingClients/2 ) { + // same behavior as a timeout + trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + } else { + // still waiting for a majority + return; + } + } + level.voteTime = 0; + trap_SetConfigstring( CS_VOTE_TIME, "" ); + +} + +/* +================== +PrintTeam +================== +*/ +void PrintTeam(int team, char *message) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + trap_SendServerCommand( i, message ); + } +} + +/* +================== +SetLeader +================== +*/ +void SetLeader(int team, int client) { + int i; + + if ( level.clients[client].pers.connected == CON_DISCONNECTED ) { + PrintTeam(team, va("print \"%s is not connected\n\"", level.clients[client].pers.netname) ); + return; + } + if (level.clients[client].sess.sessionTeam != team) { + PrintTeam(team, va("print \"%s is not on the team anymore\n\"", level.clients[client].pers.netname) ); + return; + } + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + if (level.clients[i].sess.teamLeader) { + level.clients[i].sess.teamLeader = qfalse; + ClientUserinfoChanged(i); + } + } + level.clients[client].sess.teamLeader = qtrue; + ClientUserinfoChanged( client ); + PrintTeam(team, va("print \"%s is the new team leader\n\"", level.clients[client].pers.netname) ); +} + +/* +================== +CheckTeamLeader +================== +*/ +void CheckTeamLeader( int team ) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + if (level.clients[i].sess.teamLeader) + break; + } + if (i >= level.maxclients) { + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + if (!(g_entities[i].r.svFlags & SVF_BOT)) { + level.clients[i].sess.teamLeader = qtrue; + break; + } + } + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + level.clients[i].sess.teamLeader = qtrue; + break; + } + } +} + +/* +================== +CheckTeamVote +================== +*/ +void CheckTeamVote( int team ) { + int cs_offset; + + if ( team == TEAM_RED ) + cs_offset = 0; + else if ( team == TEAM_BLUE ) + cs_offset = 1; + else + return; + + if ( !level.teamVoteTime[cs_offset] ) { + return; + } + if ( level.time - level.teamVoteTime[cs_offset] >= VOTE_TIME ) { + trap_SendServerCommand( -1, "print \"Team vote failed.\n\"" ); + } else { + if ( level.teamVoteYes[cs_offset] > level.numteamVotingClients[cs_offset]/2 ) { + // execute the command, then remove the vote + trap_SendServerCommand( -1, "print \"Team vote passed.\n\"" ); + // + if ( !Q_strncmp( "leader", level.teamVoteString[cs_offset], 6) ) { + //set the team leader + SetLeader(team, atoi(level.teamVoteString[cs_offset] + 7)); + } + else { + trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.teamVoteString[cs_offset] ) ); + } + } else if ( level.teamVoteNo[cs_offset] >= level.numteamVotingClients[cs_offset]/2 ) { + // same behavior as a timeout + trap_SendServerCommand( -1, "print \"Team vote failed.\n\"" ); + } else { + // still waiting for a majority + return; + } + } + level.teamVoteTime[cs_offset] = 0; + trap_SetConfigstring( CS_TEAMVOTE_TIME + cs_offset, "" ); + +} + + +/* +================== +CheckCvars +================== +*/ +void CheckCvars( void ) { + static int lastMod = -1; + + if ( g_password.modificationCount != lastMod ) { + lastMod = g_password.modificationCount; + if ( *g_password.string && Q_stricmp( g_password.string, "none" ) ) { + trap_Cvar_Set( "g_needpass", "1" ); + } else { + trap_Cvar_Set( "g_needpass", "0" ); + } + } +} + +/* +============= +G_RunThink + +Runs thinking code for this frame if necessary +============= +*/ +void G_RunThink (gentity_t *ent) { + float thinktime; + + thinktime = ent->nextthink; + if (thinktime <= 0) { + return; + } + if (thinktime > level.time) { + return; + } + + ent->nextthink = 0; + if (!ent->think) { + G_Error ( "NULL ent->think"); + } + ent->think (ent); +} + +/* +================ +G_RunFrame + +Advances the non-player objects in the world +================ +*/ +void G_RunFrame( int levelTime ) { + int i; + gentity_t *ent; + int msec; +int start, end; + + // if we are waiting for the level to restart, do nothing + if ( level.restarted ) { + return; + } + + level.framenum++; + level.previousTime = level.time; + level.time = levelTime; + msec = level.time - level.previousTime; + + // get any cvar changes + G_UpdateCvars(); + + // demo playback: sync recorded player states and process spectator + if ( g_svDemoPlaying.integer ) { + gentity_t *specEnt = NULL; + + // 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++ ) { + gclient_t *cl = &level.clients[i]; + gentity_t *e = &g_entities[i]; + + // find the spectator -- use client-owned origin for PVS + if ( e->client && cl->pers.connected == CON_CONNECTED + && cl->sess.sessionTeam == TEAM_SPECTATOR ) { + // 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 ); + } + // process spectator buttons for follow mode switching + // (pers.cmd is updated by ClientThink which still runs) + { + int oldButtons = cl->buttons; + cl->oldbuttons = cl->buttons; + cl->buttons = cl->pers.cmd.buttons; + // attack cycles follow targets + if ( ( cl->buttons & BUTTON_ATTACK ) && !( oldButtons & BUTTON_ATTACK ) ) { + Cmd_FollowCycle_f( e, 1 ); + } + } + specEnt = 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; + } + } + + // evaluate trajectories and link all recorded entities for PVS. + // SVD_ReadFrame injected entityState_t but didn't link because + // the server doesn't have BG_EvaluateTrajectory. + // scan up to MAX_GENTITIES because recorded entities may exceed + // level.num_entities (which only counts game-module-spawned entities). + for ( i = 0; i < MAX_GENTITIES; i++ ) { + ent = &g_entities[i]; + if ( ent->s.eType == 0 && !ent->inuse ) { + continue; + } + // skip spectator slot + if ( ent->client && ent->client->pers.connected == CON_CONNECTED + && ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + continue; + } + // compute currentOrigin from trajectory + BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin ); + ent->r.linked = qtrue; + trap_LinkEntity( ent ); + } + + // update rankings so follow mode can cycle through players + CalculateRanks(); + + // run end-of-frame for spectator (handles follow mode PS copy) + if ( specEnt ) { + ClientEndFrame( specEnt ); + } + return; + } + + // + // go through all allocated objects + // + start = trap_Milliseconds(); + ent = &g_entities[0]; + for (i=0 ; iinuse ) { + continue; + } + + // clear events that are too old + if ( level.time - ent->eventTime > EVENT_VALID_MSEC ) { + if ( ent->s.event ) { + ent->s.event = 0; // &= EV_EVENT_BITS; + if ( ent->client ) { + ent->client->ps.externalEvent = 0; + // predicted events should never be set to zero + //ent->client->ps.events[0] = 0; + //ent->client->ps.events[1] = 0; + } + } + if ( ent->freeAfterEvent ) { + // tempEntities or dropped items completely go away after their event + G_FreeEntity( ent ); + continue; + } else if ( ent->unlinkAfterEvent ) { + // items that will respawn will hide themselves after their pickup event + ent->unlinkAfterEvent = qfalse; + trap_UnlinkEntity( ent ); + } + } + + // temporary entities don't think + if ( ent->freeAfterEvent ) { + continue; + } + + if ( !ent->r.linked && ent->neverFree ) { + continue; + } + + if ( ent->s.eType == ET_MISSILE ) { + G_RunMissile( ent ); + continue; + } + + if ( ent->s.eType == ET_ITEM || ent->physicsObject ) { + G_RunItem( ent ); + continue; + } + + if ( ent->s.eType == ET_MOVER ) { + G_RunMover( ent ); + continue; + } + + if ( i < MAX_CLIENTS ) { + G_RunClient( ent ); + continue; + } + + G_RunThink( ent ); + } +end = trap_Milliseconds(); + +start = trap_Milliseconds(); + // perform final fixups on the players + ent = &g_entities[0]; + for (i=0 ; i < level.maxclients ; i++, ent++ ) { + if ( ent->inuse ) { + ClientEndFrame( ent ); + } + } +end = trap_Milliseconds(); + + // see if it is time to do a tournement restart + CheckTournament(); + + // see if it is time to end the level + CheckExitRules(); + + // update to team status? + CheckTeamStatus(); + + // cancel vote if timed out + CheckVote(); + + // check team votes + CheckTeamVote( TEAM_RED ); + CheckTeamVote( TEAM_BLUE ); + + // for tracking changes + CheckCvars(); + + if (g_listEntity.integer) { + for (i = 0; i < MAX_GENTITIES; i++) { + G_Printf("%4i: %s\n", i, g_entities[i].classname); + } + trap_Cvar_Set("g_listEntity", "0"); + } +} diff --git a/code/game/g_session.c b/code/game/g_session.c index a8586f8..7de9374 100644 --- a/code/game/g_session.c +++ b/code/game/g_session.c @@ -1,199 +1,199 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ -// -#include "g_local.h" - - -/* -======================================================================= - - SESSION DATA - -Session data is the only data that stays persistant across level loads -and tournament restarts. -======================================================================= -*/ - -/* -================ -G_WriteClientSessionData - -Called on game shutdown -================ -*/ -void G_WriteClientSessionData( gclient_t *client ) { - const char *s; - const char *var; - - s = va("%i %i %i %i %i %i %i", - client->sess.sessionTeam, - client->sess.spectatorTime, - client->sess.spectatorState, - client->sess.spectatorClient, - client->sess.wins, - client->sess.losses, - client->sess.teamLeader - ); - - var = va( "session%i", client - level.clients ); - - trap_Cvar_Set( var, s ); -} - -/* -================ -G_ReadSessionData - -Called on a reconnect -================ -*/ -void G_ReadSessionData( gclient_t *client ) { - char s[MAX_STRING_CHARS]; - const char *var; - - // bk001205 - format - int teamLeader; - int spectatorState; - int sessionTeam; - - var = va( "session%i", client - level.clients ); - trap_Cvar_VariableStringBuffer( var, s, sizeof(s) ); - - sscanf( s, "%i %i %i %i %i %i %i", - &sessionTeam, // bk010221 - format - &client->sess.spectatorTime, - &spectatorState, // bk010221 - format - &client->sess.spectatorClient, - &client->sess.wins, - &client->sess.losses, - &teamLeader // bk010221 - format - ); - - // bk001205 - format issues - client->sess.sessionTeam = (team_t)sessionTeam; - client->sess.spectatorState = (spectatorState_t)spectatorState; - client->sess.teamLeader = (qboolean)teamLeader; -} - - -/* -================ -G_InitSessionData - -Called on a first-time connect -================ -*/ -void G_InitSessionData( gclient_t *client, char *userinfo ) { - clientSession_t *sess; - const char *value; - - sess = &client->sess; - - // initial team determination - if ( g_gametype.integer >= GT_TEAM ) { - if ( g_teamAutoJoin.integer ) { - sess->sessionTeam = PickTeam( -1 ); - BroadcastTeamChange( client, -1 ); - } else { - // always spawn as spectator in team games - sess->sessionTeam = TEAM_SPECTATOR; - } - } else { - value = Info_ValueForKey( userinfo, "team" ); - if ( value[0] == 's' ) { - // a willing spectator, not a waiting-in-line - sess->sessionTeam = TEAM_SPECTATOR; - } else { - switch ( g_gametype.integer ) { - default: - case GT_FFA: - case GT_SINGLE_PLAYER: - if ( g_maxGameClients.integer > 0 && - level.numNonSpectatorClients >= g_maxGameClients.integer ) { - sess->sessionTeam = TEAM_SPECTATOR; - } else { - sess->sessionTeam = TEAM_FREE; - } - break; - case GT_TOURNAMENT: - // if the game is full, go into a waiting mode - if ( level.numNonSpectatorClients >= 2 ) { - sess->sessionTeam = TEAM_SPECTATOR; - } else { - sess->sessionTeam = TEAM_FREE; - } - break; - } - } - } - - sess->spectatorState = SPECTATOR_FREE; - sess->spectatorTime = level.time; - - G_WriteClientSessionData( client ); -} - - -/* -================== -G_InitWorldSession - -================== -*/ -void G_InitWorldSession( void ) { - char s[MAX_STRING_CHARS]; - int gt; - - trap_Cvar_VariableStringBuffer( "session", s, sizeof(s) ); - gt = atoi( s ); - - // if the gametype changed since the last session, don't use any - // client sessions - if ( g_gametype.integer != gt ) { - level.newSession = qtrue; - G_Printf( "Gametype changed, clearing session data.\n" ); - } -} - -/* -================== -G_WriteSessionData - -================== -*/ -void G_WriteSessionData( void ) { - int i; - - // don't persist demo spectator sessions — the forced TEAM_SPECTATOR - // would carry over to the next normal game - if ( g_svDemoPlaying.integer ) { - return; - } - - trap_Cvar_Set( "session", va("%i", g_gametype.integer) ); - - for ( i = 0 ; i < level.maxclients ; i++ ) { - if ( level.clients[i].pers.connected == CON_CONNECTED ) { - G_WriteClientSessionData( &level.clients[i] ); - } - } -} +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ +// +#include "g_local.h" + + +/* +======================================================================= + + SESSION DATA + +Session data is the only data that stays persistant across level loads +and tournament restarts. +======================================================================= +*/ + +/* +================ +G_WriteClientSessionData + +Called on game shutdown +================ +*/ +void G_WriteClientSessionData( gclient_t *client ) { + const char *s; + const char *var; + + s = va("%i %i %i %i %i %i %i", + client->sess.sessionTeam, + client->sess.spectatorTime, + client->sess.spectatorState, + client->sess.spectatorClient, + client->sess.wins, + client->sess.losses, + client->sess.teamLeader + ); + + var = va( "session%i", client - level.clients ); + + trap_Cvar_Set( var, s ); +} + +/* +================ +G_ReadSessionData + +Called on a reconnect +================ +*/ +void G_ReadSessionData( gclient_t *client ) { + char s[MAX_STRING_CHARS]; + const char *var; + + // bk001205 - format + int teamLeader; + int spectatorState; + int sessionTeam; + + var = va( "session%i", client - level.clients ); + trap_Cvar_VariableStringBuffer( var, s, sizeof(s) ); + + sscanf( s, "%i %i %i %i %i %i %i", + &sessionTeam, // bk010221 - format + &client->sess.spectatorTime, + &spectatorState, // bk010221 - format + &client->sess.spectatorClient, + &client->sess.wins, + &client->sess.losses, + &teamLeader // bk010221 - format + ); + + // bk001205 - format issues + client->sess.sessionTeam = (team_t)sessionTeam; + client->sess.spectatorState = (spectatorState_t)spectatorState; + client->sess.teamLeader = (qboolean)teamLeader; +} + + +/* +================ +G_InitSessionData + +Called on a first-time connect +================ +*/ +void G_InitSessionData( gclient_t *client, char *userinfo ) { + clientSession_t *sess; + const char *value; + + sess = &client->sess; + + // initial team determination + if ( g_gametype.integer >= GT_TEAM ) { + if ( g_teamAutoJoin.integer ) { + sess->sessionTeam = PickTeam( -1 ); + BroadcastTeamChange( client, -1 ); + } else { + // always spawn as spectator in team games + sess->sessionTeam = TEAM_SPECTATOR; + } + } else { + value = Info_ValueForKey( userinfo, "team" ); + if ( value[0] == 's' ) { + // a willing spectator, not a waiting-in-line + sess->sessionTeam = TEAM_SPECTATOR; + } else { + switch ( g_gametype.integer ) { + default: + case GT_FFA: + case GT_SINGLE_PLAYER: + if ( g_maxGameClients.integer > 0 && + level.numNonSpectatorClients >= g_maxGameClients.integer ) { + sess->sessionTeam = TEAM_SPECTATOR; + } else { + sess->sessionTeam = TEAM_FREE; + } + break; + case GT_TOURNAMENT: + // if the game is full, go into a waiting mode + if ( level.numNonSpectatorClients >= 2 ) { + sess->sessionTeam = TEAM_SPECTATOR; + } else { + sess->sessionTeam = TEAM_FREE; + } + break; + } + } + } + + sess->spectatorState = SPECTATOR_FREE; + sess->spectatorTime = level.time; + + G_WriteClientSessionData( client ); +} + + +/* +================== +G_InitWorldSession + +================== +*/ +void G_InitWorldSession( void ) { + char s[MAX_STRING_CHARS]; + int gt; + + trap_Cvar_VariableStringBuffer( "session", s, sizeof(s) ); + gt = atoi( s ); + + // if the gametype changed since the last session, don't use any + // client sessions + if ( g_gametype.integer != gt ) { + level.newSession = qtrue; + G_Printf( "Gametype changed, clearing session data.\n" ); + } +} + +/* +================== +G_WriteSessionData + +================== +*/ +void G_WriteSessionData( void ) { + int i; + + // don't persist demo spectator sessions -- the forced TEAM_SPECTATOR + // would carry over to the next normal game + if ( g_svDemoPlaying.integer ) { + return; + } + + trap_Cvar_Set( "session", va("%i", g_gametype.integer) ); + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected == CON_CONNECTED ) { + G_WriteClientSessionData( &level.clients[i] ); + } + } +} diff --git a/code/server/sv_init.c b/code/server/sv_init.c index 8448195..c59568c 100644 --- a/code/server/sv_init.c +++ b/code/server/sv_init.c @@ -1,724 +1,724 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ - -#include "server.h" - -/* -=============== -SV_SetConfigstring - -=============== -*/ -void SV_SetConfigstring (int index, const char *val) { - int len, i; - int maxChunkSize = MAX_STRING_CHARS - 24; - client_t *client; - - if ( index < 0 || index >= MAX_CONFIGSTRINGS ) { - Com_Error (ERR_DROP, "SV_SetConfigstring: bad index %i\n", index); - } - - if ( !val ) { - val = ""; - } - - // don't bother broadcasting an update if no change - if ( !strcmp( val, sv.configstrings[ index ] ) ) { - return; - } - - // change the string in sv - Z_Free( sv.configstrings[index] ); - sv.configstrings[index] = CopyString( val ); - - // send it to all the clients if we aren't - // spawning a new server - if ( sv.state == SS_GAME || sv.restarting ) { - - // send the data to all relevent clients - for (i = 0, client = svs.clients; i < sv_maxclients->integer ; i++, client++) { - if ( client->state < CS_PRIMED ) { - continue; - } - // do not always send server info to all clients - if ( index == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO) ) { - continue; - } - - len = strlen( val ); - if( len >= maxChunkSize ) { - int sent = 0; - int remaining = len; - char *cmd; - char buf[MAX_STRING_CHARS]; - - while (remaining > 0 ) { - if ( sent == 0 ) { - cmd = "bcs0"; - } - else if( remaining < maxChunkSize ) { - cmd = "bcs2"; - } - else { - cmd = "bcs1"; - } - Q_strncpyz( buf, &val[sent], maxChunkSize ); - - SV_SendServerCommand( client, "%s %i \"%s\"\n", cmd, index, buf ); - - sent += (maxChunkSize - 1); - remaining -= (maxChunkSize - 1); - } - } else { - // standard cs, just send it - SV_SendServerCommand( client, "cs %i \"%s\"\n", index, val ); - } - } - } -} - - - -/* -=============== -SV_GetConfigstring - -=============== -*/ -void SV_GetConfigstring( int index, char *buffer, int bufferSize ) { - if ( bufferSize < 1 ) { - Com_Error( ERR_DROP, "SV_GetConfigstring: bufferSize == %i", bufferSize ); - } - if ( index < 0 || index >= MAX_CONFIGSTRINGS ) { - Com_Error (ERR_DROP, "SV_GetConfigstring: bad index %i\n", index); - } - if ( !sv.configstrings[index] ) { - buffer[0] = 0; - return; - } - - Q_strncpyz( buffer, sv.configstrings[index], bufferSize ); -} - - -/* -=============== -SV_SetUserinfo - -=============== -*/ -void SV_SetUserinfo( int index, const char *val ) { - if ( index < 0 || index >= sv_maxclients->integer ) { - Com_Error (ERR_DROP, "SV_SetUserinfo: bad index %i\n", index); - } - - if ( !val ) { - val = ""; - } - - Q_strncpyz( svs.clients[index].userinfo, val, sizeof( svs.clients[ index ].userinfo ) ); - Q_strncpyz( svs.clients[index].name, Info_ValueForKey( val, "name" ), sizeof(svs.clients[index].name) ); -} - - - -/* -=============== -SV_GetUserinfo - -=============== -*/ -void SV_GetUserinfo( int index, char *buffer, int bufferSize ) { - if ( bufferSize < 1 ) { - Com_Error( ERR_DROP, "SV_GetUserinfo: bufferSize == %i", bufferSize ); - } - if ( index < 0 || index >= sv_maxclients->integer ) { - Com_Error (ERR_DROP, "SV_GetUserinfo: bad index %i\n", index); - } - Q_strncpyz( buffer, svs.clients[ index ].userinfo, bufferSize ); -} - - -/* -================ -SV_CreateBaseline - -Entity baselines are used to compress non-delta messages -to the clients -- only the fields that differ from the -baseline will be transmitted -================ -*/ -void SV_CreateBaseline( void ) { - sharedEntity_t *svent; - int entnum; - - for ( entnum = 1; entnum < sv.num_entities ; entnum++ ) { - svent = SV_GentityNum(entnum); - if (!svent->r.linked) { - continue; - } - svent->s.number = entnum; - - // - // take current state as baseline - // - sv.svEntities[entnum].baseline = svent->s; - } -} - - -/* -=============== -SV_BoundMaxClients - -=============== -*/ -void SV_BoundMaxClients( int minimum ) { - // get the current maxclients value - Cvar_Get( "sv_maxclients", "8", 0 ); - - sv_maxclients->modified = qfalse; - - if ( sv_maxclients->integer < minimum ) { - Cvar_Set( "sv_maxclients", va("%i", minimum) ); - } else if ( sv_maxclients->integer > MAX_CLIENTS ) { - Cvar_Set( "sv_maxclients", va("%i", MAX_CLIENTS) ); - } -} - - -/* -=============== -SV_Startup - -Called when a host starts a map when it wasn't running -one before. Successive map or map_restart commands will -NOT cause this to be called, unless the game is exited to -the menu system first. -=============== -*/ -void SV_Startup( void ) { - if ( svs.initialized ) { - Com_Error( ERR_FATAL, "SV_Startup: svs.initialized" ); - } - SV_BoundMaxClients( 1 ); - - svs.clients = Z_Malloc (sizeof(client_t) * sv_maxclients->integer ); - if ( com_dedicated->integer ) { - svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * 64; - } else { - // we don't need nearly as many when playing locally - svs.numSnapshotEntities = sv_maxclients->integer * 4 * 64; - } - svs.initialized = qtrue; - - Cvar_Set( "sv_running", "1" ); -} - - -/* -================== -SV_ChangeMaxClients -================== -*/ -void SV_ChangeMaxClients( void ) { - int oldMaxClients; - int i; - client_t *oldClients; - int count; - - // get the highest client number in use - count = 0; - for ( i = 0 ; i < sv_maxclients->integer ; i++ ) { - if ( svs.clients[i].state >= CS_CONNECTED ) { - if (i > count) - count = i; - } - } - count++; - - oldMaxClients = sv_maxclients->integer; - // never go below the highest client number in use - SV_BoundMaxClients( count ); - // if still the same - if ( sv_maxclients->integer == oldMaxClients ) { - return; - } - - oldClients = Hunk_AllocateTempMemory( count * sizeof(client_t) ); - // copy the clients to hunk memory - for ( i = 0 ; i < count ; i++ ) { - if ( svs.clients[i].state >= CS_CONNECTED ) { - oldClients[i] = svs.clients[i]; - } - else { - Com_Memset(&oldClients[i], 0, sizeof(client_t)); - } - } - - // free old clients arrays - Z_Free( svs.clients ); - - // allocate new clients - svs.clients = Z_Malloc ( sv_maxclients->integer * sizeof(client_t) ); - Com_Memset( svs.clients, 0, sv_maxclients->integer * sizeof(client_t) ); - - // copy the clients over - for ( i = 0 ; i < count ; i++ ) { - if ( oldClients[i].state >= CS_CONNECTED ) { - svs.clients[i] = oldClients[i]; - } - } - - // free the old clients on the hunk - Hunk_FreeTempMemory( oldClients ); - - // allocate new snapshot entities - if ( com_dedicated->integer ) { - svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * 64; - } else { - // we don't need nearly as many when playing locally - svs.numSnapshotEntities = sv_maxclients->integer * 4 * 64; - } -} - -/* -================ -SV_ClearServer -================ -*/ -void SV_ClearServer(void) { - int i; - - for ( i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) { - if ( sv.configstrings[i] ) { - Z_Free( sv.configstrings[i] ); - } - } - Com_Memset (&sv, 0, sizeof(sv)); -} - -/* -================ -SV_TouchCGame - - touch the cgame.vm so that a pure client can load it if it's in a seperate pk3 -================ -*/ -void SV_TouchCGame(void) { - fileHandle_t f; - char filename[MAX_QPATH]; - - Com_sprintf( filename, sizeof(filename), "vm/%s.qvm", "cgame" ); - FS_FOpenFileRead( filename, &f, qfalse ); - if ( f ) { - FS_FCloseFile( f ); - } -} - -/* -================ -SV_SpawnServer - -Change the server to a new map, taking all connected -clients along with it. -This is NOT called for map_restart -================ -*/ -void SV_SpawnServer( char *server, qboolean killBots ) { - int i; - int checksum; - qboolean isBot; - char systemInfo[16384]; - const char *p; - - // stop any active demo recording/playback (one demo = one map). - // SVD_IsStarting() returns true when SVD_Play_f is calling devmap - // internally — don't stop our own playback. - if ( SVD_IsRecording() ) { - Com_Printf( "Map change — stopping demo recording.\n" ); - SVD_StopRecord_f(); - } - if ( SVD_IsPlaying() && !SVD_IsStarting() ) { - Com_Printf( "Map change — stopping demo playback.\n" ); - SVD_CleanupPlayback(); - } - - // shut down the existing game if it is running - SV_ShutdownGameProgs(); - - Com_Printf ("------ Server Initialization ------\n"); - Com_Printf ("Server: %s\n",server); - - // if not running a dedicated server CL_MapLoading will connect the client to the server - // also print some status stuff - CL_MapLoading(); - - // make sure all the client stuff is unloaded - CL_ShutdownAll(); - - // clear the whole hunk because we're (re)loading the server - Hunk_Clear(); - - // clear collision map data - CM_ClearMap(); - - // init client structures and svs.numSnapshotEntities - if ( !Cvar_VariableValue("sv_running") ) { - SV_Startup(); - } else { - // check for maxclients change - if ( sv_maxclients->modified ) { - SV_ChangeMaxClients(); - } - } - - // clear pak references - FS_ClearPakReferences(0); - - // allocate the snapshot entities on the hunk - svs.snapshotEntities = Hunk_Alloc( sizeof(entityState_t)*svs.numSnapshotEntities, h_high ); - svs.nextSnapshotEntities = 0; - - // toggle the server bit so clients can detect that a - // server has changed - svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; - - // set nextmap to the same map, but it may be overriden - // by the game startup or another console command - Cvar_Set( "nextmap", "map_restart 0"); -// Cvar_Set( "nextmap", va("map %s", server) ); - - // wipe the entire per-level structure - SV_ClearServer(); - for ( i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) { - sv.configstrings[i] = CopyString(""); - } - - // make sure we are not paused - Cvar_Set("cl_paused", "0"); - - // get a new checksum feed and restart the file system - srand(Com_Milliseconds()); - sv.checksumFeed = ( ((int) rand() << 16) ^ rand() ) ^ Com_Milliseconds(); - FS_Restart( sv.checksumFeed ); - - CM_LoadMap( va("maps/%s.bsp", server), qfalse, &checksum ); - - // set serverinfo visible name - Cvar_Set( "mapname", server ); - - Cvar_Set( "sv_mapChecksum", va("%i",checksum) ); - - // serverid should be different each time - sv.serverId = com_frameTime; - sv.restartedServerId = sv.serverId; // I suppose the init here is just to be safe - sv.checksumFeedServerId = sv.serverId; - Cvar_Set( "sv_serverid", va("%i", sv.serverId ) ); - - // clear physics interaction links - SV_ClearWorld (); - - // media configstring setting should be done during - // the loading stage, so connected clients don't have - // to load during actual gameplay - sv.state = SS_LOADING; - - // load and spawn all other entities - SV_InitGameProgs(); - - // don't allow a map_restart if game is modified - sv_gametype->modified = qfalse; - - // run a few frames to allow everything to settle - for ( i = 0 ;i < 3 ; i++ ) { - VM_Call( gvm, GAME_RUN_FRAME, svs.time ); - SV_BotFrame( svs.time ); - svs.time += 100; - } - - // create a baseline for more efficient communications - SV_CreateBaseline (); - - for (i=0 ; iinteger ; i++) { - // send the new gamestate to all connected clients - if (svs.clients[i].state >= CS_CONNECTED) { - char *denied; - - if ( svs.clients[i].netchan.remoteAddress.type == NA_BOT ) { - if ( killBots ) { - SV_DropClient( &svs.clients[i], "" ); - continue; - } - isBot = qtrue; - } - else { - isBot = qfalse; - } - - // connect the client again - denied = VM_ExplicitArgPtr( gvm, VM_Call( gvm, GAME_CLIENT_CONNECT, i, qfalse, isBot ) ); // firstTime = qfalse - if ( denied ) { - // this generally shouldn't happen, because the client - // was connected before the level change - SV_DropClient( &svs.clients[i], denied ); - } else { - if( !isBot ) { - // when we get the next packet from a connected client, - // the new gamestate will be sent - svs.clients[i].state = CS_CONNECTED; - } - else { - client_t *client; - sharedEntity_t *ent; - - client = &svs.clients[i]; - client->state = CS_ACTIVE; - ent = SV_GentityNum( i ); - ent->s.number = i; - client->gentity = ent; - - client->deltaMessage = -1; - client->nextSnapshotTime = svs.time; // generate a snapshot immediately - - VM_Call( gvm, GAME_CLIENT_BEGIN, i ); - } - } - } - } - - // run another frame to allow things to look at all the players - VM_Call( gvm, GAME_RUN_FRAME, svs.time ); - SV_BotFrame( svs.time ); - svs.time += 100; - - if ( sv_pure->integer ) { - // the server sends these to the clients so they will only - // load pk3s also loaded at the server - p = FS_LoadedPakChecksums(); - Cvar_Set( "sv_paks", p ); - if (strlen(p) == 0) { - Com_Printf( "WARNING: sv_pure set but no PK3 files loaded\n" ); - } - p = FS_LoadedPakNames(); - Cvar_Set( "sv_pakNames", p ); - - // if a dedicated pure server we need to touch the cgame because it could be in a - // seperate pk3 file and the client will need to load the latest cgame.qvm - if ( com_dedicated->integer ) { - SV_TouchCGame(); - } - } - else { - Cvar_Set( "sv_paks", "" ); - Cvar_Set( "sv_pakNames", "" ); - } - // the server sends these to the clients so they can figure - // out which pk3s should be auto-downloaded - p = FS_ReferencedPakChecksums(); - Cvar_Set( "sv_referencedPaks", p ); - p = FS_ReferencedPakNames(); - Cvar_Set( "sv_referencedPakNames", p ); - - // save systeminfo and serverinfo strings - Q_strncpyz( systemInfo, Cvar_InfoString_Big( CVAR_SYSTEMINFO ), sizeof( systemInfo ) ); - cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; - SV_SetConfigstring( CS_SYSTEMINFO, systemInfo ); - - SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) ); - cvar_modifiedFlags &= ~CVAR_SERVERINFO; - - // any media configstring setting now should issue a warning - // and any configstring changes should be reliably transmitted - // to all clients - sv.state = SS_GAME; - - // send a heartbeat now so the master will get up to date info - SV_Heartbeat_f(); - - Hunk_SetMark(); - - Com_Printf ("-----------------------------------\n"); - - // auto-record demo if enabled - SVD_AutoRecord(); -} - -/* -=============== -SV_Init - -Only called at main exe startup, not for each game -=============== -*/ -void SV_BotInitBotLib(void); - -void SV_Init (void) { - SV_AddOperatorCommands (); - - // serverinfo vars - Cvar_Get ("dmflags", "0", CVAR_SERVERINFO); - Cvar_Get ("fraglimit", "20", CVAR_SERVERINFO); - Cvar_Get ("timelimit", "0", CVAR_SERVERINFO); - sv_gametype = Cvar_Get ("g_gametype", "0", CVAR_SERVERINFO | CVAR_LATCH ); - Cvar_Get ("sv_keywords", "", CVAR_SERVERINFO); - Cvar_Get ("protocol", va("%i", PROTOCOL_VERSION), CVAR_SERVERINFO | CVAR_ROM); - sv_mapname = Cvar_Get ("mapname", "nomap", CVAR_SERVERINFO | CVAR_ROM); - sv_privateClients = Cvar_Get ("sv_privateClients", "0", CVAR_SERVERINFO); - sv_hostname = Cvar_Get ("sv_hostname", "noname", CVAR_SERVERINFO | CVAR_ARCHIVE ); - sv_maxclients = Cvar_Get ("sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH); - - sv_maxRate = Cvar_Get ("sv_maxRate", "0", CVAR_ARCHIVE | CVAR_SERVERINFO ); - sv_minPing = Cvar_Get ("sv_minPing", "0", CVAR_ARCHIVE | CVAR_SERVERINFO ); - sv_maxPing = Cvar_Get ("sv_maxPing", "0", CVAR_ARCHIVE | CVAR_SERVERINFO ); - sv_floodProtect = Cvar_Get ("sv_floodProtect", "1", CVAR_ARCHIVE | CVAR_SERVERINFO ); - - // systeminfo - Cvar_Get ("sv_cheats", "1", CVAR_SYSTEMINFO | CVAR_ROM ); - sv_serverid = Cvar_Get ("sv_serverid", "0", CVAR_SYSTEMINFO | CVAR_ROM ); -#ifndef DLL_ONLY // bk010216 - for DLL-only servers - sv_pure = Cvar_Get ("sv_pure", "1", CVAR_SYSTEMINFO ); -#else - sv_pure = Cvar_Get ("sv_pure", "0", CVAR_SYSTEMINFO | CVAR_INIT | CVAR_ROM ); -#endif - Cvar_Get ("sv_paks", "", CVAR_SYSTEMINFO | CVAR_ROM ); - Cvar_Get ("sv_pakNames", "", CVAR_SYSTEMINFO | CVAR_ROM ); - Cvar_Get ("sv_referencedPaks", "", CVAR_SYSTEMINFO | CVAR_ROM ); - Cvar_Get ("sv_referencedPakNames", "", CVAR_SYSTEMINFO | CVAR_ROM ); - - // server vars - sv_rconPassword = Cvar_Get ("rconPassword", "", CVAR_TEMP ); - sv_privatePassword = Cvar_Get ("sv_privatePassword", "", CVAR_TEMP ); - sv_fps = Cvar_Get ("sv_fps", "20", CVAR_TEMP ); - sv_timeout = Cvar_Get ("sv_timeout", "200", CVAR_TEMP ); - sv_zombietime = Cvar_Get ("sv_zombietime", "2", CVAR_TEMP ); - Cvar_Get ("nextmap", "", CVAR_TEMP ); - - sv_allowDownload = Cvar_Get ("sv_allowDownload", "0", CVAR_SERVERINFO); - sv_master[0] = Cvar_Get ("sv_master1", MASTER_SERVER_NAME, 0 ); - sv_master[1] = Cvar_Get ("sv_master2", "", CVAR_ARCHIVE ); - sv_master[2] = Cvar_Get ("sv_master3", "", CVAR_ARCHIVE ); - sv_master[3] = Cvar_Get ("sv_master4", "", CVAR_ARCHIVE ); - sv_master[4] = Cvar_Get ("sv_master5", "", CVAR_ARCHIVE ); - sv_reconnectlimit = Cvar_Get ("sv_reconnectlimit", "3", 0); - sv_showloss = Cvar_Get ("sv_showloss", "0", 0); - sv_padPackets = Cvar_Get ("sv_padPackets", "0", 0); - sv_killserver = Cvar_Get ("sv_killserver", "0", 0); - sv_mapChecksum = Cvar_Get ("sv_mapChecksum", "", CVAR_ROM); - sv_lanForceRate = Cvar_Get ("sv_lanForceRate", "1", CVAR_ARCHIVE ); - sv_strictAuth = Cvar_Get ("sv_strictAuth", "1", CVAR_ARCHIVE ); - - // server-side demo settings - Cvar_Get ("svdemo_autorecord", "0", CVAR_ARCHIVE); - Cvar_Get ("svdemo_pauseEmpty", "1", CVAR_ARCHIVE); - Cvar_Get ("svdemo_keyframeInterval", "5", CVAR_ARCHIVE); // seconds, 0 = disabled - - // initialize bot cvars so they are listed and can be set before loading the botlib - SV_BotInitCvars(); - - // init the botlib here because we need the pre-compiler in the UI - SV_BotInitBotLib(); -} - - -/* -================== -SV_FinalMessage - -Used by SV_Shutdown to send a final message to all -connected clients before the server goes down. The messages are sent immediately, -not just stuck on the outgoing message list, because the server is going -to totally exit after returning from this function. -================== -*/ -void SV_FinalMessage( char *message ) { - int i, j; - client_t *cl; - - // send it twice, ignoring rate - for ( j = 0 ; j < 2 ; j++ ) { - for (i=0, cl = svs.clients ; i < sv_maxclients->integer ; i++, cl++) { - if (cl->state >= CS_CONNECTED) { - // don't send a disconnect to a local client - if ( cl->netchan.remoteAddress.type != NA_LOOPBACK ) { - SV_SendServerCommand( cl, "print \"%s\"", message ); - SV_SendServerCommand( cl, "disconnect" ); - } - // force a snapshot to be sent - cl->nextSnapshotTime = -1; - SV_SendClientSnapshot( cl ); - } - } - } -} - - -/* -================ -SV_Shutdown - -Called when each game quits, -before Sys_Quit or Sys_Error -================ -*/ -void SV_Shutdown( char *finalmsg ) { - if ( !com_sv_running || !com_sv_running->integer ) { - return; - } - - Com_Printf( "----- Server Shutdown -----\n" ); - - // clean up any active demo recording/playback. - // skip if SVD_Play_f is calling SV_Shutdown internally. - if ( SVD_IsRecording() ) { - SVD_StopRecord_f(); - } - if ( SVD_IsPlaying() && !SVD_IsStarting() ) { - SVD_CleanupPlayback(); - } - - if ( svs.clients && !com_errorEntered ) { - SV_FinalMessage( finalmsg ); - } - - SV_RemoveOperatorCommands(); - SV_MasterShutdown(); - SV_ShutdownGameProgs(); - - // free current level - SV_ClearServer(); - - // free server static data - if ( svs.clients ) { - Z_Free( svs.clients ); - } - Com_Memset( &svs, 0, sizeof( svs ) ); - - Cvar_Set( "sv_running", "0" ); - Cvar_Set("ui_singlePlayerActive", "0"); - - Com_Printf( "---------------------------\n" ); - - // disconnect any local clients - CL_Disconnect( qfalse ); -} - +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "server.h" + +/* +=============== +SV_SetConfigstring + +=============== +*/ +void SV_SetConfigstring (int index, const char *val) { + int len, i; + int maxChunkSize = MAX_STRING_CHARS - 24; + client_t *client; + + if ( index < 0 || index >= MAX_CONFIGSTRINGS ) { + Com_Error (ERR_DROP, "SV_SetConfigstring: bad index %i\n", index); + } + + if ( !val ) { + val = ""; + } + + // don't bother broadcasting an update if no change + if ( !strcmp( val, sv.configstrings[ index ] ) ) { + return; + } + + // change the string in sv + Z_Free( sv.configstrings[index] ); + sv.configstrings[index] = CopyString( val ); + + // send it to all the clients if we aren't + // spawning a new server + if ( sv.state == SS_GAME || sv.restarting ) { + + // send the data to all relevent clients + for (i = 0, client = svs.clients; i < sv_maxclients->integer ; i++, client++) { + if ( client->state < CS_PRIMED ) { + continue; + } + // do not always send server info to all clients + if ( index == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO) ) { + continue; + } + + len = strlen( val ); + if( len >= maxChunkSize ) { + int sent = 0; + int remaining = len; + char *cmd; + char buf[MAX_STRING_CHARS]; + + while (remaining > 0 ) { + if ( sent == 0 ) { + cmd = "bcs0"; + } + else if( remaining < maxChunkSize ) { + cmd = "bcs2"; + } + else { + cmd = "bcs1"; + } + Q_strncpyz( buf, &val[sent], maxChunkSize ); + + SV_SendServerCommand( client, "%s %i \"%s\"\n", cmd, index, buf ); + + sent += (maxChunkSize - 1); + remaining -= (maxChunkSize - 1); + } + } else { + // standard cs, just send it + SV_SendServerCommand( client, "cs %i \"%s\"\n", index, val ); + } + } + } +} + + + +/* +=============== +SV_GetConfigstring + +=============== +*/ +void SV_GetConfigstring( int index, char *buffer, int bufferSize ) { + if ( bufferSize < 1 ) { + Com_Error( ERR_DROP, "SV_GetConfigstring: bufferSize == %i", bufferSize ); + } + if ( index < 0 || index >= MAX_CONFIGSTRINGS ) { + Com_Error (ERR_DROP, "SV_GetConfigstring: bad index %i\n", index); + } + if ( !sv.configstrings[index] ) { + buffer[0] = 0; + return; + } + + Q_strncpyz( buffer, sv.configstrings[index], bufferSize ); +} + + +/* +=============== +SV_SetUserinfo + +=============== +*/ +void SV_SetUserinfo( int index, const char *val ) { + if ( index < 0 || index >= sv_maxclients->integer ) { + Com_Error (ERR_DROP, "SV_SetUserinfo: bad index %i\n", index); + } + + if ( !val ) { + val = ""; + } + + Q_strncpyz( svs.clients[index].userinfo, val, sizeof( svs.clients[ index ].userinfo ) ); + Q_strncpyz( svs.clients[index].name, Info_ValueForKey( val, "name" ), sizeof(svs.clients[index].name) ); +} + + + +/* +=============== +SV_GetUserinfo + +=============== +*/ +void SV_GetUserinfo( int index, char *buffer, int bufferSize ) { + if ( bufferSize < 1 ) { + Com_Error( ERR_DROP, "SV_GetUserinfo: bufferSize == %i", bufferSize ); + } + if ( index < 0 || index >= sv_maxclients->integer ) { + Com_Error (ERR_DROP, "SV_GetUserinfo: bad index %i\n", index); + } + Q_strncpyz( buffer, svs.clients[ index ].userinfo, bufferSize ); +} + + +/* +================ +SV_CreateBaseline + +Entity baselines are used to compress non-delta messages +to the clients -- only the fields that differ from the +baseline will be transmitted +================ +*/ +void SV_CreateBaseline( void ) { + sharedEntity_t *svent; + int entnum; + + for ( entnum = 1; entnum < sv.num_entities ; entnum++ ) { + svent = SV_GentityNum(entnum); + if (!svent->r.linked) { + continue; + } + svent->s.number = entnum; + + // + // take current state as baseline + // + sv.svEntities[entnum].baseline = svent->s; + } +} + + +/* +=============== +SV_BoundMaxClients + +=============== +*/ +void SV_BoundMaxClients( int minimum ) { + // get the current maxclients value + Cvar_Get( "sv_maxclients", "8", 0 ); + + sv_maxclients->modified = qfalse; + + if ( sv_maxclients->integer < minimum ) { + Cvar_Set( "sv_maxclients", va("%i", minimum) ); + } else if ( sv_maxclients->integer > MAX_CLIENTS ) { + Cvar_Set( "sv_maxclients", va("%i", MAX_CLIENTS) ); + } +} + + +/* +=============== +SV_Startup + +Called when a host starts a map when it wasn't running +one before. Successive map or map_restart commands will +NOT cause this to be called, unless the game is exited to +the menu system first. +=============== +*/ +void SV_Startup( void ) { + if ( svs.initialized ) { + Com_Error( ERR_FATAL, "SV_Startup: svs.initialized" ); + } + SV_BoundMaxClients( 1 ); + + svs.clients = Z_Malloc (sizeof(client_t) * sv_maxclients->integer ); + if ( com_dedicated->integer ) { + svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * 64; + } else { + // we don't need nearly as many when playing locally + svs.numSnapshotEntities = sv_maxclients->integer * 4 * 64; + } + svs.initialized = qtrue; + + Cvar_Set( "sv_running", "1" ); +} + + +/* +================== +SV_ChangeMaxClients +================== +*/ +void SV_ChangeMaxClients( void ) { + int oldMaxClients; + int i; + client_t *oldClients; + int count; + + // get the highest client number in use + count = 0; + for ( i = 0 ; i < sv_maxclients->integer ; i++ ) { + if ( svs.clients[i].state >= CS_CONNECTED ) { + if (i > count) + count = i; + } + } + count++; + + oldMaxClients = sv_maxclients->integer; + // never go below the highest client number in use + SV_BoundMaxClients( count ); + // if still the same + if ( sv_maxclients->integer == oldMaxClients ) { + return; + } + + oldClients = Hunk_AllocateTempMemory( count * sizeof(client_t) ); + // copy the clients to hunk memory + for ( i = 0 ; i < count ; i++ ) { + if ( svs.clients[i].state >= CS_CONNECTED ) { + oldClients[i] = svs.clients[i]; + } + else { + Com_Memset(&oldClients[i], 0, sizeof(client_t)); + } + } + + // free old clients arrays + Z_Free( svs.clients ); + + // allocate new clients + svs.clients = Z_Malloc ( sv_maxclients->integer * sizeof(client_t) ); + Com_Memset( svs.clients, 0, sv_maxclients->integer * sizeof(client_t) ); + + // copy the clients over + for ( i = 0 ; i < count ; i++ ) { + if ( oldClients[i].state >= CS_CONNECTED ) { + svs.clients[i] = oldClients[i]; + } + } + + // free the old clients on the hunk + Hunk_FreeTempMemory( oldClients ); + + // allocate new snapshot entities + if ( com_dedicated->integer ) { + svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * 64; + } else { + // we don't need nearly as many when playing locally + svs.numSnapshotEntities = sv_maxclients->integer * 4 * 64; + } +} + +/* +================ +SV_ClearServer +================ +*/ +void SV_ClearServer(void) { + int i; + + for ( i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) { + if ( sv.configstrings[i] ) { + Z_Free( sv.configstrings[i] ); + } + } + Com_Memset (&sv, 0, sizeof(sv)); +} + +/* +================ +SV_TouchCGame + + touch the cgame.vm so that a pure client can load it if it's in a seperate pk3 +================ +*/ +void SV_TouchCGame(void) { + fileHandle_t f; + char filename[MAX_QPATH]; + + Com_sprintf( filename, sizeof(filename), "vm/%s.qvm", "cgame" ); + FS_FOpenFileRead( filename, &f, qfalse ); + if ( f ) { + FS_FCloseFile( f ); + } +} + +/* +================ +SV_SpawnServer + +Change the server to a new map, taking all connected +clients along with it. +This is NOT called for map_restart +================ +*/ +void SV_SpawnServer( char *server, qboolean killBots ) { + int i; + int checksum; + qboolean isBot; + char systemInfo[16384]; + const char *p; + + // stop any active demo recording/playback (one demo = one map). + // SVD_IsStarting() returns true when SVD_Play_f is calling devmap + // internally -- don't stop our own playback. + if ( SVD_IsRecording() ) { + Com_Printf( "Map change -- stopping demo recording.\n" ); + SVD_StopRecord_f(); + } + if ( SVD_IsPlaying() && !SVD_IsStarting() ) { + Com_Printf( "Map change -- stopping demo playback.\n" ); + SVD_CleanupPlayback(); + } + + // shut down the existing game if it is running + SV_ShutdownGameProgs(); + + Com_Printf ("------ Server Initialization ------\n"); + Com_Printf ("Server: %s\n",server); + + // if not running a dedicated server CL_MapLoading will connect the client to the server + // also print some status stuff + CL_MapLoading(); + + // make sure all the client stuff is unloaded + CL_ShutdownAll(); + + // clear the whole hunk because we're (re)loading the server + Hunk_Clear(); + + // clear collision map data + CM_ClearMap(); + + // init client structures and svs.numSnapshotEntities + if ( !Cvar_VariableValue("sv_running") ) { + SV_Startup(); + } else { + // check for maxclients change + if ( sv_maxclients->modified ) { + SV_ChangeMaxClients(); + } + } + + // clear pak references + FS_ClearPakReferences(0); + + // allocate the snapshot entities on the hunk + svs.snapshotEntities = Hunk_Alloc( sizeof(entityState_t)*svs.numSnapshotEntities, h_high ); + svs.nextSnapshotEntities = 0; + + // toggle the server bit so clients can detect that a + // server has changed + svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; + + // set nextmap to the same map, but it may be overriden + // by the game startup or another console command + Cvar_Set( "nextmap", "map_restart 0"); +// Cvar_Set( "nextmap", va("map %s", server) ); + + // wipe the entire per-level structure + SV_ClearServer(); + for ( i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) { + sv.configstrings[i] = CopyString(""); + } + + // make sure we are not paused + Cvar_Set("cl_paused", "0"); + + // get a new checksum feed and restart the file system + srand(Com_Milliseconds()); + sv.checksumFeed = ( ((int) rand() << 16) ^ rand() ) ^ Com_Milliseconds(); + FS_Restart( sv.checksumFeed ); + + CM_LoadMap( va("maps/%s.bsp", server), qfalse, &checksum ); + + // set serverinfo visible name + Cvar_Set( "mapname", server ); + + Cvar_Set( "sv_mapChecksum", va("%i",checksum) ); + + // serverid should be different each time + sv.serverId = com_frameTime; + sv.restartedServerId = sv.serverId; // I suppose the init here is just to be safe + sv.checksumFeedServerId = sv.serverId; + Cvar_Set( "sv_serverid", va("%i", sv.serverId ) ); + + // clear physics interaction links + SV_ClearWorld (); + + // media configstring setting should be done during + // the loading stage, so connected clients don't have + // to load during actual gameplay + sv.state = SS_LOADING; + + // load and spawn all other entities + SV_InitGameProgs(); + + // don't allow a map_restart if game is modified + sv_gametype->modified = qfalse; + + // run a few frames to allow everything to settle + for ( i = 0 ;i < 3 ; i++ ) { + VM_Call( gvm, GAME_RUN_FRAME, svs.time ); + SV_BotFrame( svs.time ); + svs.time += 100; + } + + // create a baseline for more efficient communications + SV_CreateBaseline (); + + for (i=0 ; iinteger ; i++) { + // send the new gamestate to all connected clients + if (svs.clients[i].state >= CS_CONNECTED) { + char *denied; + + if ( svs.clients[i].netchan.remoteAddress.type == NA_BOT ) { + if ( killBots ) { + SV_DropClient( &svs.clients[i], "" ); + continue; + } + isBot = qtrue; + } + else { + isBot = qfalse; + } + + // connect the client again + denied = VM_ExplicitArgPtr( gvm, VM_Call( gvm, GAME_CLIENT_CONNECT, i, qfalse, isBot ) ); // firstTime = qfalse + if ( denied ) { + // this generally shouldn't happen, because the client + // was connected before the level change + SV_DropClient( &svs.clients[i], denied ); + } else { + if( !isBot ) { + // when we get the next packet from a connected client, + // the new gamestate will be sent + svs.clients[i].state = CS_CONNECTED; + } + else { + client_t *client; + sharedEntity_t *ent; + + client = &svs.clients[i]; + client->state = CS_ACTIVE; + ent = SV_GentityNum( i ); + ent->s.number = i; + client->gentity = ent; + + client->deltaMessage = -1; + client->nextSnapshotTime = svs.time; // generate a snapshot immediately + + VM_Call( gvm, GAME_CLIENT_BEGIN, i ); + } + } + } + } + + // run another frame to allow things to look at all the players + VM_Call( gvm, GAME_RUN_FRAME, svs.time ); + SV_BotFrame( svs.time ); + svs.time += 100; + + if ( sv_pure->integer ) { + // the server sends these to the clients so they will only + // load pk3s also loaded at the server + p = FS_LoadedPakChecksums(); + Cvar_Set( "sv_paks", p ); + if (strlen(p) == 0) { + Com_Printf( "WARNING: sv_pure set but no PK3 files loaded\n" ); + } + p = FS_LoadedPakNames(); + Cvar_Set( "sv_pakNames", p ); + + // if a dedicated pure server we need to touch the cgame because it could be in a + // seperate pk3 file and the client will need to load the latest cgame.qvm + if ( com_dedicated->integer ) { + SV_TouchCGame(); + } + } + else { + Cvar_Set( "sv_paks", "" ); + Cvar_Set( "sv_pakNames", "" ); + } + // the server sends these to the clients so they can figure + // out which pk3s should be auto-downloaded + p = FS_ReferencedPakChecksums(); + Cvar_Set( "sv_referencedPaks", p ); + p = FS_ReferencedPakNames(); + Cvar_Set( "sv_referencedPakNames", p ); + + // save systeminfo and serverinfo strings + Q_strncpyz( systemInfo, Cvar_InfoString_Big( CVAR_SYSTEMINFO ), sizeof( systemInfo ) ); + cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; + SV_SetConfigstring( CS_SYSTEMINFO, systemInfo ); + + SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) ); + cvar_modifiedFlags &= ~CVAR_SERVERINFO; + + // any media configstring setting now should issue a warning + // and any configstring changes should be reliably transmitted + // to all clients + sv.state = SS_GAME; + + // send a heartbeat now so the master will get up to date info + SV_Heartbeat_f(); + + Hunk_SetMark(); + + Com_Printf ("-----------------------------------\n"); + + // auto-record demo if enabled + SVD_AutoRecord(); +} + +/* +=============== +SV_Init + +Only called at main exe startup, not for each game +=============== +*/ +void SV_BotInitBotLib(void); + +void SV_Init (void) { + SV_AddOperatorCommands (); + + // serverinfo vars + Cvar_Get ("dmflags", "0", CVAR_SERVERINFO); + Cvar_Get ("fraglimit", "20", CVAR_SERVERINFO); + Cvar_Get ("timelimit", "0", CVAR_SERVERINFO); + sv_gametype = Cvar_Get ("g_gametype", "0", CVAR_SERVERINFO | CVAR_LATCH ); + Cvar_Get ("sv_keywords", "", CVAR_SERVERINFO); + Cvar_Get ("protocol", va("%i", PROTOCOL_VERSION), CVAR_SERVERINFO | CVAR_ROM); + sv_mapname = Cvar_Get ("mapname", "nomap", CVAR_SERVERINFO | CVAR_ROM); + sv_privateClients = Cvar_Get ("sv_privateClients", "0", CVAR_SERVERINFO); + sv_hostname = Cvar_Get ("sv_hostname", "noname", CVAR_SERVERINFO | CVAR_ARCHIVE ); + sv_maxclients = Cvar_Get ("sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH); + + sv_maxRate = Cvar_Get ("sv_maxRate", "0", CVAR_ARCHIVE | CVAR_SERVERINFO ); + sv_minPing = Cvar_Get ("sv_minPing", "0", CVAR_ARCHIVE | CVAR_SERVERINFO ); + sv_maxPing = Cvar_Get ("sv_maxPing", "0", CVAR_ARCHIVE | CVAR_SERVERINFO ); + sv_floodProtect = Cvar_Get ("sv_floodProtect", "1", CVAR_ARCHIVE | CVAR_SERVERINFO ); + + // systeminfo + Cvar_Get ("sv_cheats", "1", CVAR_SYSTEMINFO | CVAR_ROM ); + sv_serverid = Cvar_Get ("sv_serverid", "0", CVAR_SYSTEMINFO | CVAR_ROM ); +#ifndef DLL_ONLY // bk010216 - for DLL-only servers + sv_pure = Cvar_Get ("sv_pure", "1", CVAR_SYSTEMINFO ); +#else + sv_pure = Cvar_Get ("sv_pure", "0", CVAR_SYSTEMINFO | CVAR_INIT | CVAR_ROM ); +#endif + Cvar_Get ("sv_paks", "", CVAR_SYSTEMINFO | CVAR_ROM ); + Cvar_Get ("sv_pakNames", "", CVAR_SYSTEMINFO | CVAR_ROM ); + Cvar_Get ("sv_referencedPaks", "", CVAR_SYSTEMINFO | CVAR_ROM ); + Cvar_Get ("sv_referencedPakNames", "", CVAR_SYSTEMINFO | CVAR_ROM ); + + // server vars + sv_rconPassword = Cvar_Get ("rconPassword", "", CVAR_TEMP ); + sv_privatePassword = Cvar_Get ("sv_privatePassword", "", CVAR_TEMP ); + sv_fps = Cvar_Get ("sv_fps", "20", CVAR_TEMP ); + sv_timeout = Cvar_Get ("sv_timeout", "200", CVAR_TEMP ); + sv_zombietime = Cvar_Get ("sv_zombietime", "2", CVAR_TEMP ); + Cvar_Get ("nextmap", "", CVAR_TEMP ); + + sv_allowDownload = Cvar_Get ("sv_allowDownload", "0", CVAR_SERVERINFO); + sv_master[0] = Cvar_Get ("sv_master1", MASTER_SERVER_NAME, 0 ); + sv_master[1] = Cvar_Get ("sv_master2", "", CVAR_ARCHIVE ); + sv_master[2] = Cvar_Get ("sv_master3", "", CVAR_ARCHIVE ); + sv_master[3] = Cvar_Get ("sv_master4", "", CVAR_ARCHIVE ); + sv_master[4] = Cvar_Get ("sv_master5", "", CVAR_ARCHIVE ); + sv_reconnectlimit = Cvar_Get ("sv_reconnectlimit", "3", 0); + sv_showloss = Cvar_Get ("sv_showloss", "0", 0); + sv_padPackets = Cvar_Get ("sv_padPackets", "0", 0); + sv_killserver = Cvar_Get ("sv_killserver", "0", 0); + sv_mapChecksum = Cvar_Get ("sv_mapChecksum", "", CVAR_ROM); + sv_lanForceRate = Cvar_Get ("sv_lanForceRate", "1", CVAR_ARCHIVE ); + sv_strictAuth = Cvar_Get ("sv_strictAuth", "1", CVAR_ARCHIVE ); + + // server-side demo settings + Cvar_Get ("svdemo_autorecord", "0", CVAR_ARCHIVE); + Cvar_Get ("svdemo_pauseEmpty", "1", CVAR_ARCHIVE); + Cvar_Get ("svdemo_keyframeInterval", "5", CVAR_ARCHIVE); // seconds, 0 = disabled + + // initialize bot cvars so they are listed and can be set before loading the botlib + SV_BotInitCvars(); + + // init the botlib here because we need the pre-compiler in the UI + SV_BotInitBotLib(); +} + + +/* +================== +SV_FinalMessage + +Used by SV_Shutdown to send a final message to all +connected clients before the server goes down. The messages are sent immediately, +not just stuck on the outgoing message list, because the server is going +to totally exit after returning from this function. +================== +*/ +void SV_FinalMessage( char *message ) { + int i, j; + client_t *cl; + + // send it twice, ignoring rate + for ( j = 0 ; j < 2 ; j++ ) { + for (i=0, cl = svs.clients ; i < sv_maxclients->integer ; i++, cl++) { + if (cl->state >= CS_CONNECTED) { + // don't send a disconnect to a local client + if ( cl->netchan.remoteAddress.type != NA_LOOPBACK ) { + SV_SendServerCommand( cl, "print \"%s\"", message ); + SV_SendServerCommand( cl, "disconnect" ); + } + // force a snapshot to be sent + cl->nextSnapshotTime = -1; + SV_SendClientSnapshot( cl ); + } + } + } +} + + +/* +================ +SV_Shutdown + +Called when each game quits, +before Sys_Quit or Sys_Error +================ +*/ +void SV_Shutdown( char *finalmsg ) { + if ( !com_sv_running || !com_sv_running->integer ) { + return; + } + + Com_Printf( "----- Server Shutdown -----\n" ); + + // clean up any active demo recording/playback. + // skip if SVD_Play_f is calling SV_Shutdown internally. + if ( SVD_IsRecording() ) { + SVD_StopRecord_f(); + } + if ( SVD_IsPlaying() && !SVD_IsStarting() ) { + SVD_CleanupPlayback(); + } + + if ( svs.clients && !com_errorEntered ) { + SV_FinalMessage( finalmsg ); + } + + SV_RemoveOperatorCommands(); + SV_MasterShutdown(); + SV_ShutdownGameProgs(); + + // free current level + SV_ClearServer(); + + // free server static data + if ( svs.clients ) { + Z_Free( svs.clients ); + } + Com_Memset( &svs, 0, sizeof( svs ) ); + + Cvar_Set( "sv_running", "0" ); + Cvar_Set("ui_singlePlayerActive", "0"); + + Com_Printf( "---------------------------\n" ); + + // disconnect any local clients + CL_Disconnect( qfalse ); +} + diff --git a/code/server/sv_main.c b/code/server/sv_main.c index 27834a3..b521dd0 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -1,875 +1,875 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ - -#include "server.h" - -serverStatic_t svs; // persistant server info -server_t sv; // local server -vm_t *gvm = NULL; // game virtual machine // bk001212 init - -cvar_t *sv_fps; // time rate for running non-clients -cvar_t *sv_timeout; // seconds without any message -cvar_t *sv_zombietime; // seconds to sink messages after disconnect -cvar_t *sv_rconPassword; // password for remote server commands -cvar_t *sv_privatePassword; // password for the privateClient slots -cvar_t *sv_allowDownload; -cvar_t *sv_maxclients; - -cvar_t *sv_privateClients; // number of clients reserved for password -cvar_t *sv_hostname; -cvar_t *sv_master[MAX_MASTER_SERVERS]; // master server ip address -cvar_t *sv_reconnectlimit; // minimum seconds between connect messages -cvar_t *sv_showloss; // report when usercmds are lost -cvar_t *sv_padPackets; // add nop bytes to messages -cvar_t *sv_killserver; // menu system can set to 1 to shut server down -cvar_t *sv_mapname; -cvar_t *sv_mapChecksum; -cvar_t *sv_serverid; -cvar_t *sv_maxRate; -cvar_t *sv_minPing; -cvar_t *sv_maxPing; -cvar_t *sv_gametype; -cvar_t *sv_pure; -cvar_t *sv_floodProtect; -cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491) -cvar_t *sv_strictAuth; - -/* -============================================================================= - -EVENT MESSAGES - -============================================================================= -*/ - -/* -=============== -SV_ExpandNewlines - -Converts newlines to "\n" so a line prints nicer -=============== -*/ -char *SV_ExpandNewlines( char *in ) { - static char string[1024]; - int l; - - l = 0; - while ( *in && l < sizeof(string) - 3 ) { - if ( *in == '\n' ) { - string[l++] = '\\'; - string[l++] = 'n'; - } else { - string[l++] = *in; - } - in++; - } - string[l] = 0; - - return string; -} - -/* -====================== -SV_ReplacePendingServerCommands - - This is ugly -====================== -*/ -int SV_ReplacePendingServerCommands( client_t *client, const char *cmd ) { - int i, index, csnum1, csnum2; - - for ( i = client->reliableSent+1; i <= client->reliableSequence; i++ ) { - index = i & ( MAX_RELIABLE_COMMANDS - 1 ); - // - if ( !Q_strncmp(cmd, client->reliableCommands[ index ], strlen("cs")) ) { - sscanf(cmd, "cs %i", &csnum1); - sscanf(client->reliableCommands[ index ], "cs %i", &csnum2); - if ( csnum1 == csnum2 ) { - Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); - /* - if ( client->netchan.remoteAddress.type != NA_BOT ) { - Com_Printf( "WARNING: client %i removed double pending config string %i: %s\n", client-svs.clients, csnum1, cmd ); - } - */ - return qtrue; - } - } - } - return qfalse; -} - -/* -====================== -SV_AddServerCommand - -The given command will be transmitted to the client, and is guaranteed to -not have future snapshot_t executed before it is executed -====================== -*/ -void SV_AddServerCommand( client_t *client, const char *cmd ) { - int index, i; - - // this is very ugly but it's also a waste to for instance send multiple config string updates - // for the same config string index in one snapshot -// if ( SV_ReplacePendingServerCommands( client, cmd ) ) { -// return; -// } - - client->reliableSequence++; - // if we would be losing an old command that hasn't been acknowledged, - // we must drop the connection - // we check == instead of >= so a broadcast print added by SV_DropClient() - // doesn't cause a recursive drop client - if ( client->reliableSequence - client->reliableAcknowledge == MAX_RELIABLE_COMMANDS + 1 ) { - Com_Printf( "===== pending server commands =====\n" ); - for ( i = client->reliableAcknowledge + 1 ; i <= client->reliableSequence ; i++ ) { - Com_Printf( "cmd %5d: %s\n", i, client->reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] ); - } - Com_Printf( "cmd %5d: %s\n", i, cmd ); - SV_DropClient( client, "Server command overflow" ); - return; - } - index = client->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); - Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); -} - - -/* -================= -SV_SendServerCommand - -Sends a reliable command string to be interpreted by -the client game module: "cp", "print", "chat", etc -A NULL client will broadcast to all clients -================= -*/ -void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) { - va_list argptr; - byte message[MAX_MSGLEN]; - client_t *client; - int j; - - va_start (argptr,fmt); - Q_vsnprintf ((char *)message, sizeof(message), fmt,argptr); - va_end (argptr); - - if ( cl != NULL ) { - SV_AddServerCommand( cl, (char *)message ); - return; - } - - // hack to echo broadcast prints to console - if ( com_dedicated->integer && !strncmp( (char *)message, "print", 5) ) { - Com_Printf ("broadcast: %s\n", SV_ExpandNewlines((char *)message) ); - } - - // send the data to all relevent clients - for (j = 0, client = svs.clients; j < sv_maxclients->integer ; j++, client++) { - if ( client->state < CS_PRIMED ) { - continue; - } - SV_AddServerCommand( client, (char *)message ); - } -} - - -/* -============================================================================== - -MASTER SERVER FUNCTIONS - -============================================================================== -*/ - -/* -================ -SV_MasterHeartbeat - -Send a message to the masters every few minutes to -let it know we are alive, and log information. -We will also have a heartbeat sent when a server -changes from empty to non-empty, and full to non-full, -but not on every player enter or exit. -================ -*/ -#define HEARTBEAT_MSEC 300*1000 -#define HEARTBEAT_GAME "QuakeArena-1" -void SV_MasterHeartbeat( void ) { - static netadr_t adr[MAX_MASTER_SERVERS]; - int i; - - // "dedicated 1" is for lan play, "dedicated 2" is for inet public play - if ( !com_dedicated || com_dedicated->integer != 2 ) { - return; // only dedicated servers send heartbeats - } - - // if not time yet, don't send anything - if ( svs.time < svs.nextHeartbeatTime ) { - return; - } - svs.nextHeartbeatTime = svs.time + HEARTBEAT_MSEC; - - - // send to group masters - for ( i = 0 ; i < MAX_MASTER_SERVERS ; i++ ) { - if ( !sv_master[i]->string[0] ) { - continue; - } - - // see if we haven't already resolved the name - // resolving usually causes hitches on win95, so only - // do it when needed - if ( sv_master[i]->modified ) { - sv_master[i]->modified = qfalse; - - Com_Printf( "Resolving %s\n", sv_master[i]->string ); - if ( !NET_StringToAdr( sv_master[i]->string, &adr[i] ) ) { - // if the address failed to resolve, clear it - // so we don't take repeated dns hits - Com_Printf( "Couldn't resolve address: %s\n", sv_master[i]->string ); - Cvar_Set( sv_master[i]->name, "" ); - sv_master[i]->modified = qfalse; - continue; - } - if ( !strstr( ":", sv_master[i]->string ) ) { - adr[i].port = BigShort( PORT_MASTER ); - } - Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", sv_master[i]->string, - adr[i].ip[0], adr[i].ip[1], adr[i].ip[2], adr[i].ip[3], - BigShort( adr[i].port ) ); - } - - - Com_Printf ("Sending heartbeat to %s\n", sv_master[i]->string ); - // this command should be changed if the server info / status format - // ever incompatably changes - NET_OutOfBandPrint( NS_SERVER, adr[i], "heartbeat %s\n", HEARTBEAT_GAME ); - } -} - -/* -================= -SV_MasterShutdown - -Informs all masters that this server is going down -================= -*/ -void SV_MasterShutdown( void ) { - // send a hearbeat right now - svs.nextHeartbeatTime = -9999; - SV_MasterHeartbeat(); - - // send it again to minimize chance of drops - svs.nextHeartbeatTime = -9999; - SV_MasterHeartbeat(); - - // when the master tries to poll the server, it won't respond, so - // it will be removed from the list -} - - -/* -============================================================================== - -CONNECTIONLESS COMMANDS - -============================================================================== -*/ - -/* -================ -SVC_Status - -Responds with all the info that qplug or qspy can see about the server -and all connected players. Used for getting detailed information after -the simple info query. -================ -*/ -void SVC_Status( netadr_t from ) { - char player[1024]; - char status[MAX_MSGLEN]; - int i; - client_t *cl; - playerState_t *ps; - int statusLength; - int playerLength; - char infostring[MAX_INFO_STRING]; - - // ignore if we are in single player - if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER ) { - return; - } - - strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) ); - - // echo back the parameter to status. so master servers can use it as a challenge - // to prevent timed spoofed reply packets that add ghost servers - Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); - - // add "demo" to the sv_keywords if restricted - if ( Cvar_VariableValue( "fs_restrict" ) ) { - char keywords[MAX_INFO_STRING]; - - Com_sprintf( keywords, sizeof( keywords ), "demo %s", - Info_ValueForKey( infostring, "sv_keywords" ) ); - Info_SetValueForKey( infostring, "sv_keywords", keywords ); - } - - status[0] = 0; - statusLength = 0; - - for (i=0 ; i < sv_maxclients->integer ; i++) { - cl = &svs.clients[i]; - if ( cl->state >= CS_CONNECTED ) { - ps = SV_GameClientNum( i ); - Com_sprintf (player, sizeof(player), "%i %i \"%s\"\n", - ps->persistant[PERS_SCORE], cl->ping, cl->name); - playerLength = strlen(player); - if (statusLength + playerLength >= sizeof(status) ) { - break; // can't hold any more - } - strcpy (status + statusLength, player); - statusLength += playerLength; - } - } - - NET_OutOfBandPrint( NS_SERVER, from, "statusResponse\n%s\n%s", infostring, status ); -} - -/* -================ -SVC_Info - -Responds with a short info message that should be enough to determine -if a user is interested in a server to do a full status -================ -*/ -void SVC_Info( netadr_t from ) { - int i, count; - char *gamedir; - char infostring[MAX_INFO_STRING]; - - // ignore if we are in single player - if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) { - return; - } - - // don't count privateclients - count = 0; - for ( i = sv_privateClients->integer ; i < sv_maxclients->integer ; i++ ) { - if ( svs.clients[i].state >= CS_CONNECTED ) { - count++; - } - } - - infostring[0] = 0; - - // echo back the parameter to status. so servers can use it as a challenge - // to prevent timed spoofed reply packets that add ghost servers - Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); - - Info_SetValueForKey( infostring, "protocol", va("%i", PROTOCOL_VERSION) ); - Info_SetValueForKey( infostring, "hostname", sv_hostname->string ); - Info_SetValueForKey( infostring, "mapname", sv_mapname->string ); - Info_SetValueForKey( infostring, "clients", va("%i", count) ); - Info_SetValueForKey( infostring, "sv_maxclients", - va("%i", sv_maxclients->integer - sv_privateClients->integer ) ); - Info_SetValueForKey( infostring, "gametype", va("%i", sv_gametype->integer ) ); - Info_SetValueForKey( infostring, "pure", va("%i", sv_pure->integer ) ); - - if( sv_minPing->integer ) { - Info_SetValueForKey( infostring, "minPing", va("%i", sv_minPing->integer) ); - } - if( sv_maxPing->integer ) { - Info_SetValueForKey( infostring, "maxPing", va("%i", sv_maxPing->integer) ); - } - gamedir = Cvar_VariableString( "fs_game" ); - if( *gamedir ) { - Info_SetValueForKey( infostring, "game", gamedir ); - } - - NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring ); -} - -/* -================ -SVC_FlushRedirect - -================ -*/ -void SV_FlushRedirect( char *outputbuf ) { - NET_OutOfBandPrint( NS_SERVER, svs.redirectAddress, "print\n%s", outputbuf ); -} - -/* -=============== -SVC_RemoteCommand - -An rcon packet arrived from the network. -Shift down the remaining args -Redirect all printfs -=============== -*/ -void SVC_RemoteCommand( netadr_t from, msg_t *msg ) { - qboolean valid; - unsigned int time; - char remaining[1024]; - // TTimo - scaled down to accumulate, but not overflow anything network wise, print wise etc. - // (OOB messages are the bottleneck here) -#define SV_OUTPUTBUF_LENGTH (1024 - 16) - char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; - static unsigned int lasttime = 0; - char *cmd_aux; - - // TTimo - https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=534 - time = Com_Milliseconds(); - if (time<(lasttime+500)) { - return; - } - lasttime = time; - - if ( !strlen( sv_rconPassword->string ) || - strcmp (Cmd_Argv(1), sv_rconPassword->string) ) { - valid = qfalse; - Com_Printf ("Bad rcon from %s:\n%s\n", NET_AdrToString (from), Cmd_Argv(2) ); - } else { - valid = qtrue; - Com_Printf ("Rcon from %s:\n%s\n", NET_AdrToString (from), Cmd_Argv(2) ); - } - - // start redirecting all print outputs to the packet - svs.redirectAddress = from; - Com_BeginRedirect (sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect); - - if ( !strlen( sv_rconPassword->string ) ) { - Com_Printf ("No rconpassword set on the server.\n"); - } else if ( !valid ) { - Com_Printf ("Bad rconpassword.\n"); - } else { - remaining[0] = 0; - - // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543 - // get the command directly, "rcon " to avoid quoting issues - // extract the command by walking - // since the cmd formatting can fuckup (amount of spaces), using a dumb step by step parsing - cmd_aux = Cmd_Cmd(); - cmd_aux+=4; - while(cmd_aux[0]==' ') - cmd_aux++; - while(cmd_aux[0] && cmd_aux[0]!=' ') // password - cmd_aux++; - while(cmd_aux[0]==' ') - cmd_aux++; - - Q_strcat( remaining, sizeof(remaining), cmd_aux); - - Cmd_ExecuteString (remaining); - - } - - Com_EndRedirect (); -} - -/* -================= -SV_ConnectionlessPacket - -A connectionless packet has four leading 0xff -characters to distinguish it from a game channel. -Clients that are in the game can still send -connectionless packets. -================= -*/ -void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { - char *s; - char *c; - - MSG_BeginReadingOOB( msg ); - MSG_ReadLong( msg ); // skip the -1 marker - - if (!Q_strncmp("connect", &msg->data[4], 7)) { - Huff_Decompress(msg, 12); - } - - s = MSG_ReadStringLine( msg ); - Cmd_TokenizeString( s ); - - c = Cmd_Argv(0); - Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c); - - if (!Q_stricmp(c, "getstatus")) { - SVC_Status( from ); - } else if (!Q_stricmp(c, "getinfo")) { - SVC_Info( from ); - } else if (!Q_stricmp(c, "getchallenge")) { - SV_GetChallenge( from ); - } else if (!Q_stricmp(c, "connect")) { - SV_DirectConnect( from ); - } else if (!Q_stricmp(c, "ipAuthorize")) { - SV_AuthorizeIpPacket( from ); - } else if (!Q_stricmp(c, "rcon")) { - SVC_RemoteCommand( from, msg ); - } else if (!Q_stricmp(c, "disconnect")) { - // if a client starts up a local server, we may see some spurious - // server disconnect messages when their new server sees our final - // sequenced messages to the old client - } else { - Com_DPrintf ("bad connectionless packet from %s:\n%s\n" - , NET_AdrToString (from), s); - } -} - -//============================================================================ - -/* -================= -SV_ReadPackets -================= -*/ -void SV_PacketEvent( netadr_t from, msg_t *msg ) { - int i; - client_t *cl; - int qport; - - // check for connectionless packet (0xffffffff) first - if ( msg->cursize >= 4 && *(int *)msg->data == -1) { - SV_ConnectionlessPacket( from, msg ); - return; - } - - // read the qport out of the message so we can fix up - // stupid address translating routers - MSG_BeginReadingOOB( msg ); - MSG_ReadLong( msg ); // sequence number - qport = MSG_ReadShort( msg ) & 0xffff; - - // find which client the message is from - for (i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { - if (cl->state == CS_FREE) { - continue; - } - if ( !NET_CompareBaseAdr( from, cl->netchan.remoteAddress ) ) { - continue; - } - // it is possible to have multiple clients from a single IP - // address, so they are differentiated by the qport variable - if (cl->netchan.qport != qport) { - continue; - } - - // the IP port can't be used to differentiate them, because - // some address translating routers periodically change UDP - // port assignments - if (cl->netchan.remoteAddress.port != from.port) { - Com_Printf( "SV_PacketEvent: fixing up a translated port\n" ); - cl->netchan.remoteAddress.port = from.port; - } - - // make sure it is a valid, in sequence packet - if (SV_Netchan_Process(cl, msg)) { - // zombie clients still need to do the Netchan_Process - // to make sure they don't need to retransmit the final - // reliable message, but they don't do any other processing - if (cl->state != CS_ZOMBIE) { - cl->lastPacketTime = svs.time; // don't timeout - SV_ExecuteClientMessage( cl, msg ); - } - } - return; - } - - // if we received a sequenced packet from an address we don't recognize, - // send an out of band disconnect packet to it - NET_OutOfBandPrint( NS_SERVER, from, "disconnect" ); -} - - -/* -=================== -SV_CalcPings - -Updates the cl->ping variables -=================== -*/ -void SV_CalcPings( void ) { - int i, j; - client_t *cl; - int total, count; - int delta; - playerState_t *ps; - - for (i=0 ; i < sv_maxclients->integer ; i++) { - cl = &svs.clients[i]; - if ( cl->state != CS_ACTIVE ) { - cl->ping = 999; - continue; - } - if ( !cl->gentity ) { - cl->ping = 999; - continue; - } - if ( cl->gentity->r.svFlags & SVF_BOT ) { - cl->ping = 0; - continue; - } - - total = 0; - count = 0; - for ( j = 0 ; j < PACKET_BACKUP ; j++ ) { - if ( cl->frames[j].messageAcked <= 0 ) { - continue; - } - delta = cl->frames[j].messageAcked - cl->frames[j].messageSent; - count++; - total += delta; - } - if (!count) { - cl->ping = 999; - } else { - cl->ping = total/count; - if ( cl->ping > 999 ) { - cl->ping = 999; - } - } - - // let the game dll know about the ping - ps = SV_GameClientNum( i ); - ps->ping = cl->ping; - } -} - -/* -================== -SV_CheckTimeouts - -If a packet has not been received from a client for timeout->integer -seconds, drop the conneciton. Server time is used instead of -realtime to avoid dropping the local client while debugging. - -When a client is normally dropped, the client_t goes into a zombie state -for a few seconds to make sure any final reliable message gets resent -if necessary -================== -*/ -void SV_CheckTimeouts( void ) { - int i; - client_t *cl; - int droppoint; - int zombiepoint; - - droppoint = svs.time - 1000 * sv_timeout->integer; - zombiepoint = svs.time - 1000 * sv_zombietime->integer; - - for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { - // message times may be wrong across a changelevel - if (cl->lastPacketTime > svs.time) { - cl->lastPacketTime = svs.time; - } - - if (cl->state == CS_ZOMBIE - && cl->lastPacketTime < zombiepoint) { - // using the client id cause the cl->name is empty at this point - Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i ); - cl->state = CS_FREE; // can now be reused - continue; - } - if ( cl->state >= CS_CONNECTED && cl->lastPacketTime < droppoint) { - // wait several frames so a debugger session doesn't - // cause a timeout - if ( ++cl->timeoutCount > 5 ) { - SV_DropClient (cl, "timed out"); - cl->state = CS_FREE; // don't bother with zombie state - } - } else { - cl->timeoutCount = 0; - } - } -} - - -/* -================== -SV_CheckPaused -================== -*/ -qboolean SV_CheckPaused( void ) { - int count; - client_t *cl; - int i; - - if ( !cl_paused->integer ) { - return qfalse; - } - - // only pause if there is just a single client connected - count = 0; - for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { - if ( cl->state >= CS_CONNECTED && cl->netchan.remoteAddress.type != NA_BOT ) { - count++; - } - } - - if ( count > 1 ) { - // don't pause - if (sv_paused->integer) - Cvar_Set("sv_paused", "0"); - return qfalse; - } - - if (!sv_paused->integer) - Cvar_Set("sv_paused", "1"); - return qtrue; -} - -/* -================== -SV_Frame - -Player movement occurs as a result of packet events, which -happen before SV_Frame is called -================== -*/ -void SV_Frame( int msec ) { - int frameMsec; - int startTime; - - // the menu kills the server with this cvar - if ( sv_killserver->integer ) { - SV_Shutdown ("Server was killed.\n"); - Cvar_Set( "sv_killserver", "0" ); - return; - } - - if ( !com_sv_running->integer ) { - return; - } - - // allow pause if only the local client is connected - if ( SV_CheckPaused() ) { - return; - } - - // if it isn't time for the next frame, do nothing - if ( sv_fps->integer < 1 ) { - Cvar_Set( "sv_fps", "10" ); - } - frameMsec = 1000 / sv_fps->integer ; - - sv.timeResidual += msec; - - if (!com_dedicated->integer) SV_BotFrame( svs.time + sv.timeResidual ); - - if ( com_dedicated->integer && sv.timeResidual < frameMsec ) { - // NET_Sleep will give the OS time slices until either get a packet - // or time enough for a server frame has gone by - NET_Sleep(frameMsec - sv.timeResidual); - return; - } - - // if time is about to hit the 32nd bit, kick all clients - // and clear sv.time, rather - // than checking for negative time wraparound everywhere. - // 2giga-milliseconds = 23 days, so it won't be too often - if ( svs.time > 0x70000000 ) { - SV_Shutdown( "Restarting server due to time wrapping" ); - Cbuf_AddText( "vstr nextmap\n" ); - return; - } - // this can happen considerably earlier when lots of clients play and the map doesn't change - if ( svs.nextSnapshotEntities >= 0x7FFFFFFE - svs.numSnapshotEntities ) { - SV_Shutdown( "Restarting server due to numSnapshotEntities wrapping" ); - Cbuf_AddText( "vstr nextmap\n" ); - return; - } - - if( sv.restartTime && svs.time >= sv.restartTime ) { - sv.restartTime = 0; - Cbuf_AddText( "map_restart 0\n" ); - return; - } - - // update infostrings if anything has been changed - if ( cvar_modifiedFlags & CVAR_SERVERINFO ) { - SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) ); - cvar_modifiedFlags &= ~CVAR_SERVERINFO; - } - if ( cvar_modifiedFlags & CVAR_SYSTEMINFO ) { - SV_SetConfigstring( CS_SYSTEMINFO, Cvar_InfoString_Big( CVAR_SYSTEMINFO ) ); - cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; - } - - if ( com_speeds->integer ) { - startTime = Sys_Milliseconds (); - } else { - startTime = 0; // quite a compiler warning - } - - // update ping based on the all received frames - SV_CalcPings(); - - if (com_dedicated->integer) SV_BotFrame( svs.time ); - - // run the game simulation in chunks - while ( sv.timeResidual >= frameMsec ) { - sv.timeResidual -= frameMsec; - - if ( SVD_IsPaused() ) { - // demo paused: freeze svs.time so trajectories freeze - // and client doesn't see time jumps on unpause. - // still run game frame for spectator movement (at frozen time). - VM_Call( gvm, GAME_RUN_FRAME, svs.time ); - continue; - } - - svs.time += frameMsec; - - if ( SVD_IsPlaying() ) { - // demo playback: read recorded entities instead of running game logic - SVD_PlaybackFrame(); - // still call the game frame for spectator movement - } - - // let everything in the world think and move - VM_Call( gvm, GAME_RUN_FRAME, svs.time ); - - // capture frame for demo recording - SVD_RecordFrame(); - } - - if ( com_speeds->integer ) { - time_game = Sys_Milliseconds () - startTime; - } - - // check timeouts (skip during demo playback — zombie slots would be freed) - if ( !SVD_IsPlaying() ) { - SV_CheckTimeouts(); - } - - // send messages back to the clients - SV_SendClientMessages(); - - // send a heartbeat to the master if needed - SV_MasterHeartbeat(); -} - -//============================================================================ - +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "server.h" + +serverStatic_t svs; // persistant server info +server_t sv; // local server +vm_t *gvm = NULL; // game virtual machine // bk001212 init + +cvar_t *sv_fps; // time rate for running non-clients +cvar_t *sv_timeout; // seconds without any message +cvar_t *sv_zombietime; // seconds to sink messages after disconnect +cvar_t *sv_rconPassword; // password for remote server commands +cvar_t *sv_privatePassword; // password for the privateClient slots +cvar_t *sv_allowDownload; +cvar_t *sv_maxclients; + +cvar_t *sv_privateClients; // number of clients reserved for password +cvar_t *sv_hostname; +cvar_t *sv_master[MAX_MASTER_SERVERS]; // master server ip address +cvar_t *sv_reconnectlimit; // minimum seconds between connect messages +cvar_t *sv_showloss; // report when usercmds are lost +cvar_t *sv_padPackets; // add nop bytes to messages +cvar_t *sv_killserver; // menu system can set to 1 to shut server down +cvar_t *sv_mapname; +cvar_t *sv_mapChecksum; +cvar_t *sv_serverid; +cvar_t *sv_maxRate; +cvar_t *sv_minPing; +cvar_t *sv_maxPing; +cvar_t *sv_gametype; +cvar_t *sv_pure; +cvar_t *sv_floodProtect; +cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491) +cvar_t *sv_strictAuth; + +/* +============================================================================= + +EVENT MESSAGES + +============================================================================= +*/ + +/* +=============== +SV_ExpandNewlines + +Converts newlines to "\n" so a line prints nicer +=============== +*/ +char *SV_ExpandNewlines( char *in ) { + static char string[1024]; + int l; + + l = 0; + while ( *in && l < sizeof(string) - 3 ) { + if ( *in == '\n' ) { + string[l++] = '\\'; + string[l++] = 'n'; + } else { + string[l++] = *in; + } + in++; + } + string[l] = 0; + + return string; +} + +/* +====================== +SV_ReplacePendingServerCommands + + This is ugly +====================== +*/ +int SV_ReplacePendingServerCommands( client_t *client, const char *cmd ) { + int i, index, csnum1, csnum2; + + for ( i = client->reliableSent+1; i <= client->reliableSequence; i++ ) { + index = i & ( MAX_RELIABLE_COMMANDS - 1 ); + // + if ( !Q_strncmp(cmd, client->reliableCommands[ index ], strlen("cs")) ) { + sscanf(cmd, "cs %i", &csnum1); + sscanf(client->reliableCommands[ index ], "cs %i", &csnum2); + if ( csnum1 == csnum2 ) { + Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); + /* + if ( client->netchan.remoteAddress.type != NA_BOT ) { + Com_Printf( "WARNING: client %i removed double pending config string %i: %s\n", client-svs.clients, csnum1, cmd ); + } + */ + return qtrue; + } + } + } + return qfalse; +} + +/* +====================== +SV_AddServerCommand + +The given command will be transmitted to the client, and is guaranteed to +not have future snapshot_t executed before it is executed +====================== +*/ +void SV_AddServerCommand( client_t *client, const char *cmd ) { + int index, i; + + // this is very ugly but it's also a waste to for instance send multiple config string updates + // for the same config string index in one snapshot +// if ( SV_ReplacePendingServerCommands( client, cmd ) ) { +// return; +// } + + client->reliableSequence++; + // if we would be losing an old command that hasn't been acknowledged, + // we must drop the connection + // we check == instead of >= so a broadcast print added by SV_DropClient() + // doesn't cause a recursive drop client + if ( client->reliableSequence - client->reliableAcknowledge == MAX_RELIABLE_COMMANDS + 1 ) { + Com_Printf( "===== pending server commands =====\n" ); + for ( i = client->reliableAcknowledge + 1 ; i <= client->reliableSequence ; i++ ) { + Com_Printf( "cmd %5d: %s\n", i, client->reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] ); + } + Com_Printf( "cmd %5d: %s\n", i, cmd ); + SV_DropClient( client, "Server command overflow" ); + return; + } + index = client->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); + Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); +} + + +/* +================= +SV_SendServerCommand + +Sends a reliable command string to be interpreted by +the client game module: "cp", "print", "chat", etc +A NULL client will broadcast to all clients +================= +*/ +void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) { + va_list argptr; + byte message[MAX_MSGLEN]; + client_t *client; + int j; + + va_start (argptr,fmt); + Q_vsnprintf ((char *)message, sizeof(message), fmt,argptr); + va_end (argptr); + + if ( cl != NULL ) { + SV_AddServerCommand( cl, (char *)message ); + return; + } + + // hack to echo broadcast prints to console + if ( com_dedicated->integer && !strncmp( (char *)message, "print", 5) ) { + Com_Printf ("broadcast: %s\n", SV_ExpandNewlines((char *)message) ); + } + + // send the data to all relevent clients + for (j = 0, client = svs.clients; j < sv_maxclients->integer ; j++, client++) { + if ( client->state < CS_PRIMED ) { + continue; + } + SV_AddServerCommand( client, (char *)message ); + } +} + + +/* +============================================================================== + +MASTER SERVER FUNCTIONS + +============================================================================== +*/ + +/* +================ +SV_MasterHeartbeat + +Send a message to the masters every few minutes to +let it know we are alive, and log information. +We will also have a heartbeat sent when a server +changes from empty to non-empty, and full to non-full, +but not on every player enter or exit. +================ +*/ +#define HEARTBEAT_MSEC 300*1000 +#define HEARTBEAT_GAME "QuakeArena-1" +void SV_MasterHeartbeat( void ) { + static netadr_t adr[MAX_MASTER_SERVERS]; + int i; + + // "dedicated 1" is for lan play, "dedicated 2" is for inet public play + if ( !com_dedicated || com_dedicated->integer != 2 ) { + return; // only dedicated servers send heartbeats + } + + // if not time yet, don't send anything + if ( svs.time < svs.nextHeartbeatTime ) { + return; + } + svs.nextHeartbeatTime = svs.time + HEARTBEAT_MSEC; + + + // send to group masters + for ( i = 0 ; i < MAX_MASTER_SERVERS ; i++ ) { + if ( !sv_master[i]->string[0] ) { + continue; + } + + // see if we haven't already resolved the name + // resolving usually causes hitches on win95, so only + // do it when needed + if ( sv_master[i]->modified ) { + sv_master[i]->modified = qfalse; + + Com_Printf( "Resolving %s\n", sv_master[i]->string ); + if ( !NET_StringToAdr( sv_master[i]->string, &adr[i] ) ) { + // if the address failed to resolve, clear it + // so we don't take repeated dns hits + Com_Printf( "Couldn't resolve address: %s\n", sv_master[i]->string ); + Cvar_Set( sv_master[i]->name, "" ); + sv_master[i]->modified = qfalse; + continue; + } + if ( !strstr( ":", sv_master[i]->string ) ) { + adr[i].port = BigShort( PORT_MASTER ); + } + Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", sv_master[i]->string, + adr[i].ip[0], adr[i].ip[1], adr[i].ip[2], adr[i].ip[3], + BigShort( adr[i].port ) ); + } + + + Com_Printf ("Sending heartbeat to %s\n", sv_master[i]->string ); + // this command should be changed if the server info / status format + // ever incompatably changes + NET_OutOfBandPrint( NS_SERVER, adr[i], "heartbeat %s\n", HEARTBEAT_GAME ); + } +} + +/* +================= +SV_MasterShutdown + +Informs all masters that this server is going down +================= +*/ +void SV_MasterShutdown( void ) { + // send a hearbeat right now + svs.nextHeartbeatTime = -9999; + SV_MasterHeartbeat(); + + // send it again to minimize chance of drops + svs.nextHeartbeatTime = -9999; + SV_MasterHeartbeat(); + + // when the master tries to poll the server, it won't respond, so + // it will be removed from the list +} + + +/* +============================================================================== + +CONNECTIONLESS COMMANDS + +============================================================================== +*/ + +/* +================ +SVC_Status + +Responds with all the info that qplug or qspy can see about the server +and all connected players. Used for getting detailed information after +the simple info query. +================ +*/ +void SVC_Status( netadr_t from ) { + char player[1024]; + char status[MAX_MSGLEN]; + int i; + client_t *cl; + playerState_t *ps; + int statusLength; + int playerLength; + char infostring[MAX_INFO_STRING]; + + // ignore if we are in single player + if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER ) { + return; + } + + strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) ); + + // echo back the parameter to status. so master servers can use it as a challenge + // to prevent timed spoofed reply packets that add ghost servers + Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); + + // add "demo" to the sv_keywords if restricted + if ( Cvar_VariableValue( "fs_restrict" ) ) { + char keywords[MAX_INFO_STRING]; + + Com_sprintf( keywords, sizeof( keywords ), "demo %s", + Info_ValueForKey( infostring, "sv_keywords" ) ); + Info_SetValueForKey( infostring, "sv_keywords", keywords ); + } + + status[0] = 0; + statusLength = 0; + + for (i=0 ; i < sv_maxclients->integer ; i++) { + cl = &svs.clients[i]; + if ( cl->state >= CS_CONNECTED ) { + ps = SV_GameClientNum( i ); + Com_sprintf (player, sizeof(player), "%i %i \"%s\"\n", + ps->persistant[PERS_SCORE], cl->ping, cl->name); + playerLength = strlen(player); + if (statusLength + playerLength >= sizeof(status) ) { + break; // can't hold any more + } + strcpy (status + statusLength, player); + statusLength += playerLength; + } + } + + NET_OutOfBandPrint( NS_SERVER, from, "statusResponse\n%s\n%s", infostring, status ); +} + +/* +================ +SVC_Info + +Responds with a short info message that should be enough to determine +if a user is interested in a server to do a full status +================ +*/ +void SVC_Info( netadr_t from ) { + int i, count; + char *gamedir; + char infostring[MAX_INFO_STRING]; + + // ignore if we are in single player + if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) { + return; + } + + // don't count privateclients + count = 0; + for ( i = sv_privateClients->integer ; i < sv_maxclients->integer ; i++ ) { + if ( svs.clients[i].state >= CS_CONNECTED ) { + count++; + } + } + + infostring[0] = 0; + + // echo back the parameter to status. so servers can use it as a challenge + // to prevent timed spoofed reply packets that add ghost servers + Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); + + Info_SetValueForKey( infostring, "protocol", va("%i", PROTOCOL_VERSION) ); + Info_SetValueForKey( infostring, "hostname", sv_hostname->string ); + Info_SetValueForKey( infostring, "mapname", sv_mapname->string ); + Info_SetValueForKey( infostring, "clients", va("%i", count) ); + Info_SetValueForKey( infostring, "sv_maxclients", + va("%i", sv_maxclients->integer - sv_privateClients->integer ) ); + Info_SetValueForKey( infostring, "gametype", va("%i", sv_gametype->integer ) ); + Info_SetValueForKey( infostring, "pure", va("%i", sv_pure->integer ) ); + + if( sv_minPing->integer ) { + Info_SetValueForKey( infostring, "minPing", va("%i", sv_minPing->integer) ); + } + if( sv_maxPing->integer ) { + Info_SetValueForKey( infostring, "maxPing", va("%i", sv_maxPing->integer) ); + } + gamedir = Cvar_VariableString( "fs_game" ); + if( *gamedir ) { + Info_SetValueForKey( infostring, "game", gamedir ); + } + + NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring ); +} + +/* +================ +SVC_FlushRedirect + +================ +*/ +void SV_FlushRedirect( char *outputbuf ) { + NET_OutOfBandPrint( NS_SERVER, svs.redirectAddress, "print\n%s", outputbuf ); +} + +/* +=============== +SVC_RemoteCommand + +An rcon packet arrived from the network. +Shift down the remaining args +Redirect all printfs +=============== +*/ +void SVC_RemoteCommand( netadr_t from, msg_t *msg ) { + qboolean valid; + unsigned int time; + char remaining[1024]; + // TTimo - scaled down to accumulate, but not overflow anything network wise, print wise etc. + // (OOB messages are the bottleneck here) +#define SV_OUTPUTBUF_LENGTH (1024 - 16) + char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; + static unsigned int lasttime = 0; + char *cmd_aux; + + // TTimo - https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=534 + time = Com_Milliseconds(); + if (time<(lasttime+500)) { + return; + } + lasttime = time; + + if ( !strlen( sv_rconPassword->string ) || + strcmp (Cmd_Argv(1), sv_rconPassword->string) ) { + valid = qfalse; + Com_Printf ("Bad rcon from %s:\n%s\n", NET_AdrToString (from), Cmd_Argv(2) ); + } else { + valid = qtrue; + Com_Printf ("Rcon from %s:\n%s\n", NET_AdrToString (from), Cmd_Argv(2) ); + } + + // start redirecting all print outputs to the packet + svs.redirectAddress = from; + Com_BeginRedirect (sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect); + + if ( !strlen( sv_rconPassword->string ) ) { + Com_Printf ("No rconpassword set on the server.\n"); + } else if ( !valid ) { + Com_Printf ("Bad rconpassword.\n"); + } else { + remaining[0] = 0; + + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543 + // get the command directly, "rcon " to avoid quoting issues + // extract the command by walking + // since the cmd formatting can fuckup (amount of spaces), using a dumb step by step parsing + cmd_aux = Cmd_Cmd(); + cmd_aux+=4; + while(cmd_aux[0]==' ') + cmd_aux++; + while(cmd_aux[0] && cmd_aux[0]!=' ') // password + cmd_aux++; + while(cmd_aux[0]==' ') + cmd_aux++; + + Q_strcat( remaining, sizeof(remaining), cmd_aux); + + Cmd_ExecuteString (remaining); + + } + + Com_EndRedirect (); +} + +/* +================= +SV_ConnectionlessPacket + +A connectionless packet has four leading 0xff +characters to distinguish it from a game channel. +Clients that are in the game can still send +connectionless packets. +================= +*/ +void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { + char *s; + char *c; + + MSG_BeginReadingOOB( msg ); + MSG_ReadLong( msg ); // skip the -1 marker + + if (!Q_strncmp("connect", &msg->data[4], 7)) { + Huff_Decompress(msg, 12); + } + + s = MSG_ReadStringLine( msg ); + Cmd_TokenizeString( s ); + + c = Cmd_Argv(0); + Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c); + + if (!Q_stricmp(c, "getstatus")) { + SVC_Status( from ); + } else if (!Q_stricmp(c, "getinfo")) { + SVC_Info( from ); + } else if (!Q_stricmp(c, "getchallenge")) { + SV_GetChallenge( from ); + } else if (!Q_stricmp(c, "connect")) { + SV_DirectConnect( from ); + } else if (!Q_stricmp(c, "ipAuthorize")) { + SV_AuthorizeIpPacket( from ); + } else if (!Q_stricmp(c, "rcon")) { + SVC_RemoteCommand( from, msg ); + } else if (!Q_stricmp(c, "disconnect")) { + // if a client starts up a local server, we may see some spurious + // server disconnect messages when their new server sees our final + // sequenced messages to the old client + } else { + Com_DPrintf ("bad connectionless packet from %s:\n%s\n" + , NET_AdrToString (from), s); + } +} + +//============================================================================ + +/* +================= +SV_ReadPackets +================= +*/ +void SV_PacketEvent( netadr_t from, msg_t *msg ) { + int i; + client_t *cl; + int qport; + + // check for connectionless packet (0xffffffff) first + if ( msg->cursize >= 4 && *(int *)msg->data == -1) { + SV_ConnectionlessPacket( from, msg ); + return; + } + + // read the qport out of the message so we can fix up + // stupid address translating routers + MSG_BeginReadingOOB( msg ); + MSG_ReadLong( msg ); // sequence number + qport = MSG_ReadShort( msg ) & 0xffff; + + // find which client the message is from + for (i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + if (cl->state == CS_FREE) { + continue; + } + if ( !NET_CompareBaseAdr( from, cl->netchan.remoteAddress ) ) { + continue; + } + // it is possible to have multiple clients from a single IP + // address, so they are differentiated by the qport variable + if (cl->netchan.qport != qport) { + continue; + } + + // the IP port can't be used to differentiate them, because + // some address translating routers periodically change UDP + // port assignments + if (cl->netchan.remoteAddress.port != from.port) { + Com_Printf( "SV_PacketEvent: fixing up a translated port\n" ); + cl->netchan.remoteAddress.port = from.port; + } + + // make sure it is a valid, in sequence packet + if (SV_Netchan_Process(cl, msg)) { + // zombie clients still need to do the Netchan_Process + // to make sure they don't need to retransmit the final + // reliable message, but they don't do any other processing + if (cl->state != CS_ZOMBIE) { + cl->lastPacketTime = svs.time; // don't timeout + SV_ExecuteClientMessage( cl, msg ); + } + } + return; + } + + // if we received a sequenced packet from an address we don't recognize, + // send an out of band disconnect packet to it + NET_OutOfBandPrint( NS_SERVER, from, "disconnect" ); +} + + +/* +=================== +SV_CalcPings + +Updates the cl->ping variables +=================== +*/ +void SV_CalcPings( void ) { + int i, j; + client_t *cl; + int total, count; + int delta; + playerState_t *ps; + + for (i=0 ; i < sv_maxclients->integer ; i++) { + cl = &svs.clients[i]; + if ( cl->state != CS_ACTIVE ) { + cl->ping = 999; + continue; + } + if ( !cl->gentity ) { + cl->ping = 999; + continue; + } + if ( cl->gentity->r.svFlags & SVF_BOT ) { + cl->ping = 0; + continue; + } + + total = 0; + count = 0; + for ( j = 0 ; j < PACKET_BACKUP ; j++ ) { + if ( cl->frames[j].messageAcked <= 0 ) { + continue; + } + delta = cl->frames[j].messageAcked - cl->frames[j].messageSent; + count++; + total += delta; + } + if (!count) { + cl->ping = 999; + } else { + cl->ping = total/count; + if ( cl->ping > 999 ) { + cl->ping = 999; + } + } + + // let the game dll know about the ping + ps = SV_GameClientNum( i ); + ps->ping = cl->ping; + } +} + +/* +================== +SV_CheckTimeouts + +If a packet has not been received from a client for timeout->integer +seconds, drop the conneciton. Server time is used instead of +realtime to avoid dropping the local client while debugging. + +When a client is normally dropped, the client_t goes into a zombie state +for a few seconds to make sure any final reliable message gets resent +if necessary +================== +*/ +void SV_CheckTimeouts( void ) { + int i; + client_t *cl; + int droppoint; + int zombiepoint; + + droppoint = svs.time - 1000 * sv_timeout->integer; + zombiepoint = svs.time - 1000 * sv_zombietime->integer; + + for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + // message times may be wrong across a changelevel + if (cl->lastPacketTime > svs.time) { + cl->lastPacketTime = svs.time; + } + + if (cl->state == CS_ZOMBIE + && cl->lastPacketTime < zombiepoint) { + // using the client id cause the cl->name is empty at this point + Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i ); + cl->state = CS_FREE; // can now be reused + continue; + } + if ( cl->state >= CS_CONNECTED && cl->lastPacketTime < droppoint) { + // wait several frames so a debugger session doesn't + // cause a timeout + if ( ++cl->timeoutCount > 5 ) { + SV_DropClient (cl, "timed out"); + cl->state = CS_FREE; // don't bother with zombie state + } + } else { + cl->timeoutCount = 0; + } + } +} + + +/* +================== +SV_CheckPaused +================== +*/ +qboolean SV_CheckPaused( void ) { + int count; + client_t *cl; + int i; + + if ( !cl_paused->integer ) { + return qfalse; + } + + // only pause if there is just a single client connected + count = 0; + for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + if ( cl->state >= CS_CONNECTED && cl->netchan.remoteAddress.type != NA_BOT ) { + count++; + } + } + + if ( count > 1 ) { + // don't pause + if (sv_paused->integer) + Cvar_Set("sv_paused", "0"); + return qfalse; + } + + if (!sv_paused->integer) + Cvar_Set("sv_paused", "1"); + return qtrue; +} + +/* +================== +SV_Frame + +Player movement occurs as a result of packet events, which +happen before SV_Frame is called +================== +*/ +void SV_Frame( int msec ) { + int frameMsec; + int startTime; + + // the menu kills the server with this cvar + if ( sv_killserver->integer ) { + SV_Shutdown ("Server was killed.\n"); + Cvar_Set( "sv_killserver", "0" ); + return; + } + + if ( !com_sv_running->integer ) { + return; + } + + // allow pause if only the local client is connected + if ( SV_CheckPaused() ) { + return; + } + + // if it isn't time for the next frame, do nothing + if ( sv_fps->integer < 1 ) { + Cvar_Set( "sv_fps", "10" ); + } + frameMsec = 1000 / sv_fps->integer ; + + sv.timeResidual += msec; + + if (!com_dedicated->integer) SV_BotFrame( svs.time + sv.timeResidual ); + + if ( com_dedicated->integer && sv.timeResidual < frameMsec ) { + // NET_Sleep will give the OS time slices until either get a packet + // or time enough for a server frame has gone by + NET_Sleep(frameMsec - sv.timeResidual); + return; + } + + // if time is about to hit the 32nd bit, kick all clients + // and clear sv.time, rather + // than checking for negative time wraparound everywhere. + // 2giga-milliseconds = 23 days, so it won't be too often + if ( svs.time > 0x70000000 ) { + SV_Shutdown( "Restarting server due to time wrapping" ); + Cbuf_AddText( "vstr nextmap\n" ); + return; + } + // this can happen considerably earlier when lots of clients play and the map doesn't change + if ( svs.nextSnapshotEntities >= 0x7FFFFFFE - svs.numSnapshotEntities ) { + SV_Shutdown( "Restarting server due to numSnapshotEntities wrapping" ); + Cbuf_AddText( "vstr nextmap\n" ); + return; + } + + if( sv.restartTime && svs.time >= sv.restartTime ) { + sv.restartTime = 0; + Cbuf_AddText( "map_restart 0\n" ); + return; + } + + // update infostrings if anything has been changed + if ( cvar_modifiedFlags & CVAR_SERVERINFO ) { + SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) ); + cvar_modifiedFlags &= ~CVAR_SERVERINFO; + } + if ( cvar_modifiedFlags & CVAR_SYSTEMINFO ) { + SV_SetConfigstring( CS_SYSTEMINFO, Cvar_InfoString_Big( CVAR_SYSTEMINFO ) ); + cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; + } + + if ( com_speeds->integer ) { + startTime = Sys_Milliseconds (); + } else { + startTime = 0; // quite a compiler warning + } + + // update ping based on the all received frames + SV_CalcPings(); + + if (com_dedicated->integer) SV_BotFrame( svs.time ); + + // run the game simulation in chunks + while ( sv.timeResidual >= frameMsec ) { + sv.timeResidual -= frameMsec; + + if ( SVD_IsPaused() ) { + // demo paused: freeze svs.time so trajectories freeze + // and client doesn't see time jumps on unpause. + // still run game frame for spectator movement (at frozen time). + VM_Call( gvm, GAME_RUN_FRAME, svs.time ); + continue; + } + + svs.time += frameMsec; + + if ( SVD_IsPlaying() ) { + // demo playback: read recorded entities instead of running game logic + SVD_PlaybackFrame(); + // still call the game frame for spectator movement + } + + // let everything in the world think and move + VM_Call( gvm, GAME_RUN_FRAME, svs.time ); + + // capture frame for demo recording + SVD_RecordFrame(); + } + + if ( com_speeds->integer ) { + time_game = Sys_Milliseconds () - startTime; + } + + // check timeouts (skip during demo playback -- zombie slots would be freed) + if ( !SVD_IsPlaying() ) { + SV_CheckTimeouts(); + } + + // send messages back to the clients + SV_SendClientMessages(); + + // send a heartbeat to the master if needed + SV_MasterHeartbeat(); +} + +//============================================================================ + diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index ab5f82e..cf31610 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -1,1263 +1,1263 @@ -/* -=========================================================================== -Server-side demo recording and playback (netdemo). - -Records the full entity state array each server frame so that -demos can be played back from any viewpoint. During playback the -recorded entities are injected into sv.gentities and the normal -snapshot pipeline delivers them to a spectator client. -=========================================================================== -*/ - -#include "server.h" - -// Cvar_Set2 not in public header but needed to force-set CVAR_ROM cvars -extern cvar_t *Cvar_Set2( const char *var_name, const char *value, qboolean force ); - -// --------------------------------------------------------------- -// File format -// --------------------------------------------------------------- -// -// Header: -// 4 bytes magic "SVDM" -// 4 bytes version (1) -// 4 bytes original sv_maxclients -// 4 bytes original sv_fps -// 64 bytes map name (null-padded) -// Then: configstrings block -// for each non-empty configstring: -// 2 bytes index -// 2 bytes string length (incl NUL) -// N bytes string data -// 2 bytes index = 0xFFFF (terminator) -// -// Per frame: -// 4 bytes serverTime -// 2 bytes numEntities (how many entity records follow) -// for each entity: -// 2 bytes entity number -// entityState_t (fixed size, raw) -// 4 bytes svFlags -// 4 bytes linked -// 12 bytes currentOrigin[3] -// 12 bytes absmin[3] -// 12 bytes absmax[3] -// 2 bytes numConfigChanges -// for each change: -// 2 bytes index -// 2 bytes string length (incl NUL) -// N bytes string data -// -// Footer: -// 4 bytes serverTime = -1 (end marker) -// - -#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24)) -#define SVDEMO_VERSION 3 // v3: removed PVS data, svFlags only -#define SVDEMO_MAX_MAPNAME 64 - -// header flags - -// --------------------------------------------------------------- -// State -// --------------------------------------------------------------- - -// per-entity data stored for delta compression -typedef struct { - entityState_t es; - int svFlags; - 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; - -#define SVD_MAX_SERVERCMDS 64 -#define SVD_MAX_SERVERCMD_LEN 1024 - -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 - - // buffered server commands for current frame - char serverCmds[SVD_MAX_SERVERCMDS][SVD_MAX_SERVERCMD_LEN]; - int numServerCmds; - qboolean mapRestarted; // set by SVD_ResetDeltaState, written as frame flag - qboolean isKeyframe; // next frame is a keyframe (delta from baseline) - int keyframeInterval; // frames between keyframes (0 = disabled) - int framesSinceKeyframe; // counter for next keyframe - - // playback - fileHandle_t playFile; - qboolean playing; - int playMaxClients; // original maxclients from the recording - int playFps; // original sv_fps - char *savedConfigstrings[MAX_CONFIGSTRINGS]; // from demo header, re-applied after map load - char playMapName[SVDEMO_MAX_MAPNAME]; - qboolean endOfDemo; - qboolean needConfigstrings; // apply saved configstrings on first frame - qboolean starting; // SVD_Play_f is running devmap internally - qboolean paused; - qboolean seeked; // just seeked, next frame needs RESET - svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read - svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states - - // keyframe index (shared by recording and playback) - int numKeyframes; - int maxKeyframes; // allocated size - int *keyframeTimes; // serverTime of each keyframe - int *keyframeOffsets; // file offset of each keyframe -} svDemo_t; - -static svDemo_t demo; -// --------------------------------------------------------------- -// Recording helpers -// --------------------------------------------------------------- - -static void SVD_WriteInt( fileHandle_t f, int v ) { - FS_Write( &v, 4, f ); -} - -static void SVD_WriteShort( fileHandle_t f, short v ) { - FS_Write( &v, 2, f ); -} - -static int SVD_ReadInt( fileHandle_t f ) { - int v = 0; - FS_Read( &v, 4, f ); - return v; -} - -static short SVD_ReadShort( fileHandle_t f ) { - short v = 0; - FS_Read( &v, 2, f ); - return v; -} - -// --------------------------------------------------------------- -// Write header -// --------------------------------------------------------------- - -static void SVD_WriteHeader( fileHandle_t f ) { - int i; - char mapBuf[SVDEMO_MAX_MAPNAME]; - - SVD_WriteInt( f, SVDEMO_MAGIC ); - SVD_WriteInt( f, SVDEMO_VERSION ); - SVD_WriteInt( f, 0 ); // flags (reserved) - SVD_WriteInt( f, sv_maxclients->integer ); - SVD_WriteInt( f, sv_fps->integer ); - - // map name - memset( mapBuf, 0, sizeof(mapBuf) ); - Q_strncpyz( mapBuf, sv.configstrings[CS_SERVERINFO], sizeof(mapBuf) ); - // actually store the mapname from CS_SERVERINFO... or just the map name - { - const char *mapname = Cvar_VariableString("mapname"); - memset( mapBuf, 0, sizeof(mapBuf) ); - Q_strncpyz( mapBuf, mapname, sizeof(mapBuf) ); - } - FS_Write( mapBuf, SVDEMO_MAX_MAPNAME, f ); - - // configstrings - for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { - if ( sv.configstrings[i] && sv.configstrings[i][0] ) { - int len = strlen( sv.configstrings[i] ) + 1; - SVD_WriteShort( f, (short)i ); - SVD_WriteShort( f, (short)len ); - FS_Write( sv.configstrings[i], len, f ); - - // store initial copy for delta detection - if ( demo.lastConfigstrings[i] ) { - Z_Free( demo.lastConfigstrings[i] ); - } - demo.lastConfigstrings[i] = CopyString( sv.configstrings[i] ); - } - } - // terminator - SVD_WriteShort( f, (short)0xFFFF ); -} - -// --------------------------------------------------------------- -// Write one frame -// --------------------------------------------------------------- - -static void SVD_WriteFrame( fileHandle_t f ) { - int i; - sharedEntity_t *ent; - short numChanges; - msg_t msg; - static byte msgBuf[MAX_GENTITIES * 300]; // worst case: all entities full write from baseline - - SVD_WriteInt( f, svs.time ); - SVD_WriteShort( f, (short)sv.num_entities ); - - // frame flags: bit 0 = map restarted, bit 1 = keyframe - { - byte frameFlags = 0; - if ( demo.mapRestarted ) { - frameFlags |= 1; - demo.mapRestarted = qfalse; - } - if ( demo.isKeyframe ) { - frameFlags |= 2; - demo.isKeyframe = qfalse; - } - FS_Write( &frameFlags, 1, f ); - } - - // delta-compress all entities into a message buffer - MSG_Init( &msg, msgBuf, sizeof(msgBuf) ); - - for ( i = 0; i < sv.num_entities; i++ ) { - qboolean active; - ent = SV_GentityNum( i ); - active = ( ent->r.linked || ent->s.eType != 0 ); - - if ( active ) { - entityState_t baseline; - entityState_t *from; - if ( demo.prevEntities[i].active ) { - from = &demo.prevEntities[i].es; - } else { - Com_Memset( &baseline, 0, sizeof(baseline) ); - baseline.number = i; - from = &baseline; - } - MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue ); - - // write svFlags only if changed (rarely changes) - if ( ent->r.svFlags != demo.prevEntities[i].svFlags ) { - MSG_WriteBits( &msg, 1, 1 ); - MSG_WriteLong( &msg, ent->r.svFlags ); - } else { - MSG_WriteBits( &msg, 0, 1 ); - } - - // update prev state - demo.prevEntities[i].es = ent->s; - demo.prevEntities[i].svFlags = ent->r.svFlags; - demo.prevEntities[i].active = qtrue; - } else if ( demo.prevEntities[i].active ) { - // entity was removed — write a remove marker - MSG_WriteDeltaEntity( &msg, &demo.prevEntities[i].es, NULL, qtrue ); - demo.prevEntities[i].active = qfalse; - } - // else: entity was inactive and still inactive — write nothing - } - - // end of entities marker - MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS ); - - // write entity message to file - SVD_WriteInt( f, msg.cursize ); - FS_Write( msg.data, msg.cursize, f ); - - // write player states (delta compressed) - { - msg_t psmsg; - static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline - 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 ); - qboolean isSpectator; - playerState_t specPs; - - // detect spectators: free cam or follow mode - isSpectator = active && ( ps->pm_type == PM_SPECTATOR || (ps->pm_flags & PMF_FOLLOW) ); - - // for spectators, record a sanitized ps so they appear on - // the scoreboard as spectators (follow mode corrupts their ps - // with the followed player's data) - if ( isSpectator ) { - Com_Memset( &specPs, 0, sizeof(specPs) ); - specPs.commandTime = ps->commandTime; - specPs.pm_type = PM_SPECTATOR; - specPs.persistant[PERS_TEAM] = TEAM_SPECTATOR; - specPs.clientNum = i; - ps = &specPs; - } - - 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 ); - - SVD_WriteInt( f, psmsg.cursize ); - FS_Write( psmsg.data, psmsg.cursize, f ); - } - - // configstring changes - numChanges = 0; - for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { - const char *cur = sv.configstrings[i] ? sv.configstrings[i] : ""; - const char *old = demo.lastConfigstrings[i] ? demo.lastConfigstrings[i] : ""; - if ( strcmp( cur, old ) != 0 ) { - numChanges++; - } - } - SVD_WriteShort( f, numChanges ); - - if ( numChanges > 0 ) { - for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { - const char *cur = sv.configstrings[i] ? sv.configstrings[i] : ""; - const char *old = demo.lastConfigstrings[i] ? demo.lastConfigstrings[i] : ""; - if ( strcmp( cur, old ) != 0 ) { - int len = strlen( cur ) + 1; - SVD_WriteShort( f, (short)i ); - SVD_WriteShort( f, (short)len ); - FS_Write( cur, len, f ); - - if ( demo.lastConfigstrings[i] ) { - Z_Free( demo.lastConfigstrings[i] ); - } - demo.lastConfigstrings[i] = CopyString( cur ); - } - } - } - - // write buffered server commands (chat, prints, etc.) - SVD_WriteShort( f, (short)demo.numServerCmds ); - for ( i = 0; i < demo.numServerCmds; i++ ) { - short len = (short)( strlen( demo.serverCmds[i] ) + 1 ); - SVD_WriteShort( f, len ); - FS_Write( demo.serverCmds[i], len, f ); - } - demo.numServerCmds = 0; -} - -// --------------------------------------------------------------- -// Recording commands -// --------------------------------------------------------------- - -/* -Start recording a demo with the given name. -Returns qtrue on success. -*/ -static qboolean SVD_StartRecording( const char *demoname ) { - char path[MAX_OSPATH]; - - if ( demo.recording ) { - Com_Printf( "Already recording a server demo.\n" ); - return qfalse; - } - - if ( sv.state != SS_GAME ) { - Com_Printf( "Not running a server.\n" ); - return qfalse; - } - - Com_sprintf( path, sizeof(path), "svdemos/%s.svdm", demoname ); - - demo.recordFile = FS_FOpenFileWrite( path ); - if ( !demo.recordFile ) { - Com_Printf( "ERROR: couldn't open %s for writing.\n", path ); - return qfalse; - } - - Com_Printf( "Recording server demo to %s\n", path ); - demo.recording = qtrue; - - // clear delta state for fresh recording - Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); - Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); - - // keyframe interval from cvar (seconds to frames at sv_fps) - { - int secs = Cvar_VariableIntegerValue( "svdemo_keyframeInterval" ); - if ( secs > 0 ) { - demo.keyframeInterval = secs * sv_fps->integer; - } else { - demo.keyframeInterval = 0; - } - // first frame is always a keyframe (makes beginning seekable) - demo.framesSinceKeyframe = demo.keyframeInterval; - demo.numKeyframes = 0; - } - - SVD_WriteHeader( demo.recordFile ); - return qtrue; -} - -void SVD_Record_f( void ) { - char *s; - - s = Cmd_Argv(1); - if ( !s[0] ) { - Com_Printf( "Usage: svdemo_record \n" ); - return; - } - - SVD_StartRecording( s ); -} - -/* -Auto-record: called from SV_SpawnServer after the map is fully loaded. -Generates a name from map name + timestamp. -*/ -void SVD_AutoRecord( void ) { - char demoname[MAX_OSPATH]; - const char *mapname; - qtime_t now; - - if ( demo.recording || demo.playing ) { - return; - } - - if ( !Cvar_VariableIntegerValue( "svdemo_autorecord" ) ) { - return; - } - - if ( sv.state != SS_GAME ) { - return; - } - - mapname = Cvar_VariableString( "mapname" ); - Com_RealTime( &now ); - Com_sprintf( demoname, sizeof(demoname), "%s_%04d%02d%02d_%02d%02d%02d", - mapname, - 1900 + now.tm_year, 1 + now.tm_mon, now.tm_mday, - now.tm_hour, now.tm_min, now.tm_sec ); - - SVD_StartRecording( demoname ); -} - -void SVD_StopRecord_f( void ) { - int i; - - if ( !demo.recording ) { - Com_Printf( "Not recording a server demo.\n" ); - return; - } - - // write end marker - SVD_WriteInt( demo.recordFile, -1 ); - - // write keyframe index after the end marker. - // layout: [numKf][time0 off0 time1 off1 ...][numKf_copy] - // numKf_copy at the very end lets playback find the table - // by seeking to fileLen - 4. - { - int kf; - SVD_WriteInt( demo.recordFile, demo.numKeyframes ); - for ( kf = 0; kf < demo.numKeyframes; kf++ ) { - SVD_WriteInt( demo.recordFile, demo.keyframeTimes[kf] ); - SVD_WriteInt( demo.recordFile, demo.keyframeOffsets[kf] ); - } - SVD_WriteInt( demo.recordFile, demo.numKeyframes ); // copy at end - Com_Printf( "Wrote %d keyframes.\n", demo.numKeyframes ); - } - - FS_FCloseFile( demo.recordFile ); - demo.recordFile = 0; - demo.recording = qfalse; - - // free configstring tracking - for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { - if ( demo.lastConfigstrings[i] ) { - Z_Free( demo.lastConfigstrings[i] ); - demo.lastConfigstrings[i] = NULL; - } - } - - // free keyframe index - if ( demo.keyframeTimes ) { Z_Free( demo.keyframeTimes ); demo.keyframeTimes = NULL; } - if ( demo.keyframeOffsets ) { Z_Free( demo.keyframeOffsets ); demo.keyframeOffsets = NULL; } - demo.numKeyframes = demo.maxKeyframes = 0; - - Com_Printf( "Server demo recording stopped.\n" ); -} - -/* -Called from SV_Frame() after the game has run its frame. -*/ -/* -Reset delta compression state. Call on map_restart so the next -recorded frame writes full entity/player states from baseline. -*/ -/* -Capture a broadcast server command for the current frame. -Called from SV_SendServerCommand when cl == NULL (broadcast). -*/ -void SVD_CaptureServerCommand( const char *cmd ) { - int i; - - if ( !demo.recording ) { - return; - } - if ( demo.numServerCmds >= SVD_MAX_SERVERCMDS ) { - return; // overflow, drop command - } - - // deduplicate: per-client chat is sent N times (once per client), - // only store the first occurrence - for ( i = 0; i < demo.numServerCmds; i++ ) { - if ( !strcmp( demo.serverCmds[i], cmd ) ) { - return; - } - } - - Q_strncpyz( demo.serverCmds[demo.numServerCmds], cmd, SVD_MAX_SERVERCMD_LEN ); - demo.numServerCmds++; -} - -void SVD_ResetDeltaState( void ) { - if ( !demo.recording ) { - return; - } - Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); - Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); - demo.mapRestarted = qtrue; // signal next frame to write restart marker -} - -void SVD_RecordFrame( void ) { - if ( !demo.recording ) { - return; - } - - // periodic keyframe: reset delta state so this frame is decodable - // from baseline. record file offset for the keyframe index. - if ( demo.keyframeInterval > 0 ) { - demo.framesSinceKeyframe++; - if ( demo.framesSinceKeyframe >= demo.keyframeInterval ) { - demo.framesSinceKeyframe = 0; - demo.isKeyframe = qtrue; - Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); - Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); - // store keyframe: file offset before writing, serverTime - if ( demo.numKeyframes >= demo.maxKeyframes ) { - int newMax = demo.maxKeyframes ? demo.maxKeyframes * 2 : 256; - int *newTimes = Z_Malloc( newMax * sizeof(int) ); - int *newOffsets = Z_Malloc( newMax * sizeof(int) ); - if ( demo.keyframeTimes ) { - Com_Memcpy( newTimes, demo.keyframeTimes, demo.numKeyframes * sizeof(int) ); - Com_Memcpy( newOffsets, demo.keyframeOffsets, demo.numKeyframes * sizeof(int) ); - Z_Free( demo.keyframeTimes ); - Z_Free( demo.keyframeOffsets ); - } - demo.keyframeTimes = newTimes; - demo.keyframeOffsets = newOffsets; - demo.maxKeyframes = newMax; - } - demo.keyframeTimes[demo.numKeyframes] = svs.time; - demo.keyframeOffsets[demo.numKeyframes] = FS_FTell( demo.recordFile ); - demo.numKeyframes++; - } - } - - SVD_WriteFrame( demo.recordFile ); -} - -// --------------------------------------------------------------- -// Playback: read header -// --------------------------------------------------------------- - -static qboolean SVD_ReadHeader( fileHandle_t f ) { - int magic, version; - - magic = SVD_ReadInt( f ); - if ( magic != SVDEMO_MAGIC ) { - Com_Printf( "Not a valid server demo file.\n" ); - return qfalse; - } - - version = SVD_ReadInt( f ); - if ( version != SVDEMO_VERSION ) { - Com_Printf( "Unsupported server demo version %d.\n", version ); - return qfalse; - } - - SVD_ReadInt( f ); // flags (reserved) - - demo.playMaxClients = SVD_ReadInt( f ); - demo.playFps = SVD_ReadInt( f ); - FS_Read( demo.playMapName, SVDEMO_MAX_MAPNAME, f ); - demo.playMapName[SVDEMO_MAX_MAPNAME - 1] = '\0'; - - // read configstrings — store for re-application after map load - { - short idx; - while (1) { - idx = SVD_ReadShort( f ); - if ( idx == (short)0xFFFF ) { - break; - } - { - short len = SVD_ReadShort( f ); - char buf[BIG_INFO_STRING]; - if ( len > 0 && len < (short)sizeof(buf) ) { - FS_Read( buf, len, f ); - buf[len - 1] = '\0'; - if ( demo.savedConfigstrings[idx] ) { - Z_Free( demo.savedConfigstrings[idx] ); - } - demo.savedConfigstrings[idx] = CopyString( buf ); - } - } - } - } - - return qtrue; -} - -/* -Re-apply recorded configstrings after the map has loaded. -Called after devmap finishes in SVD_Play_f. -*/ -static void SVD_ApplyConfigstrings( void ) { - int i; - for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { - // skip CS_SERVERINFO and CS_SYSTEMINFO — they contain latched cvars - // (sv_maxclients, sv_pure, etc.) that would trigger a map restart - if ( i == CS_SERVERINFO || i == CS_SYSTEMINFO ) { - continue; - } - if ( demo.savedConfigstrings[i] && demo.savedConfigstrings[i][0] ) { - SV_SetConfigstring( i, demo.savedConfigstrings[i] ); - } - } -} - -// --------------------------------------------------------------- -// Playback: read one frame, populate sv.gentities -// --------------------------------------------------------------- - -static qboolean SVD_ReadFrame( fileHandle_t f ) { - int serverTime; - short numEnts, numChanges; - int i, entNum, blockLen; - sharedEntity_t *ent; - msg_t msg; - static byte msgBuf[MAX_GENTITIES * 300]; - entityState_t newEs; - - serverTime = SVD_ReadInt( f ); - if ( serverTime == -1 ) { - return qfalse; // end of demo - } - - // set svs.time to recorded time so entity trajectory interpolation - // works correctly (rockets, grenades, etc. use pos.trTime relative - // to server time). zombie timeout is already skipped during playback. - svs.time = serverTime; - numEnts = SVD_ReadShort( f ); - - // read frame flags - { - byte frameFlags; - FS_Read( &frameFlags, 1, f ); - if ( frameFlags & 3 ) { - // bit 0 = map restart, bit 1 = keyframe. - // both mean: delta state was reset during recording, - // so reset playback delta state to decode from baseline. - Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); - Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); - } - if ( ( frameFlags & 1 ) || demo.seeked ) { - // map restart or seek: reset entity interpolation in cgame - svs.snapFlagServerBit |= SNAPFLAG_RESET_ENTITIES; - demo.seeked = qfalse; - } - } - - // read entity message - blockLen = SVD_ReadInt( f ); - if ( blockLen <= 0 || blockLen > (int)sizeof(msgBuf) ) { - return qfalse; - } - FS_Read( msgBuf, blockLen, f ); - MSG_Init( &msg, msgBuf, sizeof(msgBuf) ); - msg.cursize = blockLen; - - // clear all entities (spectator's entity is recreated by ClientThink_real) - for ( i = 0; i < sv.num_entities; i++ ) { - ent = SV_GentityNum( i ); - if ( ent->r.linked ) { - SV_UnlinkEntity( ent ); - } - ent->s.eType = 0; - ent->s.number = i; - } - - // parse delta-compressed entities - while ( 1 ) { - entNum = MSG_ReadBits( &msg, GENTITYNUM_BITS ); - if ( entNum == MAX_GENTITIES - 1 ) { - break; // end of entities marker - } - if ( msg.readcount > msg.cursize ) { - break; - } - - { - entityState_t baseline; - entityState_t *from; - Com_Memset( &newEs, 0, sizeof(newEs) ); - if ( demo.playPrevEntities[entNum].active ) { - from = &demo.playPrevEntities[entNum].es; - } else { - Com_Memset( &baseline, 0, sizeof(baseline) ); - baseline.number = entNum; - from = &baseline; - } - MSG_ReadDeltaEntity( &msg, from, &newEs, entNum ); - } - - if ( newEs.number == MAX_GENTITIES - 1 ) { - // entity was removed - demo.playPrevEntities[entNum].active = qfalse; - continue; - } - - // read svFlags - if ( MSG_ReadBits( &msg, 1 ) ) { - demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg ); - } - - demo.playPrevEntities[entNum].es = newEs; - demo.playPrevEntities[entNum].active = qtrue; - - // apply to server entity and link for PVS. - // use trBase as initial origin — G_RunFrame will refine with - // BG_EvaluateTrajectory for moving entities (rockets etc). - ent = SV_GentityNum( entNum ); - ent->s = newEs; - ent->s.number = entNum; - ent->r.svFlags = demo.playPrevEntities[entNum].svFlags; - VectorCopy( newEs.pos.trBase, ent->r.currentOrigin ); - ent->r.linked = qtrue; - SV_LinkEntity( ent ); - - if ( entNum + 1 > sv.num_entities ) { - sv.num_entities = entNum + 1; - } - } - - // read player states (delta compressed) - { - msg_t psmsg; - static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline - 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 - { - playerState_t *gamePs = SV_GameClientNum( clientNum ); - *gamePs = newPs; - } - } else { - demo.playPrevPlayers[clientNum].active = qfalse; - // clear game playerState so G_RunFrame sees commandTime=0 - { - playerState_t *gamePs = SV_GameClientNum( clientNum ); - Com_Memset( gamePs, 0, sizeof(*gamePs) ); - } - } - } - } - } - - // read configstring changes (skip SERVERINFO/SYSTEMINFO to avoid latch restarts) - numChanges = SVD_ReadShort( f ); - for ( i = 0; i < numChanges; i++ ) { - short idx = SVD_ReadShort( f ); - short len = SVD_ReadShort( f ); - char buf[BIG_INFO_STRING]; - if ( len > 0 && len < (short)sizeof(buf) ) { - FS_Read( buf, len, f ); - buf[len - 1] = '\0'; - if ( idx != CS_SERVERINFO && idx != CS_SYSTEMINFO ) { - SV_SetConfigstring( idx, buf ); - } - } - } - - // read server commands (chat, prints, etc.) and replay to spectator - { - short numCmds = SVD_ReadShort( f ); - for ( i = 0; i < numCmds; i++ ) { - short len = SVD_ReadShort( f ); - char buf[SVD_MAX_SERVERCMD_LEN]; - if ( len > 0 && len < (short)sizeof(buf) ) { - FS_Read( buf, len, f ); - buf[len - 1] = '\0'; - // broadcast — only the spectator is CS_ACTIVE, zombies are skipped - SV_SendServerCommand( NULL, "%s", buf ); - } - } - } - - return qtrue; -} - -// --------------------------------------------------------------- -// Playback commands -// --------------------------------------------------------------- - -void SVD_Play_f( void ) { - char name[MAX_OSPATH]; - char *s; - int len; - - if ( demo.recording ) { - Com_Printf( "Stop recording first (svdemo_stop).\n" ); - return; - } - - s = Cmd_Argv(1); - if ( !s[0] ) { - Com_Printf( "Usage: svdemo_play \n" ); - return; - } - - // stop current playback if switching demos - if ( demo.playing ) { - SVD_CleanupPlayback(); - } - - Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s ); - - memset( &demo, 0, sizeof(demo) ); - - len = FS_FOpenFileRead( name, &demo.playFile, qtrue ); - if ( !demo.playFile || len <= 0 ) { - Com_Printf( "ERROR: couldn't open %s.\n", name ); - return; - } - - if ( !SVD_ReadHeader( demo.playFile ) ) { - FS_FCloseFile( demo.playFile ); - demo.playFile = 0; - return; - } - - // read keyframe index from the end of the file. - // layout: [frames][-1][numKf][time0 off0 ...][numKf_copy] - // last 4 bytes of file = numKf_copy. - { - int frameStart = FS_FTell( demo.playFile ); - int numKf, kf; - - FS_Seek( demo.playFile, len - 4, FS_SEEK_SET ); - numKf = SVD_ReadInt( demo.playFile ); - - if ( numKf > 0 && numKf < 1000000 ) { - // seek to start of keyframe table: end - 4 - numKf*8 - 4 - int tableStart = len - 4 - numKf * 8 - 4; - FS_Seek( demo.playFile, tableStart + 4, FS_SEEK_SET ); // skip numKf - - demo.numKeyframes = numKf; - demo.maxKeyframes = numKf; - demo.keyframeTimes = Z_Malloc( numKf * sizeof(int) ); - demo.keyframeOffsets = Z_Malloc( numKf * sizeof(int) ); - - for ( kf = 0; kf < numKf; kf++ ) { - demo.keyframeTimes[kf] = SVD_ReadInt( demo.playFile ); - demo.keyframeOffsets[kf] = SVD_ReadInt( demo.playFile ); - } - Com_Printf( "Loaded %d keyframes.\n", numKf ); - } - - // seek back to start of frame data - FS_Seek( demo.playFile, frameStart, FS_SEEK_SET ); - } - - demo.playing = qtrue; - demo.endOfDemo = qfalse; - demo.needConfigstrings = qtrue; - - Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n", - demo.playMapName, demo.playMaxClients, demo.playFps ); - - // Shut down current server first so no clients carry over to - // reserved slots. SV_Shutdown triggers our cleanup hook, but - // demo.starting prevents it from clearing our state. - demo.starting = qtrue; - if ( com_sv_running->integer ) { - SV_Shutdown( "Demo playback\n" ); - } - - // Set demo cvar BEFORE devmap so G_InitGame can read it. - // The previous server is gone so no old game module to conflict with. - Cvar_Set2( "sv_demoplaying", "1", qtrue ); - - Cbuf_ExecuteText( EXEC_NOW, va("set sv_maxclients %d\n", MAX_CLIENTS) ); - Cbuf_ExecuteText( EXEC_NOW, va("set sv_fps %d\n", demo.playFps) ); - Cbuf_ExecuteText( EXEC_NOW, va("devmap %s\n", demo.playMapName) ); - demo.starting = qfalse; - - // CS_SVDEMO configstring is set by G_InitGame from the cvar - - // Reserve recorded player slots. Server is fresh (SV_Shutdown cleared - // old clients), local client hasn't connected yet. - { - int i; - for ( i = 0; i < demo.playMaxClients; i++ ) { - if ( svs.clients[i].state == CS_FREE ) { - svs.clients[i].state = CS_ZOMBIE; - svs.clients[i].rate = 10000; - svs.clients[i].nextSnapshotTime = 0x7FFFFFFF; - svs.clients[i].lastPacketTime = svs.time; - } - } - } -} - -void SVD_CleanupPlayback( void ) { - int i; - - if ( !demo.playing ) { - return; - } - - FS_FCloseFile( demo.playFile ); - - // free saved configstrings - for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { - if ( demo.savedConfigstrings[i] ) { - Z_Free( demo.savedConfigstrings[i] ); - } - } - - // free zombie client slots - for ( i = 0; i < demo.playMaxClients; i++ ) { - if ( svs.clients[i].state == CS_ZOMBIE ) { - svs.clients[i].state = CS_FREE; - } - } - - // free keyframe index - if ( demo.keyframeTimes ) { Z_Free( demo.keyframeTimes ); } - if ( demo.keyframeOffsets ) { Z_Free( demo.keyframeOffsets ); } - - memset( &demo, 0, sizeof(demo) ); - Cvar_Set2( "sv_demoplaying", "0", qtrue ); -} - -void SVD_StopPlay_f( void ) { - if ( !demo.playing ) { - Com_Printf( "Not playing a server demo.\n" ); - return; - } - - SVD_CleanupPlayback(); - Com_Printf( "Server demo playback stopped.\n" ); - - // disconnect to return to main menu - Cbuf_ExecuteText( EXEC_APPEND, "disconnect\n" ); -} - -/* -Unified stop command: stops recording or playback, whichever is active. -*/ -void SVD_Stop_f( void ) { - if ( demo.recording ) { - SVD_StopRecord_f(); - } else if ( demo.playing ) { - SVD_StopPlay_f(); - } else { - Com_Printf( "Not recording or playing a server demo.\n" ); - } -} - -void SVD_Pause_f( void ) { - if ( !demo.playing ) { - Com_Printf( "Not playing a server demo.\n" ); - return; - } - demo.paused = !demo.paused; - if ( !demo.paused ) { - // resuming — toggle SERVERCOUNT to reset client snapshot timing - // (drifted during pause from identical serverTimes). - svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; - } - Com_Printf( "Demo playback %s.\n", demo.paused ? "paused" : "resumed" ); -} - -void SVD_Seek_f( void ) { - int targetTime, i, bestKf; - float seconds; - - if ( !demo.playing ) { - Com_Printf( "Not playing a server demo.\n" ); - return; - } - - if ( Cmd_Argc() < 2 ) { - Com_Printf( "Usage: svdemo_seek \n" ); - return; - } - - if ( demo.numKeyframes <= 0 ) { - Com_Printf( "No keyframes in this demo — seeking not available.\n" ); - return; - } - - seconds = atof( Cmd_Argv(1) ); - targetTime = svs.time + (int)(seconds * 1000); - - // find nearest keyframe at or before target time - bestKf = -1; - for ( i = 0; i < demo.numKeyframes; i++ ) { - if ( demo.keyframeTimes[i] <= targetTime ) { - bestKf = i; - } else { - break; - } - } - - if ( bestKf < 0 ) { - // target is before the first keyframe — seek to first - bestKf = 0; - targetTime = demo.keyframeTimes[0]; - } - - // seek to keyframe file position - FS_Seek( demo.playFile, demo.keyframeOffsets[bestKf], FS_SEEK_SET ); - - // reset delta state (keyframe is encoded from baseline) - Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); - Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); - - // set svs.time to the keyframe time so the SV_Frame loop - // doesn't advance from the old time before reading - svs.time = demo.keyframeTimes[bestKf]; - - // toggle SERVERCOUNT to reset client time delta - svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; - - demo.seeked = qtrue; - demo.endOfDemo = qfalse; - - // read the keyframe directly (works even when paused) - svs.snapFlagServerBit &= ~SNAPFLAG_RESET_ENTITIES; - if ( !SVD_ReadFrame( demo.playFile ) ) { - demo.endOfDemo = qtrue; - } - - // reset client snapshot timing - { - 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 (for G_RunFrame + snapshot) - sv.timeResidual = 1000 / sv_fps->integer; - - Com_Printf( "Seeked to time %d.\n", svs.time ); -} - -void SVD_SeekExact_f( void ) { - int targetTime, i, bestKf; - float seconds; - - if ( !demo.playing ) { - Com_Printf( "Not playing a server demo.\n" ); - return; - } - - if ( Cmd_Argc() < 2 ) { - Com_Printf( "Usage: svdemo_seekexact \n" ); - return; - } - - if ( demo.numKeyframes <= 0 ) { - Com_Printf( "No keyframes in this demo.\n" ); - return; - } - - seconds = atof( Cmd_Argv(1) ); - targetTime = svs.time + (int)(seconds * 1000); - - // find nearest keyframe at or before target time - bestKf = -1; - for ( i = 0; i < demo.numKeyframes; i++ ) { - if ( demo.keyframeTimes[i] <= targetTime ) { - bestKf = i; - } else { - break; - } - } - - if ( bestKf < 0 ) { - bestKf = 0; - targetTime = demo.keyframeTimes[0]; - } - - // seek to keyframe - FS_Seek( demo.playFile, demo.keyframeOffsets[bestKf], FS_SEEK_SET ); - Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); - Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); - svs.time = demo.keyframeTimes[bestKf]; - svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; - demo.seeked = qtrue; - demo.endOfDemo = qfalse; - - // read forward from keyframe to target time - while ( svs.time < targetTime ) { - if ( !SVD_ReadFrame( demo.playFile ) ) { - demo.endOfDemo = qtrue; - break; - } - } - - // reset client snapshot timing - { - int j; - for ( j = 0; j < sv_maxclients->integer; j++ ) { - if ( svs.clients[j].state >= CS_ACTIVE ) { - svs.clients[j].nextSnapshotTime = svs.time; - } - } - } - - sv.timeResidual = 1000 / sv_fps->integer; - - Com_Printf( "Seeked to time %d (read forward %d ms from keyframe).\n", - svs.time, svs.time - demo.keyframeTimes[bestKf] ); -} - -/* -Called from SV_Frame() to advance playback by one frame. -Returns qtrue if a frame was read, qfalse if demo ended. -*/ -qboolean SVD_PlaybackFrame( void ) { - if ( !demo.playing || demo.endOfDemo ) { - return qfalse; - } - - - // manual pause — don't consume demo data - if ( demo.paused ) { - return qfalse; - } - - // wait for a spectator to be fully in-game before starting playback. - // the server keeps running frames (so the connection handshake completes) - // but no demo data is consumed until someone is CS_ACTIVE. - if ( SVD_ShouldPause() ) { - return qfalse; - } - - // apply recorded configstrings once after map load - if ( demo.needConfigstrings ) { - SVD_ApplyConfigstrings(); - 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 ) ) { - Com_Printf( "Server demo playback finished.\n" ); - SVD_CleanupPlayback(); - Cbuf_ExecuteText( EXEC_APPEND, "disconnect\n" ); - return qfalse; - } - - return qtrue; -} - -// --------------------------------------------------------------- -// Queries -// --------------------------------------------------------------- - -qboolean SVD_IsRecording( void ) { - return demo.recording; -} - -qboolean SVD_IsPlaying( void ) { - return demo.playing; -} - -qboolean SVD_IsPaused( void ) { - return demo.playing && demo.paused; -} - -qboolean SVD_IsStarting( void ) { - return demo.starting; -} - - -/* -Returns qtrue if demo playback should pause (no active spectators). -Controlled by svdemo_pauseEmpty cvar. -*/ -qboolean SVD_ShouldPause( void ) { - int i; - - if ( !Cvar_VariableIntegerValue( "svdemo_pauseEmpty" ) ) { - return qfalse; - } - - for ( i = 0; i < sv_maxclients->integer; i++ ) { - if ( svs.clients[i].state == CS_ACTIVE ) { - return qfalse; // someone is in-game and watching - } - } - - return qtrue; // nobody connected, pause -} - +/* +=========================================================================== +Server-side demo recording and playback (netdemo). + +Records the full entity state array each server frame so that +demos can be played back from any viewpoint. During playback the +recorded entities are injected into sv.gentities and the normal +snapshot pipeline delivers them to a spectator client. +=========================================================================== +*/ + +#include "server.h" + +// Cvar_Set2 not in public header but needed to force-set CVAR_ROM cvars +extern cvar_t *Cvar_Set2( const char *var_name, const char *value, qboolean force ); + +// --------------------------------------------------------------- +// File format +// --------------------------------------------------------------- +// +// Header: +// 4 bytes magic "SVDM" +// 4 bytes version (1) +// 4 bytes original sv_maxclients +// 4 bytes original sv_fps +// 64 bytes map name (null-padded) +// Then: configstrings block +// for each non-empty configstring: +// 2 bytes index +// 2 bytes string length (incl NUL) +// N bytes string data +// 2 bytes index = 0xFFFF (terminator) +// +// Per frame: +// 4 bytes serverTime +// 2 bytes numEntities (how many entity records follow) +// for each entity: +// 2 bytes entity number +// entityState_t (fixed size, raw) +// 4 bytes svFlags +// 4 bytes linked +// 12 bytes currentOrigin[3] +// 12 bytes absmin[3] +// 12 bytes absmax[3] +// 2 bytes numConfigChanges +// for each change: +// 2 bytes index +// 2 bytes string length (incl NUL) +// N bytes string data +// +// Footer: +// 4 bytes serverTime = -1 (end marker) +// + +#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24)) +#define SVDEMO_VERSION 3 // v3: removed PVS data, svFlags only +#define SVDEMO_MAX_MAPNAME 64 + +// header flags + +// --------------------------------------------------------------- +// State +// --------------------------------------------------------------- + +// per-entity data stored for delta compression +typedef struct { + entityState_t es; + int svFlags; + 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; + +#define SVD_MAX_SERVERCMDS 64 +#define SVD_MAX_SERVERCMD_LEN 1024 + +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 + + // buffered server commands for current frame + char serverCmds[SVD_MAX_SERVERCMDS][SVD_MAX_SERVERCMD_LEN]; + int numServerCmds; + qboolean mapRestarted; // set by SVD_ResetDeltaState, written as frame flag + qboolean isKeyframe; // next frame is a keyframe (delta from baseline) + int keyframeInterval; // frames between keyframes (0 = disabled) + int framesSinceKeyframe; // counter for next keyframe + + // playback + fileHandle_t playFile; + qboolean playing; + int playMaxClients; // original maxclients from the recording + int playFps; // original sv_fps + char *savedConfigstrings[MAX_CONFIGSTRINGS]; // from demo header, re-applied after map load + char playMapName[SVDEMO_MAX_MAPNAME]; + qboolean endOfDemo; + qboolean needConfigstrings; // apply saved configstrings on first frame + qboolean starting; // SVD_Play_f is running devmap internally + qboolean paused; + qboolean seeked; // just seeked, next frame needs RESET + svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read + svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states + + // keyframe index (shared by recording and playback) + int numKeyframes; + int maxKeyframes; // allocated size + int *keyframeTimes; // serverTime of each keyframe + int *keyframeOffsets; // file offset of each keyframe +} svDemo_t; + +static svDemo_t demo; +// --------------------------------------------------------------- +// Recording helpers +// --------------------------------------------------------------- + +static void SVD_WriteInt( fileHandle_t f, int v ) { + FS_Write( &v, 4, f ); +} + +static void SVD_WriteShort( fileHandle_t f, short v ) { + FS_Write( &v, 2, f ); +} + +static int SVD_ReadInt( fileHandle_t f ) { + int v = 0; + FS_Read( &v, 4, f ); + return v; +} + +static short SVD_ReadShort( fileHandle_t f ) { + short v = 0; + FS_Read( &v, 2, f ); + return v; +} + +// --------------------------------------------------------------- +// Write header +// --------------------------------------------------------------- + +static void SVD_WriteHeader( fileHandle_t f ) { + int i; + char mapBuf[SVDEMO_MAX_MAPNAME]; + + SVD_WriteInt( f, SVDEMO_MAGIC ); + SVD_WriteInt( f, SVDEMO_VERSION ); + SVD_WriteInt( f, 0 ); // flags (reserved) + SVD_WriteInt( f, sv_maxclients->integer ); + SVD_WriteInt( f, sv_fps->integer ); + + // map name + memset( mapBuf, 0, sizeof(mapBuf) ); + Q_strncpyz( mapBuf, sv.configstrings[CS_SERVERINFO], sizeof(mapBuf) ); + // actually store the mapname from CS_SERVERINFO... or just the map name + { + const char *mapname = Cvar_VariableString("mapname"); + memset( mapBuf, 0, sizeof(mapBuf) ); + Q_strncpyz( mapBuf, mapname, sizeof(mapBuf) ); + } + FS_Write( mapBuf, SVDEMO_MAX_MAPNAME, f ); + + // configstrings + for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + if ( sv.configstrings[i] && sv.configstrings[i][0] ) { + int len = strlen( sv.configstrings[i] ) + 1; + SVD_WriteShort( f, (short)i ); + SVD_WriteShort( f, (short)len ); + FS_Write( sv.configstrings[i], len, f ); + + // store initial copy for delta detection + if ( demo.lastConfigstrings[i] ) { + Z_Free( demo.lastConfigstrings[i] ); + } + demo.lastConfigstrings[i] = CopyString( sv.configstrings[i] ); + } + } + // terminator + SVD_WriteShort( f, (short)0xFFFF ); +} + +// --------------------------------------------------------------- +// Write one frame +// --------------------------------------------------------------- + +static void SVD_WriteFrame( fileHandle_t f ) { + int i; + sharedEntity_t *ent; + short numChanges; + msg_t msg; + static byte msgBuf[MAX_GENTITIES * 300]; // worst case: all entities full write from baseline + + SVD_WriteInt( f, svs.time ); + SVD_WriteShort( f, (short)sv.num_entities ); + + // frame flags: bit 0 = map restarted, bit 1 = keyframe + { + byte frameFlags = 0; + if ( demo.mapRestarted ) { + frameFlags |= 1; + demo.mapRestarted = qfalse; + } + if ( demo.isKeyframe ) { + frameFlags |= 2; + demo.isKeyframe = qfalse; + } + FS_Write( &frameFlags, 1, f ); + } + + // delta-compress all entities into a message buffer + MSG_Init( &msg, msgBuf, sizeof(msgBuf) ); + + for ( i = 0; i < sv.num_entities; i++ ) { + qboolean active; + ent = SV_GentityNum( i ); + active = ( ent->r.linked || ent->s.eType != 0 ); + + if ( active ) { + entityState_t baseline; + entityState_t *from; + if ( demo.prevEntities[i].active ) { + from = &demo.prevEntities[i].es; + } else { + Com_Memset( &baseline, 0, sizeof(baseline) ); + baseline.number = i; + from = &baseline; + } + MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue ); + + // write svFlags only if changed (rarely changes) + if ( ent->r.svFlags != demo.prevEntities[i].svFlags ) { + MSG_WriteBits( &msg, 1, 1 ); + MSG_WriteLong( &msg, ent->r.svFlags ); + } else { + MSG_WriteBits( &msg, 0, 1 ); + } + + // update prev state + demo.prevEntities[i].es = ent->s; + demo.prevEntities[i].svFlags = ent->r.svFlags; + demo.prevEntities[i].active = qtrue; + } else if ( demo.prevEntities[i].active ) { + // entity was removed -- write a remove marker + MSG_WriteDeltaEntity( &msg, &demo.prevEntities[i].es, NULL, qtrue ); + demo.prevEntities[i].active = qfalse; + } + // else: entity was inactive and still inactive -- write nothing + } + + // end of entities marker + MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS ); + + // write entity message to file + SVD_WriteInt( f, msg.cursize ); + FS_Write( msg.data, msg.cursize, f ); + + // write player states (delta compressed) + { + msg_t psmsg; + static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline + 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 ); + qboolean isSpectator; + playerState_t specPs; + + // detect spectators: free cam or follow mode + isSpectator = active && ( ps->pm_type == PM_SPECTATOR || (ps->pm_flags & PMF_FOLLOW) ); + + // for spectators, record a sanitized ps so they appear on + // the scoreboard as spectators (follow mode corrupts their ps + // with the followed player's data) + if ( isSpectator ) { + Com_Memset( &specPs, 0, sizeof(specPs) ); + specPs.commandTime = ps->commandTime; + specPs.pm_type = PM_SPECTATOR; + specPs.persistant[PERS_TEAM] = TEAM_SPECTATOR; + specPs.clientNum = i; + ps = &specPs; + } + + 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 ); + + SVD_WriteInt( f, psmsg.cursize ); + FS_Write( psmsg.data, psmsg.cursize, f ); + } + + // configstring changes + numChanges = 0; + for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + const char *cur = sv.configstrings[i] ? sv.configstrings[i] : ""; + const char *old = demo.lastConfigstrings[i] ? demo.lastConfigstrings[i] : ""; + if ( strcmp( cur, old ) != 0 ) { + numChanges++; + } + } + SVD_WriteShort( f, numChanges ); + + if ( numChanges > 0 ) { + for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + const char *cur = sv.configstrings[i] ? sv.configstrings[i] : ""; + const char *old = demo.lastConfigstrings[i] ? demo.lastConfigstrings[i] : ""; + if ( strcmp( cur, old ) != 0 ) { + int len = strlen( cur ) + 1; + SVD_WriteShort( f, (short)i ); + SVD_WriteShort( f, (short)len ); + FS_Write( cur, len, f ); + + if ( demo.lastConfigstrings[i] ) { + Z_Free( demo.lastConfigstrings[i] ); + } + demo.lastConfigstrings[i] = CopyString( cur ); + } + } + } + + // write buffered server commands (chat, prints, etc.) + SVD_WriteShort( f, (short)demo.numServerCmds ); + for ( i = 0; i < demo.numServerCmds; i++ ) { + short len = (short)( strlen( demo.serverCmds[i] ) + 1 ); + SVD_WriteShort( f, len ); + FS_Write( demo.serverCmds[i], len, f ); + } + demo.numServerCmds = 0; +} + +// --------------------------------------------------------------- +// Recording commands +// --------------------------------------------------------------- + +/* +Start recording a demo with the given name. +Returns qtrue on success. +*/ +static qboolean SVD_StartRecording( const char *demoname ) { + char path[MAX_OSPATH]; + + if ( demo.recording ) { + Com_Printf( "Already recording a server demo.\n" ); + return qfalse; + } + + if ( sv.state != SS_GAME ) { + Com_Printf( "Not running a server.\n" ); + return qfalse; + } + + Com_sprintf( path, sizeof(path), "svdemos/%s.svdm", demoname ); + + demo.recordFile = FS_FOpenFileWrite( path ); + if ( !demo.recordFile ) { + Com_Printf( "ERROR: couldn't open %s for writing.\n", path ); + return qfalse; + } + + Com_Printf( "Recording server demo to %s\n", path ); + demo.recording = qtrue; + + // clear delta state for fresh recording + Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); + Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); + + // keyframe interval from cvar (seconds to frames at sv_fps) + { + int secs = Cvar_VariableIntegerValue( "svdemo_keyframeInterval" ); + if ( secs > 0 ) { + demo.keyframeInterval = secs * sv_fps->integer; + } else { + demo.keyframeInterval = 0; + } + // first frame is always a keyframe (makes beginning seekable) + demo.framesSinceKeyframe = demo.keyframeInterval; + demo.numKeyframes = 0; + } + + SVD_WriteHeader( demo.recordFile ); + return qtrue; +} + +void SVD_Record_f( void ) { + char *s; + + s = Cmd_Argv(1); + if ( !s[0] ) { + Com_Printf( "Usage: svdemo_record \n" ); + return; + } + + SVD_StartRecording( s ); +} + +/* +Auto-record: called from SV_SpawnServer after the map is fully loaded. +Generates a name from map name + timestamp. +*/ +void SVD_AutoRecord( void ) { + char demoname[MAX_OSPATH]; + const char *mapname; + qtime_t now; + + if ( demo.recording || demo.playing ) { + return; + } + + if ( !Cvar_VariableIntegerValue( "svdemo_autorecord" ) ) { + return; + } + + if ( sv.state != SS_GAME ) { + return; + } + + mapname = Cvar_VariableString( "mapname" ); + Com_RealTime( &now ); + Com_sprintf( demoname, sizeof(demoname), "%s_%04d%02d%02d_%02d%02d%02d", + mapname, + 1900 + now.tm_year, 1 + now.tm_mon, now.tm_mday, + now.tm_hour, now.tm_min, now.tm_sec ); + + SVD_StartRecording( demoname ); +} + +void SVD_StopRecord_f( void ) { + int i; + + if ( !demo.recording ) { + Com_Printf( "Not recording a server demo.\n" ); + return; + } + + // write end marker + SVD_WriteInt( demo.recordFile, -1 ); + + // write keyframe index after the end marker. + // layout: [numKf][time0 off0 time1 off1 ...][numKf_copy] + // numKf_copy at the very end lets playback find the table + // by seeking to fileLen - 4. + { + int kf; + SVD_WriteInt( demo.recordFile, demo.numKeyframes ); + for ( kf = 0; kf < demo.numKeyframes; kf++ ) { + SVD_WriteInt( demo.recordFile, demo.keyframeTimes[kf] ); + SVD_WriteInt( demo.recordFile, demo.keyframeOffsets[kf] ); + } + SVD_WriteInt( demo.recordFile, demo.numKeyframes ); // copy at end + Com_Printf( "Wrote %d keyframes.\n", demo.numKeyframes ); + } + + FS_FCloseFile( demo.recordFile ); + demo.recordFile = 0; + demo.recording = qfalse; + + // free configstring tracking + for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + if ( demo.lastConfigstrings[i] ) { + Z_Free( demo.lastConfigstrings[i] ); + demo.lastConfigstrings[i] = NULL; + } + } + + // free keyframe index + if ( demo.keyframeTimes ) { Z_Free( demo.keyframeTimes ); demo.keyframeTimes = NULL; } + if ( demo.keyframeOffsets ) { Z_Free( demo.keyframeOffsets ); demo.keyframeOffsets = NULL; } + demo.numKeyframes = demo.maxKeyframes = 0; + + Com_Printf( "Server demo recording stopped.\n" ); +} + +/* +Called from SV_Frame() after the game has run its frame. +*/ +/* +Reset delta compression state. Call on map_restart so the next +recorded frame writes full entity/player states from baseline. +*/ +/* +Capture a broadcast server command for the current frame. +Called from SV_SendServerCommand when cl == NULL (broadcast). +*/ +void SVD_CaptureServerCommand( const char *cmd ) { + int i; + + if ( !demo.recording ) { + return; + } + if ( demo.numServerCmds >= SVD_MAX_SERVERCMDS ) { + return; // overflow, drop command + } + + // deduplicate: per-client chat is sent N times (once per client), + // only store the first occurrence + for ( i = 0; i < demo.numServerCmds; i++ ) { + if ( !strcmp( demo.serverCmds[i], cmd ) ) { + return; + } + } + + Q_strncpyz( demo.serverCmds[demo.numServerCmds], cmd, SVD_MAX_SERVERCMD_LEN ); + demo.numServerCmds++; +} + +void SVD_ResetDeltaState( void ) { + if ( !demo.recording ) { + return; + } + Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); + Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); + demo.mapRestarted = qtrue; // signal next frame to write restart marker +} + +void SVD_RecordFrame( void ) { + if ( !demo.recording ) { + return; + } + + // periodic keyframe: reset delta state so this frame is decodable + // from baseline. record file offset for the keyframe index. + if ( demo.keyframeInterval > 0 ) { + demo.framesSinceKeyframe++; + if ( demo.framesSinceKeyframe >= demo.keyframeInterval ) { + demo.framesSinceKeyframe = 0; + demo.isKeyframe = qtrue; + Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); + Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) ); + // store keyframe: file offset before writing, serverTime + if ( demo.numKeyframes >= demo.maxKeyframes ) { + int newMax = demo.maxKeyframes ? demo.maxKeyframes * 2 : 256; + int *newTimes = Z_Malloc( newMax * sizeof(int) ); + int *newOffsets = Z_Malloc( newMax * sizeof(int) ); + if ( demo.keyframeTimes ) { + Com_Memcpy( newTimes, demo.keyframeTimes, demo.numKeyframes * sizeof(int) ); + Com_Memcpy( newOffsets, demo.keyframeOffsets, demo.numKeyframes * sizeof(int) ); + Z_Free( demo.keyframeTimes ); + Z_Free( demo.keyframeOffsets ); + } + demo.keyframeTimes = newTimes; + demo.keyframeOffsets = newOffsets; + demo.maxKeyframes = newMax; + } + demo.keyframeTimes[demo.numKeyframes] = svs.time; + demo.keyframeOffsets[demo.numKeyframes] = FS_FTell( demo.recordFile ); + demo.numKeyframes++; + } + } + + SVD_WriteFrame( demo.recordFile ); +} + +// --------------------------------------------------------------- +// Playback: read header +// --------------------------------------------------------------- + +static qboolean SVD_ReadHeader( fileHandle_t f ) { + int magic, version; + + magic = SVD_ReadInt( f ); + if ( magic != SVDEMO_MAGIC ) { + Com_Printf( "Not a valid server demo file.\n" ); + return qfalse; + } + + version = SVD_ReadInt( f ); + if ( version != SVDEMO_VERSION ) { + Com_Printf( "Unsupported server demo version %d.\n", version ); + return qfalse; + } + + SVD_ReadInt( f ); // flags (reserved) + + demo.playMaxClients = SVD_ReadInt( f ); + demo.playFps = SVD_ReadInt( f ); + FS_Read( demo.playMapName, SVDEMO_MAX_MAPNAME, f ); + demo.playMapName[SVDEMO_MAX_MAPNAME - 1] = '\0'; + + // read configstrings -- store for re-application after map load + { + short idx; + while (1) { + idx = SVD_ReadShort( f ); + if ( idx == (short)0xFFFF ) { + break; + } + { + short len = SVD_ReadShort( f ); + char buf[BIG_INFO_STRING]; + if ( len > 0 && len < (short)sizeof(buf) ) { + FS_Read( buf, len, f ); + buf[len - 1] = '\0'; + if ( demo.savedConfigstrings[idx] ) { + Z_Free( demo.savedConfigstrings[idx] ); + } + demo.savedConfigstrings[idx] = CopyString( buf ); + } + } + } + } + + return qtrue; +} + +/* +Re-apply recorded configstrings after the map has loaded. +Called after devmap finishes in SVD_Play_f. +*/ +static void SVD_ApplyConfigstrings( void ) { + int i; + for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + // skip CS_SERVERINFO and CS_SYSTEMINFO -- they contain latched cvars + // (sv_maxclients, sv_pure, etc.) that would trigger a map restart + if ( i == CS_SERVERINFO || i == CS_SYSTEMINFO ) { + continue; + } + if ( demo.savedConfigstrings[i] && demo.savedConfigstrings[i][0] ) { + SV_SetConfigstring( i, demo.savedConfigstrings[i] ); + } + } +} + +// --------------------------------------------------------------- +// Playback: read one frame, populate sv.gentities +// --------------------------------------------------------------- + +static qboolean SVD_ReadFrame( fileHandle_t f ) { + int serverTime; + short numEnts, numChanges; + int i, entNum, blockLen; + sharedEntity_t *ent; + msg_t msg; + static byte msgBuf[MAX_GENTITIES * 300]; + entityState_t newEs; + + serverTime = SVD_ReadInt( f ); + if ( serverTime == -1 ) { + return qfalse; // end of demo + } + + // set svs.time to recorded time so entity trajectory interpolation + // works correctly (rockets, grenades, etc. use pos.trTime relative + // to server time). zombie timeout is already skipped during playback. + svs.time = serverTime; + numEnts = SVD_ReadShort( f ); + + // read frame flags + { + byte frameFlags; + FS_Read( &frameFlags, 1, f ); + if ( frameFlags & 3 ) { + // bit 0 = map restart, bit 1 = keyframe. + // both mean: delta state was reset during recording, + // so reset playback delta state to decode from baseline. + Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); + Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); + } + if ( ( frameFlags & 1 ) || demo.seeked ) { + // map restart or seek: reset entity interpolation in cgame + svs.snapFlagServerBit |= SNAPFLAG_RESET_ENTITIES; + demo.seeked = qfalse; + } + } + + // read entity message + blockLen = SVD_ReadInt( f ); + if ( blockLen <= 0 || blockLen > (int)sizeof(msgBuf) ) { + return qfalse; + } + FS_Read( msgBuf, blockLen, f ); + MSG_Init( &msg, msgBuf, sizeof(msgBuf) ); + msg.cursize = blockLen; + + // clear all entities (spectator's entity is recreated by ClientThink_real) + for ( i = 0; i < sv.num_entities; i++ ) { + ent = SV_GentityNum( i ); + if ( ent->r.linked ) { + SV_UnlinkEntity( ent ); + } + ent->s.eType = 0; + ent->s.number = i; + } + + // parse delta-compressed entities + while ( 1 ) { + entNum = MSG_ReadBits( &msg, GENTITYNUM_BITS ); + if ( entNum == MAX_GENTITIES - 1 ) { + break; // end of entities marker + } + if ( msg.readcount > msg.cursize ) { + break; + } + + { + entityState_t baseline; + entityState_t *from; + Com_Memset( &newEs, 0, sizeof(newEs) ); + if ( demo.playPrevEntities[entNum].active ) { + from = &demo.playPrevEntities[entNum].es; + } else { + Com_Memset( &baseline, 0, sizeof(baseline) ); + baseline.number = entNum; + from = &baseline; + } + MSG_ReadDeltaEntity( &msg, from, &newEs, entNum ); + } + + if ( newEs.number == MAX_GENTITIES - 1 ) { + // entity was removed + demo.playPrevEntities[entNum].active = qfalse; + continue; + } + + // read svFlags + if ( MSG_ReadBits( &msg, 1 ) ) { + demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg ); + } + + demo.playPrevEntities[entNum].es = newEs; + demo.playPrevEntities[entNum].active = qtrue; + + // apply to server entity and link for PVS. + // use trBase as initial origin -- G_RunFrame will refine with + // BG_EvaluateTrajectory for moving entities (rockets etc). + ent = SV_GentityNum( entNum ); + ent->s = newEs; + ent->s.number = entNum; + ent->r.svFlags = demo.playPrevEntities[entNum].svFlags; + VectorCopy( newEs.pos.trBase, ent->r.currentOrigin ); + ent->r.linked = qtrue; + SV_LinkEntity( ent ); + + if ( entNum + 1 > sv.num_entities ) { + sv.num_entities = entNum + 1; + } + } + + // read player states (delta compressed) + { + msg_t psmsg; + static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline + 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 + { + playerState_t *gamePs = SV_GameClientNum( clientNum ); + *gamePs = newPs; + } + } else { + demo.playPrevPlayers[clientNum].active = qfalse; + // clear game playerState so G_RunFrame sees commandTime=0 + { + playerState_t *gamePs = SV_GameClientNum( clientNum ); + Com_Memset( gamePs, 0, sizeof(*gamePs) ); + } + } + } + } + } + + // read configstring changes (skip SERVERINFO/SYSTEMINFO to avoid latch restarts) + numChanges = SVD_ReadShort( f ); + for ( i = 0; i < numChanges; i++ ) { + short idx = SVD_ReadShort( f ); + short len = SVD_ReadShort( f ); + char buf[BIG_INFO_STRING]; + if ( len > 0 && len < (short)sizeof(buf) ) { + FS_Read( buf, len, f ); + buf[len - 1] = '\0'; + if ( idx != CS_SERVERINFO && idx != CS_SYSTEMINFO ) { + SV_SetConfigstring( idx, buf ); + } + } + } + + // read server commands (chat, prints, etc.) and replay to spectator + { + short numCmds = SVD_ReadShort( f ); + for ( i = 0; i < numCmds; i++ ) { + short len = SVD_ReadShort( f ); + char buf[SVD_MAX_SERVERCMD_LEN]; + if ( len > 0 && len < (short)sizeof(buf) ) { + FS_Read( buf, len, f ); + buf[len - 1] = '\0'; + // broadcast -- only the spectator is CS_ACTIVE, zombies are skipped + SV_SendServerCommand( NULL, "%s", buf ); + } + } + } + + return qtrue; +} + +// --------------------------------------------------------------- +// Playback commands +// --------------------------------------------------------------- + +void SVD_Play_f( void ) { + char name[MAX_OSPATH]; + char *s; + int len; + + if ( demo.recording ) { + Com_Printf( "Stop recording first (svdemo_stop).\n" ); + return; + } + + s = Cmd_Argv(1); + if ( !s[0] ) { + Com_Printf( "Usage: svdemo_play \n" ); + return; + } + + // stop current playback if switching demos + if ( demo.playing ) { + SVD_CleanupPlayback(); + } + + Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s ); + + memset( &demo, 0, sizeof(demo) ); + + len = FS_FOpenFileRead( name, &demo.playFile, qtrue ); + if ( !demo.playFile || len <= 0 ) { + Com_Printf( "ERROR: couldn't open %s.\n", name ); + return; + } + + if ( !SVD_ReadHeader( demo.playFile ) ) { + FS_FCloseFile( demo.playFile ); + demo.playFile = 0; + return; + } + + // read keyframe index from the end of the file. + // layout: [frames][-1][numKf][time0 off0 ...][numKf_copy] + // last 4 bytes of file = numKf_copy. + { + int frameStart = FS_FTell( demo.playFile ); + int numKf, kf; + + FS_Seek( demo.playFile, len - 4, FS_SEEK_SET ); + numKf = SVD_ReadInt( demo.playFile ); + + if ( numKf > 0 && numKf < 1000000 ) { + // seek to start of keyframe table: end - 4 - numKf*8 - 4 + int tableStart = len - 4 - numKf * 8 - 4; + FS_Seek( demo.playFile, tableStart + 4, FS_SEEK_SET ); // skip numKf + + demo.numKeyframes = numKf; + demo.maxKeyframes = numKf; + demo.keyframeTimes = Z_Malloc( numKf * sizeof(int) ); + demo.keyframeOffsets = Z_Malloc( numKf * sizeof(int) ); + + for ( kf = 0; kf < numKf; kf++ ) { + demo.keyframeTimes[kf] = SVD_ReadInt( demo.playFile ); + demo.keyframeOffsets[kf] = SVD_ReadInt( demo.playFile ); + } + Com_Printf( "Loaded %d keyframes.\n", numKf ); + } + + // seek back to start of frame data + FS_Seek( demo.playFile, frameStart, FS_SEEK_SET ); + } + + demo.playing = qtrue; + demo.endOfDemo = qfalse; + demo.needConfigstrings = qtrue; + + Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n", + demo.playMapName, demo.playMaxClients, demo.playFps ); + + // Shut down current server first so no clients carry over to + // reserved slots. SV_Shutdown triggers our cleanup hook, but + // demo.starting prevents it from clearing our state. + demo.starting = qtrue; + if ( com_sv_running->integer ) { + SV_Shutdown( "Demo playback\n" ); + } + + // Set demo cvar BEFORE devmap so G_InitGame can read it. + // The previous server is gone so no old game module to conflict with. + Cvar_Set2( "sv_demoplaying", "1", qtrue ); + + Cbuf_ExecuteText( EXEC_NOW, va("set sv_maxclients %d\n", MAX_CLIENTS) ); + Cbuf_ExecuteText( EXEC_NOW, va("set sv_fps %d\n", demo.playFps) ); + Cbuf_ExecuteText( EXEC_NOW, va("devmap %s\n", demo.playMapName) ); + demo.starting = qfalse; + + // CS_SVDEMO configstring is set by G_InitGame from the cvar + + // Reserve recorded player slots. Server is fresh (SV_Shutdown cleared + // old clients), local client hasn't connected yet. + { + int i; + for ( i = 0; i < demo.playMaxClients; i++ ) { + if ( svs.clients[i].state == CS_FREE ) { + svs.clients[i].state = CS_ZOMBIE; + svs.clients[i].rate = 10000; + svs.clients[i].nextSnapshotTime = 0x7FFFFFFF; + svs.clients[i].lastPacketTime = svs.time; + } + } + } +} + +void SVD_CleanupPlayback( void ) { + int i; + + if ( !demo.playing ) { + return; + } + + FS_FCloseFile( demo.playFile ); + + // free saved configstrings + for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + if ( demo.savedConfigstrings[i] ) { + Z_Free( demo.savedConfigstrings[i] ); + } + } + + // free zombie client slots + for ( i = 0; i < demo.playMaxClients; i++ ) { + if ( svs.clients[i].state == CS_ZOMBIE ) { + svs.clients[i].state = CS_FREE; + } + } + + // free keyframe index + if ( demo.keyframeTimes ) { Z_Free( demo.keyframeTimes ); } + if ( demo.keyframeOffsets ) { Z_Free( demo.keyframeOffsets ); } + + memset( &demo, 0, sizeof(demo) ); + Cvar_Set2( "sv_demoplaying", "0", qtrue ); +} + +void SVD_StopPlay_f( void ) { + if ( !demo.playing ) { + Com_Printf( "Not playing a server demo.\n" ); + return; + } + + SVD_CleanupPlayback(); + Com_Printf( "Server demo playback stopped.\n" ); + + // disconnect to return to main menu + Cbuf_ExecuteText( EXEC_APPEND, "disconnect\n" ); +} + +/* +Unified stop command: stops recording or playback, whichever is active. +*/ +void SVD_Stop_f( void ) { + if ( demo.recording ) { + SVD_StopRecord_f(); + } else if ( demo.playing ) { + SVD_StopPlay_f(); + } else { + Com_Printf( "Not recording or playing a server demo.\n" ); + } +} + +void SVD_Pause_f( void ) { + if ( !demo.playing ) { + Com_Printf( "Not playing a server demo.\n" ); + return; + } + demo.paused = !demo.paused; + if ( !demo.paused ) { + // resuming -- toggle SERVERCOUNT to reset client snapshot timing + // (drifted during pause from identical serverTimes). + svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; + } + Com_Printf( "Demo playback %s.\n", demo.paused ? "paused" : "resumed" ); +} + +void SVD_Seek_f( void ) { + int targetTime, i, bestKf; + float seconds; + + if ( !demo.playing ) { + Com_Printf( "Not playing a server demo.\n" ); + return; + } + + if ( Cmd_Argc() < 2 ) { + Com_Printf( "Usage: svdemo_seek \n" ); + return; + } + + if ( demo.numKeyframes <= 0 ) { + Com_Printf( "No keyframes in this demo -- seeking not available.\n" ); + return; + } + + seconds = atof( Cmd_Argv(1) ); + targetTime = svs.time + (int)(seconds * 1000); + + // find nearest keyframe at or before target time + bestKf = -1; + for ( i = 0; i < demo.numKeyframes; i++ ) { + if ( demo.keyframeTimes[i] <= targetTime ) { + bestKf = i; + } else { + break; + } + } + + if ( bestKf < 0 ) { + // target is before the first keyframe -- seek to first + bestKf = 0; + targetTime = demo.keyframeTimes[0]; + } + + // seek to keyframe file position + FS_Seek( demo.playFile, demo.keyframeOffsets[bestKf], FS_SEEK_SET ); + + // reset delta state (keyframe is encoded from baseline) + Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); + Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); + + // set svs.time to the keyframe time so the SV_Frame loop + // doesn't advance from the old time before reading + svs.time = demo.keyframeTimes[bestKf]; + + // toggle SERVERCOUNT to reset client time delta + svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; + + demo.seeked = qtrue; + demo.endOfDemo = qfalse; + + // read the keyframe directly (works even when paused) + svs.snapFlagServerBit &= ~SNAPFLAG_RESET_ENTITIES; + if ( !SVD_ReadFrame( demo.playFile ) ) { + demo.endOfDemo = qtrue; + } + + // reset client snapshot timing + { + 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 (for G_RunFrame + snapshot) + sv.timeResidual = 1000 / sv_fps->integer; + + Com_Printf( "Seeked to time %d.\n", svs.time ); +} + +void SVD_SeekExact_f( void ) { + int targetTime, i, bestKf; + float seconds; + + if ( !demo.playing ) { + Com_Printf( "Not playing a server demo.\n" ); + return; + } + + if ( Cmd_Argc() < 2 ) { + Com_Printf( "Usage: svdemo_seekexact \n" ); + return; + } + + if ( demo.numKeyframes <= 0 ) { + Com_Printf( "No keyframes in this demo.\n" ); + return; + } + + seconds = atof( Cmd_Argv(1) ); + targetTime = svs.time + (int)(seconds * 1000); + + // find nearest keyframe at or before target time + bestKf = -1; + for ( i = 0; i < demo.numKeyframes; i++ ) { + if ( demo.keyframeTimes[i] <= targetTime ) { + bestKf = i; + } else { + break; + } + } + + if ( bestKf < 0 ) { + bestKf = 0; + targetTime = demo.keyframeTimes[0]; + } + + // seek to keyframe + FS_Seek( demo.playFile, demo.keyframeOffsets[bestKf], FS_SEEK_SET ); + Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) ); + Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) ); + svs.time = demo.keyframeTimes[bestKf]; + svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; + demo.seeked = qtrue; + demo.endOfDemo = qfalse; + + // read forward from keyframe to target time + while ( svs.time < targetTime ) { + if ( !SVD_ReadFrame( demo.playFile ) ) { + demo.endOfDemo = qtrue; + break; + } + } + + // reset client snapshot timing + { + int j; + for ( j = 0; j < sv_maxclients->integer; j++ ) { + if ( svs.clients[j].state >= CS_ACTIVE ) { + svs.clients[j].nextSnapshotTime = svs.time; + } + } + } + + sv.timeResidual = 1000 / sv_fps->integer; + + Com_Printf( "Seeked to time %d (read forward %d ms from keyframe).\n", + svs.time, svs.time - demo.keyframeTimes[bestKf] ); +} + +/* +Called from SV_Frame() to advance playback by one frame. +Returns qtrue if a frame was read, qfalse if demo ended. +*/ +qboolean SVD_PlaybackFrame( void ) { + if ( !demo.playing || demo.endOfDemo ) { + return qfalse; + } + + + // manual pause -- don't consume demo data + if ( demo.paused ) { + return qfalse; + } + + // wait for a spectator to be fully in-game before starting playback. + // the server keeps running frames (so the connection handshake completes) + // but no demo data is consumed until someone is CS_ACTIVE. + if ( SVD_ShouldPause() ) { + return qfalse; + } + + // apply recorded configstrings once after map load + if ( demo.needConfigstrings ) { + SVD_ApplyConfigstrings(); + 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 ) ) { + Com_Printf( "Server demo playback finished.\n" ); + SVD_CleanupPlayback(); + Cbuf_ExecuteText( EXEC_APPEND, "disconnect\n" ); + return qfalse; + } + + return qtrue; +} + +// --------------------------------------------------------------- +// Queries +// --------------------------------------------------------------- + +qboolean SVD_IsRecording( void ) { + return demo.recording; +} + +qboolean SVD_IsPlaying( void ) { + return demo.playing; +} + +qboolean SVD_IsPaused( void ) { + return demo.playing && demo.paused; +} + +qboolean SVD_IsStarting( void ) { + return demo.starting; +} + + +/* +Returns qtrue if demo playback should pause (no active spectators). +Controlled by svdemo_pauseEmpty cvar. +*/ +qboolean SVD_ShouldPause( void ) { + int i; + + if ( !Cvar_VariableIntegerValue( "svdemo_pauseEmpty" ) ) { + return qfalse; + } + + for ( i = 0; i < sv_maxclients->integer; i++ ) { + if ( svs.clients[i].state == CS_ACTIVE ) { + return qfalse; // someone is in-game and watching + } + } + + return qtrue; // nobody connected, pause +} +