diff --git a/code/cgame/cg_draw.c b/code/cgame/cg_draw.c index e29c10f..7d7126e 100644 --- a/code/cgame/cg_draw.c +++ b/code/cgame/cg_draw.c @@ -1650,6 +1650,16 @@ static void CG_DrawDisconnect( void ) { const char *s; int w; // bk010215 - FIXME char message[1024]; + // server demo playback: detect pause from frozen snapshot time + if ( cg.svDemoPlayback ) { + if ( cg.nextSnap && cg.nextSnap->serverTime == cg.snap->serverTime ) { + s = "Playback Paused"; + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + CG_DrawBigString( 320 - w/2, 100, s, 1.0F ); + } + return; + } + // draw the phone jack if we are completely past our buffers cmdNum = trap_GetCurrentCmdNumber() - CMD_BACKUP + 1; trap_GetUserCmd( cmdNum, &cmd ); diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 4dd5c9d..0c8e98a 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -457,6 +457,10 @@ typedef struct { int clientNum; qboolean demoPlayback; + qboolean svDemoPlayback; // server-side demo playback mode + qboolean svDemoFreeCamera; // free camera (client-owned movement) + playerState_t svDemoCameraPs; // local playerState for free camera + int svDemoCameraTime; // real time of last camera update qboolean levelShot; // taking a level menu screenshot int deferredPlayerLoading; qboolean loading; // don't defer players at initial startup @@ -1618,6 +1622,7 @@ qboolean trap_GetUserCmd( int cmdNumber, usercmd_t *ucmd ); // used for the weapon select and zoom void trap_SetUserCmdValue( int stateValue, float sensitivityScale ); +void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z ); // aids for VM testing void testPrintInt( char *string, int i ); diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index aa9ec27..97f65a2 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1896,6 +1896,13 @@ void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ) { // get the gamestate from the client system trap_GetGameState( &cgs.gameState ); + // detect server-side demo playback from configstring + cg.svDemoPlayback = ( atoi( CG_ConfigString( CS_SVDEMO ) ) != 0 ); + if ( cg.svDemoPlayback ) { + cg.svDemoFreeCamera = qtrue; + // camera ps will be initialized from first snapshot in CG_PredictPlayerState + } + // check version s = CG_ConfigString( CS_GAME_VERSION ); if ( strcmp( s, GAME_VERSION ) ) { diff --git a/code/cgame/cg_predict.c b/code/cgame/cg_predict.c index fec2cfb..ed0d72d 100644 --- a/code/cgame/cg_predict.c +++ b/code/cgame/cg_predict.c @@ -432,6 +432,59 @@ void CG_PredictPlayerState( void ) { // demo playback just copies the moves if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) ) { CG_InterpolatePlayerState( qfalse ); + // send followed player's origin for PVS during server demo follow mode + if ( cg.svDemoPlayback && (cg.snap->ps.pm_flags & PMF_FOLLOW) ) { + trap_SetClientOrigin( qtrue, + cg.snap->ps.origin[0], + cg.snap->ps.origin[1], + cg.snap->ps.origin[2] ); + cg.svDemoFreeCamera = qfalse; + } + return; + } + + // server-side demo free camera: run local PmoveSingle + if ( cg.svDemoPlayback && cg.svDemoFreeCamera ) { + int realTime = trap_Milliseconds(); + int dt = realTime - cg.svDemoCameraTime; + pmove_t pm; + + // lazy init: place camera at spectator spawn from first snapshot + if ( cg.svDemoCameraPs.speed == 0 ) { + cg.svDemoCameraPs = cg.snap->ps; + cg.svDemoCameraPs.pm_type = PM_SPECTATOR; + cg.svDemoCameraPs.speed = 480; + cg.svDemoCameraTime = realTime; + dt = 1; + } + + if ( dt < 1 ) dt = 1; + if ( dt > 200 ) dt = 200; + cg.svDemoCameraTime = realTime; + + // get latest usercmd for input + current = trap_GetCurrentCmdNumber(); + trap_GetUserCmd( current, &latestCmd ); + + // set up pmove + memset( &pm, 0, sizeof(pm) ); + pm.ps = &cg.svDemoCameraPs; + pm.cmd = latestCmd; + pm.cmd.serverTime = cg.svDemoCameraPs.commandTime + dt; + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; + pm.trace = CG_Trace; + pm.pointcontents = CG_PointContents; + + Pmove( &pm ); + + // use as the predicted state for rendering + cg.predictedPlayerState = cg.svDemoCameraPs; + + // send origin for PVS + trap_SetClientOrigin( qtrue, + cg.svDemoCameraPs.origin[0], + cg.svDemoCameraPs.origin[1], + cg.svDemoCameraPs.origin[2] ); return; } diff --git a/code/cgame/cg_public.h b/code/cgame/cg_public.h index 8514143..d667ce2 100644 --- a/code/cgame/cg_public.h +++ b/code/cgame/cg_public.h @@ -164,6 +164,7 @@ typedef enum { CG_R_INPVS, // 1.32 CG_FS_SEEK, + CG_SETCLIENTORIGIN, // set client-owned origin for demo spectator PVS /* CG_LOADCAMERA, diff --git a/code/cgame/cg_snapshot.c b/code/cgame/cg_snapshot.c index add49f0..f10c9ab 100644 --- a/code/cgame/cg_snapshot.c +++ b/code/cgame/cg_snapshot.c @@ -1,403 +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 ); - } - } - -} - - -/* -=================== -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; - - 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; - } - - // 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 - if ( cg.nextSnap->serverTime < cg.snap->serverTime ) { - 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/cgame/cg_syscalls.c b/code/cgame/cg_syscalls.c index a7bf659..2446b4b 100644 --- a/code/cgame/cg_syscalls.c +++ b/code/cgame/cg_syscalls.c @@ -333,6 +333,10 @@ void trap_SetUserCmdValue( int stateValue, float sensitivityScale ) { syscall( CG_SETUSERCMDVALUE, stateValue, PASSFLOAT(sensitivityScale) ); } +void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z ) { + syscall( CG_SETCLIENTORIGIN, hasOrigin, PASSFLOAT(x), PASSFLOAT(y), PASSFLOAT(z) ); +} + void testPrintInt( char *string, int i ) { syscall( CG_TESTPRINTINT, string, i ); } diff --git a/code/client/cl_cgame.c b/code/client/cl_cgame.c index 0b259d3..e399cef 100644 --- a/code/client/cl_cgame.c +++ b/code/client/cl_cgame.c @@ -457,6 +457,14 @@ int CL_CgameSystemCalls( int *args ) { return 0; case CG_FS_SEEK: return FS_Seek( args[1], args[2], args[3] ); + case CG_SETCLIENTORIGIN: + cl.cgameHasOrigin = args[1]; + if ( args[1] ) { + cl.cgameOrigin[0] = VMF(2); + cl.cgameOrigin[1] = VMF(3); + cl.cgameOrigin[2] = VMF(4); + } + return 0; case CG_SENDCONSOLECOMMAND: Cbuf_AddText( VMA(1) ); return 0; diff --git a/code/client/cl_input.c b/code/client/cl_input.c index f450ee7..c3b4778 100644 --- a/code/client/cl_input.c +++ b/code/client/cl_input.c @@ -516,6 +516,12 @@ void CL_FinishMove( usercmd_t *cmd ) { for (i=0 ; i<3 ; i++) { cmd->angles[i] = ANGLE2SHORT(cl.viewangles[i]); } + + // client-owned origin for demo spectator PVS + cmd->hasOrigin = cl.cgameHasOrigin; + if ( cl.cgameHasOrigin ) { + VectorCopy( cl.cgameOrigin, cmd->origin ); + } } diff --git a/code/client/cl_parse.c b/code/client/cl_parse.c index 9de3588..8626b3f 100644 --- a/code/client/cl_parse.c +++ b/code/client/cl_parse.c @@ -288,17 +288,30 @@ void CL_ParseSnapshot( msg_t *msg ) { cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse; } - // copy to the current good spot - cl.snap = newSnap; - cl.snap.ping = 999; - // calculate ping time - for ( i = 0 ; i < PACKET_BACKUP ; i++ ) { - packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK; - if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) { - cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime; - break; + { + int oldSnapFlags = cl.snap.snapFlags; + + // copy to the current good spot + cl.snap = newSnap; + cl.snap.ping = 999; + // calculate ping time + for ( i = 0 ; i < PACKET_BACKUP ; i++ ) { + packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK; + if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) { + cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime; + break; + } + } + // if server count changed (map_restart or demo unpause), force-reset + // the time delta so the client doesn't slowly drift back to sync + if ( ( oldSnapFlags ^ newSnap.snapFlags ) & SNAPFLAG_SERVERCOUNT ) { + cl.serverTimeDelta = cl.snap.serverTime - cls.realtime; + cl.oldServerTime = cl.snap.serverTime; + cl.serverTime = cl.snap.serverTime; + cl.oldFrameServerTime = cl.snap.serverTime; // prevent backwards time error on seek } } + // save the frame off in the backup array for later delta comparisons cl.snapshots[cl.snap.messageNum & PACKET_MASK] = cl.snap; diff --git a/code/client/client.h b/code/client/client.h index 09e02a8..fe36445 100644 --- a/code/client/client.h +++ b/code/client/client.h @@ -106,6 +106,8 @@ typedef struct { // cgame communicates a few values to the client system int cgameUserCmdValue; // current weapon to add to usercmd_t float cgameSensitivity; + qboolean cgameHasOrigin; // client-owned origin for demo spectator PVS + vec3_t cgameOrigin; // the origin to send // cmds[cmdNumber] is the predicted command, [cmdNumber-1] is the last // properly generated command diff --git a/code/game/bg_public.h b/code/game/bg_public.h index 0dd59db..5dc1cf2 100644 --- a/code/game/bg_public.h +++ b/code/game/bg_public.h @@ -79,6 +79,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #define CS_FLAGSTATUS 23 // string indicating flag status in CTF #define CS_SHADERSTATE 24 #define CS_BOTINFO 25 +#define CS_SVDEMO 26 // "1" during server-side demo playback #define CS_ITEMS 27 // string of 0's and 1's that tell which items are present diff --git a/code/game/g_active.c b/code/game/g_active.c index 853f5fb..f78dac8 100644 --- a/code/game/g_active.c +++ b/code/game/g_active.c @@ -1,1191 +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; - - 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_client.c b/code/game/g_client.c index 5a584d4..6d43d7c 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -1012,6 +1012,12 @@ void ClientBegin( int clientNum ) { client->pers.enterTime = level.time; client->pers.teamState.state = TEAM_BEGIN; + // demo playback: force all clients to spectator + if ( g_svDemoPlaying.integer ) { + client->sess.sessionTeam = TEAM_SPECTATOR; + ClientUserinfoChanged( ent->client - level.clients ); + } + // save eflags around this, because changing teams will // cause this to happen with a valid entity, and we // want to make sure the teleport bit is set right diff --git a/code/game/g_cmds.c b/code/game/g_cmds.c index e72c80e..dc87e58 100644 --- a/code/game/g_cmds.c +++ b/code/game/g_cmds.c @@ -661,6 +661,20 @@ void Cmd_Team_f( gentity_t *ent ) { int oldTeam; char s[MAX_TOKEN_CHARS]; + // demo playback: only allow "team spectator" (to exit follow mode) + if ( g_svDemoPlaying.integer ) { + char s2[MAX_TOKEN_CHARS]; + if ( trap_Argc() == 2 ) { + trap_Argv( 1, s2, sizeof(s2) ); + if ( !Q_stricmp( s2, "spectator" ) || !Q_stricmp( s2, "s" ) ) { + StopFollowing( ent ); + return; + } + } + trap_SendServerCommand( ent-g_entities, "print \"Only spectating allowed during demo playback.\n\"" ); + return; + } + if ( trap_Argc() != 2 ) { oldTeam = ent->client->sess.sessionTeam; switch ( oldTeam ) { diff --git a/code/game/g_local.h b/code/game/g_local.h index 1a55955..057722c 100644 --- a/code/game/g_local.h +++ b/code/game/g_local.h @@ -641,6 +641,7 @@ void ClientCommand( int clientNum ); // g_active.c // void ClientThink( int clientNum ); +void ClientThink_real( gentity_t *ent ); void ClientEndFrame( gentity_t *ent ); void G_RunClient( gentity_t *ent ); @@ -715,6 +716,7 @@ extern gentity_t g_entities[MAX_GENTITIES]; #define FOFS(x) ((int)&(((gentity_t *)0)->x)) extern vmCvar_t g_gametype; +extern vmCvar_t g_svDemoPlaying; extern vmCvar_t g_dedicated; extern vmCvar_t g_cheats; extern vmCvar_t g_maxclients; // allow this many total, including spectators diff --git a/code/game/g_main.c b/code/game/g_main.c index 9f60272..b6303f1 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -1,1832 +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_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_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(); - - 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; - 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(); - - // - // 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 e65c3e1..7de9374 100644 --- a/code/game/g_session.c +++ b/code/game/g_session.c @@ -1,193 +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; - - 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/game/q_shared.h b/code/game/q_shared.h index 99e5a67..32a772f 100644 --- a/code/game/q_shared.h +++ b/code/game/q_shared.h @@ -1085,6 +1085,7 @@ typedef enum { #define SNAPFLAG_RATE_DELAYED 1 #define SNAPFLAG_NOT_ACTIVE 2 // snapshot used during connection and for zombies #define SNAPFLAG_SERVERCOUNT 4 // toggled every map_restart so transitions can be detected +#define SNAPFLAG_RESET_ENTITIES 16 // snap all entities to current position, no interpolation // // per-level limits @@ -1252,8 +1253,11 @@ typedef struct usercmd_s { int serverTime; int angles[3]; int buttons; - byte weapon; // weapon + byte weapon; // weapon signed char forwardmove, rightmove, upmove; + // client-owned origin for demo spectator PVS (optional) + qboolean hasOrigin; + float origin[3]; } usercmd_t; //=================================================================== diff --git a/code/qcommon/msg.c b/code/qcommon/msg.c index 97110f8..e4f6696 100644 --- a/code/qcommon/msg.c +++ b/code/qcommon/msg.c @@ -702,18 +702,27 @@ void MSG_WriteDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t * from->weapon == to->weapon) { MSG_WriteBits( msg, 0, 1 ); // no change oldsize += 7; - return; + } else { + key ^= to->serverTime; + MSG_WriteBits( msg, 1, 1 ); + MSG_WriteDeltaKey( msg, key, from->angles[0], to->angles[0], 16 ); + MSG_WriteDeltaKey( msg, key, from->angles[1], to->angles[1], 16 ); + MSG_WriteDeltaKey( msg, key, from->angles[2], to->angles[2], 16 ); + MSG_WriteDeltaKey( msg, key, from->forwardmove, to->forwardmove, 8 ); + MSG_WriteDeltaKey( msg, key, from->rightmove, to->rightmove, 8 ); + MSG_WriteDeltaKey( msg, key, from->upmove, to->upmove, 8 ); + MSG_WriteDeltaKey( msg, key, from->buttons, to->buttons, 16 ); + MSG_WriteDeltaKey( msg, key, from->weapon, to->weapon, 8 ); + } + // optional client-owned origin (always written, independent of field changes) + if ( to->hasOrigin ) { + MSG_WriteBits( msg, 1, 1 ); + MSG_WriteFloat( msg, to->origin[0] ); + MSG_WriteFloat( msg, to->origin[1] ); + MSG_WriteFloat( msg, to->origin[2] ); + } else { + MSG_WriteBits( msg, 0, 1 ); } - key ^= to->serverTime; - MSG_WriteBits( msg, 1, 1 ); - MSG_WriteDeltaKey( msg, key, from->angles[0], to->angles[0], 16 ); - MSG_WriteDeltaKey( msg, key, from->angles[1], to->angles[1], 16 ); - MSG_WriteDeltaKey( msg, key, from->angles[2], to->angles[2], 16 ); - MSG_WriteDeltaKey( msg, key, from->forwardmove, to->forwardmove, 8 ); - MSG_WriteDeltaKey( msg, key, from->rightmove, to->rightmove, 8 ); - MSG_WriteDeltaKey( msg, key, from->upmove, to->upmove, 8 ); - MSG_WriteDeltaKey( msg, key, from->buttons, to->buttons, 16 ); - MSG_WriteDeltaKey( msg, key, from->weapon, to->weapon, 8 ); } @@ -748,6 +757,15 @@ void MSG_ReadDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t *t to->buttons = from->buttons; to->weapon = from->weapon; } + // optional client-owned origin + if ( MSG_ReadBits( msg, 1 ) ) { + to->hasOrigin = qtrue; + to->origin[0] = MSG_ReadFloat( msg ); + to->origin[1] = MSG_ReadFloat( msg ); + to->origin[2] = MSG_ReadFloat( msg ); + } else { + to->hasOrigin = qfalse; + } } /* diff --git a/code/quake3.vcxproj b/code/quake3.vcxproj index 340ba18..25d6f18 100644 --- a/code/quake3.vcxproj +++ b/code/quake3.vcxproj @@ -995,6 +995,8 @@ MaxSpeed MaxSpeed + + Disabled true diff --git a/code/server/lz4.c b/code/server/lz4.c new file mode 100644 index 0000000..1af8ea4 --- /dev/null +++ b/code/server/lz4.c @@ -0,0 +1,2825 @@ +/* + LZ4 - Fast LZ compression algorithm + Copyright (c) Yann Collet. All rights reserved. + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - LZ4 homepage : http://www.lz4.org + - LZ4 source repository : https://github.com/lz4/lz4 +*/ + +/*-************************************ +* Tuning parameters +**************************************/ +/* + * LZ4_HEAPMODE : + * Select how stateless compression functions like `LZ4_compress_default()` + * allocate memory for their hash table, + * in memory stack (0:default, fastest), or in memory heap (1:requires malloc()). + */ +#ifndef LZ4_HEAPMODE +# define LZ4_HEAPMODE 0 +#endif + +/* + * LZ4_ACCELERATION_DEFAULT : + * Select "acceleration" for LZ4_compress_fast() when parameter value <= 0 + */ +#define LZ4_ACCELERATION_DEFAULT 1 +/* + * LZ4_ACCELERATION_MAX : + * Any "acceleration" value higher than this threshold + * get treated as LZ4_ACCELERATION_MAX instead (fix #876) + */ +#define LZ4_ACCELERATION_MAX 65537 + + +/*-************************************ +* CPU Feature Detection +**************************************/ +/* LZ4_FORCE_MEMORY_ACCESS + * By default, access to unaligned memory is controlled by `memcpy()`, which is safe and portable. + * Unfortunately, on some target/compiler combinations, the generated assembly is sub-optimal. + * The below switch allow to select different access method for improved performance. + * Method 0 (default) : use `memcpy()`. Safe and portable. + * Method 1 : `__packed` statement. It depends on compiler extension (ie, not portable). + * This method is safe if your compiler supports it, and *generally* as fast or faster than `memcpy`. + * Method 2 : direct access. This method is portable but violate C standard. + * It can generate buggy code on targets which assembly generation depends on alignment. + * But in some circumstances, it's the only known way to get the most performance (ie GCC + ARMv6) + * See https://fastcompression.blogspot.fr/2015/08/accessing-unaligned-memory.html for details. + * Prefer these methods in priority order (0 > 1 > 2) + */ +#ifndef LZ4_FORCE_MEMORY_ACCESS /* can be defined externally */ +# if defined(__GNUC__) && \ + ( defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_6J__) || defined(__ARM_ARCH_6K__) \ + || defined(__ARM_ARCH_6Z__) || defined(__ARM_ARCH_6ZK__) || defined(__ARM_ARCH_6T2__) \ + || (defined(__riscv) && defined(__riscv_zicclsm)) ) +# define LZ4_FORCE_MEMORY_ACCESS 2 +# elif (defined(__INTEL_COMPILER) && !defined(_WIN32)) || defined(__GNUC__) || defined(_MSC_VER) +# define LZ4_FORCE_MEMORY_ACCESS 1 +# endif +#endif + +/* + * LZ4_FORCE_SW_BITCOUNT + * Define this parameter if your target system or compiler does not support hardware bit count + */ +#if defined(_MSC_VER) && defined(_WIN32_WCE) /* Visual Studio for WinCE doesn't support Hardware bit count */ +# undef LZ4_FORCE_SW_BITCOUNT /* avoid double def */ +# define LZ4_FORCE_SW_BITCOUNT +#endif + + + +/*-************************************ +* Dependency +**************************************/ +/* + * LZ4_SRC_INCLUDED: + * Amalgamation flag, whether lz4.c is included + */ +#ifndef LZ4_SRC_INCLUDED +# define LZ4_SRC_INCLUDED 1 +#endif + +#ifndef LZ4_DISABLE_DEPRECATE_WARNINGS +# define LZ4_DISABLE_DEPRECATE_WARNINGS /* due to LZ4_decompress_safe_withPrefix64k */ +#endif + +#ifndef LZ4_STATIC_LINKING_ONLY +# define LZ4_STATIC_LINKING_ONLY +#endif +#include "lz4.h" +/* see also "memory routines" below */ + + +/*-************************************ +* Compiler Options +**************************************/ +#if defined(_MSC_VER) && (_MSC_VER >= 1400) /* Visual Studio 2005+ */ +# include /* only present in VS2005+ */ +# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ +# pragma warning(disable : 6237) /* disable: C6237: conditional expression is always 0 */ +# pragma warning(disable : 6239) /* disable: C6239: ( && ) always evaluates to the result of */ +# pragma warning(disable : 6240) /* disable: C6240: ( && ) always evaluates to the result of */ +# pragma warning(disable : 6326) /* disable: C6326: Potential comparison of a constant with another constant */ +#endif /* _MSC_VER */ + +#ifndef LZ4_FORCE_INLINE +# if defined (_MSC_VER) && !defined (__clang__) /* MSVC */ +# define LZ4_FORCE_INLINE static __forceinline +# else +# if defined (__cplusplus) || defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 */ +# if defined (__GNUC__) || defined (__clang__) +# define LZ4_FORCE_INLINE static inline __attribute__((always_inline)) +# else +# define LZ4_FORCE_INLINE static inline +# endif +# else +# define LZ4_FORCE_INLINE static +# endif /* __STDC_VERSION__ */ +# endif /* _MSC_VER */ +#endif /* LZ4_FORCE_INLINE */ + +/* LZ4_FORCE_O2 and LZ4_FORCE_INLINE + * gcc on ppc64le generates an unrolled SIMDized loop for LZ4_wildCopy8, + * together with a simple 8-byte copy loop as a fall-back path. + * However, this optimization hurts the decompression speed by >30%, + * because the execution does not go to the optimized loop + * for typical compressible data, and all of the preamble checks + * before going to the fall-back path become useless overhead. + * This optimization happens only with the -O3 flag, and -O2 generates + * a simple 8-byte copy loop. + * With gcc on ppc64le, all of the LZ4_decompress_* and LZ4_wildCopy8 + * functions are annotated with __attribute__((optimize("O2"))), + * and also LZ4_wildCopy8 is forcibly inlined, so that the O2 attribute + * of LZ4_wildCopy8 does not affect the compression speed. + */ +#if defined(__PPC64__) && defined(__LITTLE_ENDIAN__) && defined(__GNUC__) && !defined(__clang__) +# define LZ4_FORCE_O2 __attribute__((optimize("O2"))) +# undef LZ4_FORCE_INLINE +# define LZ4_FORCE_INLINE static __inline __attribute__((optimize("O2"),always_inline)) +#else +# define LZ4_FORCE_O2 +#endif + +#if (defined(__GNUC__) && (__GNUC__ >= 3)) || (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 800)) || defined(__clang__) +# define expect(expr,value) (__builtin_expect ((expr),(value)) ) +#else +# define expect(expr,value) (expr) +#endif + +#ifndef likely +#define likely(expr) expect((expr) != 0, 1) +#endif +#ifndef unlikely +#define unlikely(expr) expect((expr) != 0, 0) +#endif + +/* Should the alignment test prove unreliable, for some reason, + * it can be disabled by setting LZ4_ALIGN_TEST to 0 */ +#ifndef LZ4_ALIGN_TEST /* can be externally provided */ +# define LZ4_ALIGN_TEST 1 +#endif + + +/*-************************************ +* Memory routines +**************************************/ + +/*! LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION : + * Disable relatively high-level LZ4/HC functions that use dynamic memory + * allocation functions (malloc(), calloc(), free()). + * + * Note that this is a compile-time switch. And since it disables + * public/stable LZ4 v1 API functions, we don't recommend using this + * symbol to generate a library for distribution. + * + * The following public functions are removed when this symbol is defined. + * - lz4 : LZ4_createStream, LZ4_freeStream, + * LZ4_createStreamDecode, LZ4_freeStreamDecode, LZ4_create (deprecated) + * - lz4hc : LZ4_createStreamHC, LZ4_freeStreamHC, + * LZ4_createHC (deprecated), LZ4_freeHC (deprecated) + * - lz4frame, lz4file : All LZ4F_* functions + */ +#if defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +# define ALLOC(s) lz4_error_memory_allocation_is_disabled +# define ALLOC_AND_ZERO(s) lz4_error_memory_allocation_is_disabled +# define FREEMEM(p) lz4_error_memory_allocation_is_disabled +#elif defined(LZ4_USER_MEMORY_FUNCTIONS) +/* memory management functions can be customized by user project. + * Below functions must exist somewhere in the Project + * and be available at link time */ +void* LZ4_malloc(size_t s); +void* LZ4_calloc(size_t n, size_t s); +void LZ4_free(void* p); +# define ALLOC(s) LZ4_malloc(s) +# define ALLOC_AND_ZERO(s) LZ4_calloc(1,s) +# define FREEMEM(p) LZ4_free(p) +#else +# include /* malloc, calloc, free */ +# define ALLOC(s) malloc(s) +# define ALLOC_AND_ZERO(s) calloc(1,s) +# define FREEMEM(p) free(p) +#endif + +#if ! LZ4_FREESTANDING +# include /* memset, memcpy */ +#endif +#if !defined(LZ4_memset) +# define LZ4_memset(p,v,s) memset((p),(v),(s)) +#endif +#define MEM_INIT(p,v,s) LZ4_memset((p),(v),(s)) + + +/*-************************************ +* Common Constants +**************************************/ +#define MINMATCH 4 + +#define WILDCOPYLENGTH 8 +#define LASTLITERALS 5 /* see ../doc/lz4_Block_format.md#parsing-restrictions */ +#define MFLIMIT 12 /* see ../doc/lz4_Block_format.md#parsing-restrictions */ +#define MATCH_SAFEGUARD_DISTANCE ((2*WILDCOPYLENGTH) - MINMATCH) /* ensure it's possible to write 2 x wildcopyLength without overflowing output buffer */ +#define FASTLOOP_SAFE_DISTANCE 64 +static const int LZ4_minLength = (MFLIMIT+1); + +#define KB *(1 <<10) +#define MB *(1 <<20) +#define GB *(1U<<30) + +#define LZ4_DISTANCE_ABSOLUTE_MAX 65535 +#if (LZ4_DISTANCE_MAX > LZ4_DISTANCE_ABSOLUTE_MAX) /* max supported by LZ4 format */ +# error "LZ4_DISTANCE_MAX is too big : must be <= 65535" +#endif + +#define ML_BITS 4 +#define ML_MASK ((1U<=1) +# include +#else +# ifndef assert +# define assert(condition) ((void)0) +# endif +#endif + +#define LZ4_STATIC_ASSERT(c) { enum { LZ4_static_assert = 1/(int)(!!(c)) }; } /* use after variable declarations */ + +#if defined(LZ4_DEBUG) && (LZ4_DEBUG>=2) +# include + static int g_debuglog_enable = 1; +# define DEBUGLOG(l, ...) { \ + if ((g_debuglog_enable) && (l<=LZ4_DEBUG)) { \ + fprintf(stderr, __FILE__ " %i: ", __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, " \n"); \ + } } +#else +# define DEBUGLOG(l, ...) {} /* disabled */ +#endif + +static int LZ4_isAligned(const void* ptr, size_t alignment) +{ + return ((size_t)ptr & (alignment -1)) == 0; +} + + +/*-************************************ +* Types +**************************************/ +#include +#if defined(__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# include + typedef unsigned char BYTE; /*uint8_t not necessarily blessed to alias arbitrary type*/ + typedef uint16_t U16; + typedef uint32_t U32; + typedef int32_t S32; + typedef uint64_t U64; + typedef uintptr_t uptrval; +#else +# if UINT_MAX != 4294967295UL +# error "LZ4 code (when not C++ or C99) assumes that sizeof(int) == 4" +# endif + typedef unsigned char BYTE; + typedef unsigned short U16; + typedef unsigned int U32; + typedef signed int S32; + typedef unsigned long long U64; + typedef size_t uptrval; /* generally true, except OpenVMS-64 */ +#endif + +#if defined(__x86_64__) + typedef U64 reg_t; /* 64-bits in x32 mode */ +#else + typedef size_t reg_t; /* 32-bits in x32 mode */ +#endif + +typedef enum { + notLimited = 0, + limitedOutput = 1, + fillOutput = 2 +} limitedOutput_directive; + + +/*-************************************ +* Reading and writing into memory +**************************************/ + +/** + * LZ4 relies on memcpy with a constant size being inlined. In freestanding + * environments, the compiler can't assume the implementation of memcpy() is + * standard compliant, so it can't apply its specialized memcpy() inlining + * logic. When possible, use __builtin_memcpy() to tell the compiler to analyze + * memcpy() as if it were standard compliant, so it can inline it in freestanding + * environments. This is needed when decompressing the Linux Kernel, for example. + */ +#if !defined(LZ4_memcpy) +# if defined(__GNUC__) && (__GNUC__ >= 4) +# define LZ4_memcpy(dst, src, size) __builtin_memcpy(dst, src, size) +# else +# define LZ4_memcpy(dst, src, size) memcpy(dst, src, size) +# endif +#endif + +#if !defined(LZ4_memmove) +# if defined(__GNUC__) && (__GNUC__ >= 4) +# define LZ4_memmove __builtin_memmove +# else +# define LZ4_memmove memmove +# endif +#endif + +static unsigned LZ4_isLittleEndian(void) +{ + const union { U32 u; BYTE c[4]; } one = { 1 }; /* don't use static : performance detrimental */ + return one.c[0]; +} + +#if defined(__GNUC__) || defined(__INTEL_COMPILER) +#define LZ4_PACK( __Declaration__ ) __Declaration__ __attribute__((__packed__)) +#elif defined(_MSC_VER) +#define LZ4_PACK( __Declaration__ ) __pragma( pack(push, 1) ) __Declaration__ __pragma( pack(pop)) +#endif + +#if defined(LZ4_FORCE_MEMORY_ACCESS) && (LZ4_FORCE_MEMORY_ACCESS==2) +/* lie to the compiler about data alignment; use with caution */ + +static U16 LZ4_read16(const void* memPtr) { return *(const U16*) memPtr; } +static U32 LZ4_read32(const void* memPtr) { return *(const U32*) memPtr; } +static reg_t LZ4_read_ARCH(const void* memPtr) { return *(const reg_t*) memPtr; } + +static void LZ4_write16(void* memPtr, U16 value) { *(U16*)memPtr = value; } +static void LZ4_write32(void* memPtr, U32 value) { *(U32*)memPtr = value; } + +#elif defined(LZ4_FORCE_MEMORY_ACCESS) && (LZ4_FORCE_MEMORY_ACCESS==1) + +/* __pack instructions are safer, but compiler specific, hence potentially problematic for some compilers */ +/* currently only defined for gcc and icc */ +LZ4_PACK(typedef struct { U16 u16; }) LZ4_unalign16; +LZ4_PACK(typedef struct { U32 u32; }) LZ4_unalign32; +LZ4_PACK(typedef struct { reg_t uArch; }) LZ4_unalignST; + +static U16 LZ4_read16(const void* ptr) { return ((const LZ4_unalign16*)ptr)->u16; } +static U32 LZ4_read32(const void* ptr) { return ((const LZ4_unalign32*)ptr)->u32; } +static reg_t LZ4_read_ARCH(const void* ptr) { return ((const LZ4_unalignST*)ptr)->uArch; } + +static void LZ4_write16(void* memPtr, U16 value) { ((LZ4_unalign16*)memPtr)->u16 = value; } +static void LZ4_write32(void* memPtr, U32 value) { ((LZ4_unalign32*)memPtr)->u32 = value; } + +#else /* safe and portable access using memcpy() */ + +static U16 LZ4_read16(const void* memPtr) +{ + U16 val; LZ4_memcpy(&val, memPtr, sizeof(val)); return val; +} + +static U32 LZ4_read32(const void* memPtr) +{ + U32 val; LZ4_memcpy(&val, memPtr, sizeof(val)); return val; +} + +static reg_t LZ4_read_ARCH(const void* memPtr) +{ + reg_t val; LZ4_memcpy(&val, memPtr, sizeof(val)); return val; +} + +static void LZ4_write16(void* memPtr, U16 value) +{ + LZ4_memcpy(memPtr, &value, sizeof(value)); +} + +static void LZ4_write32(void* memPtr, U32 value) +{ + LZ4_memcpy(memPtr, &value, sizeof(value)); +} + +#endif /* LZ4_FORCE_MEMORY_ACCESS */ + + +static U16 LZ4_readLE16(const void* memPtr) +{ + if (LZ4_isLittleEndian()) { + return LZ4_read16(memPtr); + } else { + const BYTE* p = (const BYTE*)memPtr; + return (U16)((U16)p[0] | (p[1]<<8)); + } +} + +#ifdef LZ4_STATIC_LINKING_ONLY_ENDIANNESS_INDEPENDENT_OUTPUT +static U32 LZ4_readLE32(const void* memPtr) +{ + if (LZ4_isLittleEndian()) { + return LZ4_read32(memPtr); + } else { + const BYTE* p = (const BYTE*)memPtr; + return (U32)p[0] | (p[1]<<8) | (p[2]<<16) | (p[3]<<24); + } +} +#endif + +static void LZ4_writeLE16(void* memPtr, U16 value) +{ + if (LZ4_isLittleEndian()) { + LZ4_write16(memPtr, value); + } else { + BYTE* p = (BYTE*)memPtr; + p[0] = (BYTE) value; + p[1] = (BYTE)(value>>8); + } +} + +/* customized variant of memcpy, which can overwrite up to 8 bytes beyond dstEnd */ +LZ4_FORCE_INLINE +void LZ4_wildCopy8(void* dstPtr, const void* srcPtr, void* dstEnd) +{ + BYTE* d = (BYTE*)dstPtr; + const BYTE* s = (const BYTE*)srcPtr; + BYTE* const e = (BYTE*)dstEnd; + + do { LZ4_memcpy(d,s,8); d+=8; s+=8; } while (d= 16. */ +LZ4_FORCE_INLINE void +LZ4_wildCopy32(void* dstPtr, const void* srcPtr, void* dstEnd) +{ + BYTE* d = (BYTE*)dstPtr; + const BYTE* s = (const BYTE*)srcPtr; + BYTE* const e = (BYTE*)dstEnd; + + do { LZ4_memcpy(d,s,16); LZ4_memcpy(d+16,s+16,16); d+=32; s+=32; } while (d= dstPtr + MINMATCH + * - there is at least 12 bytes available to write after dstEnd */ +LZ4_FORCE_INLINE void +LZ4_memcpy_using_offset(BYTE* dstPtr, const BYTE* srcPtr, BYTE* dstEnd, const size_t offset) +{ + BYTE v[8]; + + assert(dstEnd >= dstPtr + MINMATCH); + + switch(offset) { + case 1: + MEM_INIT(v, *srcPtr, 8); + break; + case 2: + LZ4_memcpy(v, srcPtr, 2); + LZ4_memcpy(&v[2], srcPtr, 2); +#if defined(_MSC_VER) && (_MSC_VER <= 1937) /* MSVC 2022 ver 17.7 or earlier */ +# pragma warning(push) +# pragma warning(disable : 6385) /* warning C6385: Reading invalid data from 'v'. */ +#endif + LZ4_memcpy(&v[4], v, 4); +#if defined(_MSC_VER) && (_MSC_VER <= 1937) /* MSVC 2022 ver 17.7 or earlier */ +# pragma warning(pop) +#endif + break; + case 4: + LZ4_memcpy(v, srcPtr, 4); + LZ4_memcpy(&v[4], srcPtr, 4); + break; + default: + LZ4_memcpy_using_offset_base(dstPtr, srcPtr, dstEnd, offset); + return; + } + + LZ4_memcpy(dstPtr, v, 8); + dstPtr += 8; + while (dstPtr < dstEnd) { + LZ4_memcpy(dstPtr, v, 8); + dstPtr += 8; + } +} +#endif + + +/*-************************************ +* Common functions +**************************************/ +static unsigned LZ4_NbCommonBytes (reg_t val) +{ + assert(val != 0); + if (LZ4_isLittleEndian()) { + if (sizeof(val) == 8) { +# if defined(_MSC_VER) && (_MSC_VER >= 1800) && (defined(_M_AMD64) && !defined(_M_ARM64EC)) && !defined(LZ4_FORCE_SW_BITCOUNT) +/*-************************************************************************************************* +* ARM64EC is a Microsoft-designed ARM64 ABI compatible with AMD64 applications on ARM64 Windows 11. +* The ARM64EC ABI does not support AVX/AVX2/AVX512 instructions, nor their relevant intrinsics +* including _tzcnt_u64. Therefore, we need to neuter the _tzcnt_u64 code path for ARM64EC. +****************************************************************************************************/ +# if defined(__clang__) && (__clang_major__ < 10) + /* Avoid undefined clang-cl intrinsics issue. + * See https://github.com/lz4/lz4/pull/1017 for details. */ + return (unsigned)__builtin_ia32_tzcnt_u64(val) >> 3; +# else + /* x64 CPUS without BMI support interpret `TZCNT` as `REP BSF` */ + return (unsigned)_tzcnt_u64(val) >> 3; +# endif +# elif defined(_MSC_VER) && defined(_WIN64) && !defined(LZ4_FORCE_SW_BITCOUNT) + unsigned long r = 0; + _BitScanForward64(&r, (U64)val); + return (unsigned)r >> 3; +# elif (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ + ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ + !defined(LZ4_FORCE_SW_BITCOUNT) + return (unsigned)__builtin_ctzll((U64)val) >> 3; +# else + const U64 m = 0x0101010101010101ULL; + val ^= val - 1; + return (unsigned)(((U64)((val & (m - 1)) * m)) >> 56); +# endif + } else /* 32 bits */ { +# if defined(_MSC_VER) && (_MSC_VER >= 1400) && !defined(LZ4_FORCE_SW_BITCOUNT) + unsigned long r; + _BitScanForward(&r, (U32)val); + return (unsigned)r >> 3; +# elif (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ + ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ + !defined(__TINYC__) && !defined(LZ4_FORCE_SW_BITCOUNT) + return (unsigned)__builtin_ctz((U32)val) >> 3; +# else + const U32 m = 0x01010101; + return (unsigned)((((val - 1) ^ val) & (m - 1)) * m) >> 24; +# endif + } + } else /* Big Endian CPU */ { + if (sizeof(val)==8) { +# if (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ + ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ + !defined(__TINYC__) && !defined(LZ4_FORCE_SW_BITCOUNT) + return (unsigned)__builtin_clzll((U64)val) >> 3; +# else +#if 1 + /* this method is probably faster, + * but adds a 128 bytes lookup table */ + static const unsigned char ctz7_tab[128] = { + 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, + }; + U64 const mask = 0x0101010101010101ULL; + U64 const t = (((val >> 8) - mask) | val) & mask; + return ctz7_tab[(t * 0x0080402010080402ULL) >> 57]; +#else + /* this method doesn't consume memory space like the previous one, + * but it contains several branches, + * that may end up slowing execution */ + static const U32 by32 = sizeof(val)*4; /* 32 on 64 bits (goal), 16 on 32 bits. + Just to avoid some static analyzer complaining about shift by 32 on 32-bits target. + Note that this code path is never triggered in 32-bits mode. */ + unsigned r; + if (!(val>>by32)) { r=4; } else { r=0; val>>=by32; } + if (!(val>>16)) { r+=2; val>>=8; } else { val>>=24; } + r += (!val); + return r; +#endif +# endif + } else /* 32 bits */ { +# if (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ + ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ + !defined(LZ4_FORCE_SW_BITCOUNT) + return (unsigned)__builtin_clz((U32)val) >> 3; +# else + val >>= 8; + val = ((((val + 0x00FFFF00) | 0x00FFFFFF) + val) | + (val + 0x00FF0000)) >> 24; + return (unsigned)val ^ 3; +# endif + } + } +} + + +#define STEPSIZE sizeof(reg_t) +LZ4_FORCE_INLINE +unsigned LZ4_count(const BYTE* pIn, const BYTE* pMatch, const BYTE* pInLimit) +{ + const BYTE* const pStart = pIn; + + if (likely(pIn < pInLimit-(STEPSIZE-1))) { + reg_t const diff = LZ4_read_ARCH(pMatch) ^ LZ4_read_ARCH(pIn); + if (!diff) { + pIn+=STEPSIZE; pMatch+=STEPSIZE; + } else { + return LZ4_NbCommonBytes(diff); + } } + + while (likely(pIn < pInLimit-(STEPSIZE-1))) { + reg_t const diff = LZ4_read_ARCH(pMatch) ^ LZ4_read_ARCH(pIn); + if (!diff) { pIn+=STEPSIZE; pMatch+=STEPSIZE; continue; } + pIn += LZ4_NbCommonBytes(diff); + return (unsigned)(pIn - pStart); + } + + if ((STEPSIZE==8) && (pIn<(pInLimit-3)) && (LZ4_read32(pMatch) == LZ4_read32(pIn))) { pIn+=4; pMatch+=4; } + if ((pIn<(pInLimit-1)) && (LZ4_read16(pMatch) == LZ4_read16(pIn))) { pIn+=2; pMatch+=2; } + if ((pIn compression run slower on incompressible data */ + + +/*-************************************ +* Local Structures and types +**************************************/ +typedef enum { clearedTable = 0, byPtr, byU32, byU16 } tableType_t; + +/** + * This enum distinguishes several different modes of accessing previous + * content in the stream. + * + * - noDict : There is no preceding content. + * - withPrefix64k : Table entries up to ctx->dictSize before the current blob + * blob being compressed are valid and refer to the preceding + * content (of length ctx->dictSize), which is available + * contiguously preceding in memory the content currently + * being compressed. + * - usingExtDict : Like withPrefix64k, but the preceding content is somewhere + * else in memory, starting at ctx->dictionary with length + * ctx->dictSize. + * - usingDictCtx : Everything concerning the preceding content is + * in a separate context, pointed to by ctx->dictCtx. + * ctx->dictionary, ctx->dictSize, and table entries + * in the current context that refer to positions + * preceding the beginning of the current compression are + * ignored. Instead, ctx->dictCtx->dictionary and ctx->dictCtx + * ->dictSize describe the location and size of the preceding + * content, and matches are found by looking in the ctx + * ->dictCtx->hashTable. + */ +typedef enum { noDict = 0, withPrefix64k, usingExtDict, usingDictCtx } dict_directive; +typedef enum { noDictIssue = 0, dictSmall } dictIssue_directive; + + +/*-************************************ +* Local Utils +**************************************/ +int LZ4_versionNumber (void) { return LZ4_VERSION_NUMBER; } +const char* LZ4_versionString(void) { return LZ4_VERSION_STRING; } +int LZ4_compressBound(int isize) { return LZ4_COMPRESSBOUND(isize); } +int LZ4_sizeofState(void) { return sizeof(LZ4_stream_t); } + + +/*-**************************************** +* Internal Definitions, used only in Tests +*******************************************/ +#if defined (__cplusplus) +extern "C" { +#endif + +int LZ4_compress_forceExtDict (LZ4_stream_t* LZ4_dict, const char* source, char* dest, int srcSize); + +int LZ4_decompress_safe_forceExtDict(const char* source, char* dest, + int compressedSize, int maxOutputSize, + const void* dictStart, size_t dictSize); +int LZ4_decompress_safe_partial_forceExtDict(const char* source, char* dest, + int compressedSize, int targetOutputSize, int dstCapacity, + const void* dictStart, size_t dictSize); +#if defined (__cplusplus) +} +#endif + +/*-****************************** +* Compression functions +********************************/ +LZ4_FORCE_INLINE U32 LZ4_hash4(U32 sequence, tableType_t const tableType) +{ + if (tableType == byU16) + return ((sequence * 2654435761U) >> ((MINMATCH*8)-(LZ4_HASHLOG+1))); + else + return ((sequence * 2654435761U) >> ((MINMATCH*8)-LZ4_HASHLOG)); +} + +LZ4_FORCE_INLINE U32 LZ4_hash5(U64 sequence, tableType_t const tableType) +{ + const U32 hashLog = (tableType == byU16) ? LZ4_HASHLOG+1 : LZ4_HASHLOG; + if (LZ4_isLittleEndian()) { + const U64 prime5bytes = 889523592379ULL; + return (U32)(((sequence << 24) * prime5bytes) >> (64 - hashLog)); + } else { + const U64 prime8bytes = 11400714785074694791ULL; + return (U32)(((sequence >> 24) * prime8bytes) >> (64 - hashLog)); + } +} + +LZ4_FORCE_INLINE U32 LZ4_hashPosition(const void* const p, tableType_t const tableType) +{ + if ((sizeof(reg_t)==8) && (tableType != byU16)) return LZ4_hash5(LZ4_read_ARCH(p), tableType); + +#ifdef LZ4_STATIC_LINKING_ONLY_ENDIANNESS_INDEPENDENT_OUTPUT + return LZ4_hash4(LZ4_readLE32(p), tableType); +#else + return LZ4_hash4(LZ4_read32(p), tableType); +#endif +} + +LZ4_FORCE_INLINE void LZ4_clearHash(U32 h, void* tableBase, tableType_t const tableType) +{ + switch (tableType) + { + default: /* fallthrough */ + case clearedTable: { /* illegal! */ assert(0); return; } + case byPtr: { const BYTE** hashTable = (const BYTE**)tableBase; hashTable[h] = NULL; return; } + case byU32: { U32* hashTable = (U32*) tableBase; hashTable[h] = 0; return; } + case byU16: { U16* hashTable = (U16*) tableBase; hashTable[h] = 0; return; } + } +} + +LZ4_FORCE_INLINE void LZ4_putIndexOnHash(U32 idx, U32 h, void* tableBase, tableType_t const tableType) +{ + switch (tableType) + { + default: /* fallthrough */ + case clearedTable: /* fallthrough */ + case byPtr: { /* illegal! */ assert(0); return; } + case byU32: { U32* hashTable = (U32*) tableBase; hashTable[h] = idx; return; } + case byU16: { U16* hashTable = (U16*) tableBase; assert(idx < 65536); hashTable[h] = (U16)idx; return; } + } +} + +/* LZ4_putPosition*() : only used in byPtr mode */ +LZ4_FORCE_INLINE void LZ4_putPositionOnHash(const BYTE* p, U32 h, + void* tableBase, tableType_t const tableType) +{ + const BYTE** const hashTable = (const BYTE**)tableBase; + assert(tableType == byPtr); (void)tableType; + hashTable[h] = p; +} + +LZ4_FORCE_INLINE void LZ4_putPosition(const BYTE* p, void* tableBase, tableType_t tableType) +{ + U32 const h = LZ4_hashPosition(p, tableType); + LZ4_putPositionOnHash(p, h, tableBase, tableType); +} + +/* LZ4_getIndexOnHash() : + * Index of match position registered in hash table. + * hash position must be calculated by using base+index, or dictBase+index. + * Assumption 1 : only valid if tableType == byU32 or byU16. + * Assumption 2 : h is presumed valid (within limits of hash table) + */ +LZ4_FORCE_INLINE U32 LZ4_getIndexOnHash(U32 h, const void* tableBase, tableType_t tableType) +{ + LZ4_STATIC_ASSERT(LZ4_MEMORY_USAGE > 2); + if (tableType == byU32) { + const U32* const hashTable = (const U32*) tableBase; + assert(h < (1U << (LZ4_MEMORY_USAGE-2))); + return hashTable[h]; + } + if (tableType == byU16) { + const U16* const hashTable = (const U16*) tableBase; + assert(h < (1U << (LZ4_MEMORY_USAGE-1))); + return hashTable[h]; + } + assert(0); return 0; /* forbidden case */ +} + +static const BYTE* LZ4_getPositionOnHash(U32 h, const void* tableBase, tableType_t tableType) +{ + assert(tableType == byPtr); (void)tableType; + { const BYTE* const* hashTable = (const BYTE* const*) tableBase; return hashTable[h]; } +} + +LZ4_FORCE_INLINE const BYTE* +LZ4_getPosition(const BYTE* p, + const void* tableBase, tableType_t tableType) +{ + U32 const h = LZ4_hashPosition(p, tableType); + return LZ4_getPositionOnHash(h, tableBase, tableType); +} + +LZ4_FORCE_INLINE void +LZ4_prepareTable(LZ4_stream_t_internal* const cctx, + const int inputSize, + const tableType_t tableType) { + /* If the table hasn't been used, it's guaranteed to be zeroed out, and is + * therefore safe to use no matter what mode we're in. Otherwise, we figure + * out if it's safe to leave as is or whether it needs to be reset. + */ + if ((tableType_t)cctx->tableType != clearedTable) { + assert(inputSize >= 0); + if ((tableType_t)cctx->tableType != tableType + || ((tableType == byU16) && cctx->currentOffset + (unsigned)inputSize >= 0xFFFFU) + || ((tableType == byU32) && cctx->currentOffset > 1 GB) + || tableType == byPtr + || inputSize >= 4 KB) + { + DEBUGLOG(4, "LZ4_prepareTable: Resetting table in %p", (void*)cctx); + MEM_INIT(cctx->hashTable, 0, LZ4_HASHTABLESIZE); + cctx->currentOffset = 0; + cctx->tableType = (U32)clearedTable; + } else { + DEBUGLOG(4, "LZ4_prepareTable: Re-use hash table (no reset)"); + } + } + + /* Adding a gap, so all previous entries are > LZ4_DISTANCE_MAX back, + * is faster than compressing without a gap. + * However, compressing with currentOffset == 0 is faster still, + * so we preserve that case. + */ + if (cctx->currentOffset != 0 && tableType == byU32) { + DEBUGLOG(5, "LZ4_prepareTable: adding 64KB to currentOffset"); + cctx->currentOffset += 64 KB; + } + + /* Finally, clear history */ + cctx->dictCtx = NULL; + cctx->dictionary = NULL; + cctx->dictSize = 0; +} + +/** LZ4_compress_generic_validated() : + * inlined, to ensure branches are decided at compilation time. + * The following conditions are presumed already validated: + * - source != NULL + * - inputSize > 0 + */ +LZ4_FORCE_INLINE int LZ4_compress_generic_validated( + LZ4_stream_t_internal* const cctx, + const char* const source, + char* const dest, + const int inputSize, + int* inputConsumed, /* only written when outputDirective == fillOutput */ + const int maxOutputSize, + const limitedOutput_directive outputDirective, + const tableType_t tableType, + const dict_directive dictDirective, + const dictIssue_directive dictIssue, + const int acceleration) +{ + int result; + const BYTE* ip = (const BYTE*)source; + + U32 const startIndex = cctx->currentOffset; + const BYTE* base = (const BYTE*)source - startIndex; + const BYTE* lowLimit; + + const LZ4_stream_t_internal* dictCtx = (const LZ4_stream_t_internal*) cctx->dictCtx; + const BYTE* const dictionary = + dictDirective == usingDictCtx ? dictCtx->dictionary : cctx->dictionary; + const U32 dictSize = + dictDirective == usingDictCtx ? dictCtx->dictSize : cctx->dictSize; + const U32 dictDelta = + (dictDirective == usingDictCtx) ? startIndex - dictCtx->currentOffset : 0; /* make indexes in dictCtx comparable with indexes in current context */ + + int const maybe_extMem = (dictDirective == usingExtDict) || (dictDirective == usingDictCtx); + U32 const prefixIdxLimit = startIndex - dictSize; /* used when dictDirective == dictSmall */ + const BYTE* const dictEnd = dictionary ? dictionary + dictSize : dictionary; + const BYTE* anchor = (const BYTE*) source; + const BYTE* const iend = ip + inputSize; + const BYTE* const mflimitPlusOne = iend - MFLIMIT + 1; + const BYTE* const matchlimit = iend - LASTLITERALS; + + /* the dictCtx currentOffset is indexed on the start of the dictionary, + * while a dictionary in the current context precedes the currentOffset */ + const BYTE* dictBase = (dictionary == NULL) ? NULL : + (dictDirective == usingDictCtx) ? + dictionary + dictSize - dictCtx->currentOffset : + dictionary + dictSize - startIndex; + + BYTE* op = (BYTE*) dest; + BYTE* const olimit = op + maxOutputSize; + + U32 offset = 0; + U32 forwardH; + + DEBUGLOG(5, "LZ4_compress_generic_validated: srcSize=%i, tableType=%u", inputSize, tableType); + assert(ip != NULL); + if (tableType == byU16) assert(inputSize= 1); + + lowLimit = (const BYTE*)source - (dictDirective == withPrefix64k ? dictSize : 0); + + /* Update context state */ + if (dictDirective == usingDictCtx) { + /* Subsequent linked blocks can't use the dictionary. */ + /* Instead, they use the block we just compressed. */ + cctx->dictCtx = NULL; + cctx->dictSize = (U32)inputSize; + } else { + cctx->dictSize += (U32)inputSize; + } + cctx->currentOffset += (U32)inputSize; + cctx->tableType = (U32)tableType; + + if (inputSizehashTable, byPtr); + } else { + LZ4_putIndexOnHash(startIndex, h, cctx->hashTable, tableType); + } } + ip++; forwardH = LZ4_hashPosition(ip, tableType); + + /* Main Loop */ + for ( ; ; ) { + const BYTE* match; + BYTE* token; + const BYTE* filledIp; + + /* Find a match */ + if (tableType == byPtr) { + const BYTE* forwardIp = ip; + int step = 1; + int searchMatchNb = acceleration << LZ4_skipTrigger; + do { + U32 const h = forwardH; + ip = forwardIp; + forwardIp += step; + step = (searchMatchNb++ >> LZ4_skipTrigger); + + if (unlikely(forwardIp > mflimitPlusOne)) goto _last_literals; + assert(ip < mflimitPlusOne); + + match = LZ4_getPositionOnHash(h, cctx->hashTable, tableType); + forwardH = LZ4_hashPosition(forwardIp, tableType); + LZ4_putPositionOnHash(ip, h, cctx->hashTable, tableType); + + } while ( (match+LZ4_DISTANCE_MAX < ip) + || (LZ4_read32(match) != LZ4_read32(ip)) ); + + } else { /* byU32, byU16 */ + + const BYTE* forwardIp = ip; + int step = 1; + int searchMatchNb = acceleration << LZ4_skipTrigger; + do { + U32 const h = forwardH; + U32 const current = (U32)(forwardIp - base); + U32 matchIndex = LZ4_getIndexOnHash(h, cctx->hashTable, tableType); + assert(matchIndex <= current); + assert(forwardIp - base < (ptrdiff_t)(2 GB - 1)); + ip = forwardIp; + forwardIp += step; + step = (searchMatchNb++ >> LZ4_skipTrigger); + + if (unlikely(forwardIp > mflimitPlusOne)) goto _last_literals; + assert(ip < mflimitPlusOne); + + if (dictDirective == usingDictCtx) { + if (matchIndex < startIndex) { + /* there was no match, try the dictionary */ + assert(tableType == byU32); + matchIndex = LZ4_getIndexOnHash(h, dictCtx->hashTable, byU32); + match = dictBase + matchIndex; + matchIndex += dictDelta; /* make dictCtx index comparable with current context */ + lowLimit = dictionary; + } else { + match = base + matchIndex; + lowLimit = (const BYTE*)source; + } + } else if (dictDirective == usingExtDict) { + if (matchIndex < startIndex) { + DEBUGLOG(7, "extDict candidate: matchIndex=%5u < startIndex=%5u", matchIndex, startIndex); + assert(startIndex - matchIndex >= MINMATCH); + assert(dictBase); + match = dictBase + matchIndex; + lowLimit = dictionary; + } else { + match = base + matchIndex; + lowLimit = (const BYTE*)source; + } + } else { /* single continuous memory segment */ + match = base + matchIndex; + } + forwardH = LZ4_hashPosition(forwardIp, tableType); + LZ4_putIndexOnHash(current, h, cctx->hashTable, tableType); + + DEBUGLOG(7, "candidate at pos=%u (offset=%u \n", matchIndex, current - matchIndex); + if ((dictIssue == dictSmall) && (matchIndex < prefixIdxLimit)) { continue; } /* match outside of valid area */ + assert(matchIndex < current); + if ( ((tableType != byU16) || (LZ4_DISTANCE_MAX < LZ4_DISTANCE_ABSOLUTE_MAX)) + && (matchIndex+LZ4_DISTANCE_MAX < current)) { + continue; + } /* too far */ + assert((current - matchIndex) <= LZ4_DISTANCE_MAX); /* match now expected within distance */ + + if (LZ4_read32(match) == LZ4_read32(ip)) { + if (maybe_extMem) offset = current - matchIndex; + break; /* match found */ + } + + } while(1); + } + + /* Catch up */ + filledIp = ip; + assert(ip > anchor); /* this is always true as ip has been advanced before entering the main loop */ + if ((match > lowLimit) && unlikely(ip[-1] == match[-1])) { + do { ip--; match--; } while (((ip > anchor) & (match > lowLimit)) && (unlikely(ip[-1] == match[-1]))); + } + + /* Encode Literals */ + { unsigned const litLength = (unsigned)(ip - anchor); + token = op++; + if ((outputDirective == limitedOutput) && /* Check output buffer overflow */ + (unlikely(op + litLength + (2 + 1 + LASTLITERALS) + (litLength/255) > olimit)) ) { + return 0; /* cannot compress within `dst` budget. Stored indexes in hash table are nonetheless fine */ + } + if ((outputDirective == fillOutput) && + (unlikely(op + (litLength+240)/255 /* litlen */ + litLength /* literals */ + 2 /* offset */ + 1 /* token */ + MFLIMIT - MINMATCH /* min last literals so last match is <= end - MFLIMIT */ > olimit))) { + op--; + goto _last_literals; + } + if (litLength >= RUN_MASK) { + unsigned len = litLength - RUN_MASK; + *token = (RUN_MASK<= 255 ; len-=255) *op++ = 255; + *op++ = (BYTE)len; + } + else *token = (BYTE)(litLength< olimit)) { + /* the match was too close to the end, rewind and go to last literals */ + op = token; + goto _last_literals; + } + + /* Encode Offset */ + if (maybe_extMem) { /* static test */ + DEBUGLOG(6, " with offset=%u (ext if > %i)", offset, (int)(ip - (const BYTE*)source)); + assert(offset <= LZ4_DISTANCE_MAX && offset > 0); + LZ4_writeLE16(op, (U16)offset); op+=2; + } else { + DEBUGLOG(6, " with offset=%u (same segment)", (U32)(ip - match)); + assert(ip-match <= LZ4_DISTANCE_MAX); + LZ4_writeLE16(op, (U16)(ip - match)); op+=2; + } + + /* Encode MatchLength */ + { unsigned matchCode; + + if ( (dictDirective==usingExtDict || dictDirective==usingDictCtx) + && (lowLimit==dictionary) /* match within extDict */ ) { + const BYTE* limit = ip + (dictEnd-match); + assert(dictEnd > match); + if (limit > matchlimit) limit = matchlimit; + matchCode = LZ4_count(ip+MINMATCH, match+MINMATCH, limit); + ip += (size_t)matchCode + MINMATCH; + if (ip==limit) { + unsigned const more = LZ4_count(limit, (const BYTE*)source, matchlimit); + matchCode += more; + ip += more; + } + DEBUGLOG(6, " with matchLength=%u starting in extDict", matchCode+MINMATCH); + } else { + matchCode = LZ4_count(ip+MINMATCH, match+MINMATCH, matchlimit); + ip += (size_t)matchCode + MINMATCH; + DEBUGLOG(6, " with matchLength=%u", matchCode+MINMATCH); + } + + if ((outputDirective) && /* Check output buffer overflow */ + (unlikely(op + (1 + LASTLITERALS) + (matchCode+240)/255 > olimit)) ) { + if (outputDirective == fillOutput) { + /* Match description too long : reduce it */ + U32 newMatchCode = 15 /* in token */ - 1 /* to avoid needing a zero byte */ + ((U32)(olimit - op) - 1 - LASTLITERALS) * 255; + ip -= matchCode - newMatchCode; + assert(newMatchCode < matchCode); + matchCode = newMatchCode; + if (unlikely(ip <= filledIp)) { + /* We have already filled up to filledIp so if ip ends up less than filledIp + * we have positions in the hash table beyond the current position. This is + * a problem if we reuse the hash table. So we have to remove these positions + * from the hash table. + */ + const BYTE* ptr; + DEBUGLOG(5, "Clearing %u positions", (U32)(filledIp - ip)); + for (ptr = ip; ptr <= filledIp; ++ptr) { + U32 const h = LZ4_hashPosition(ptr, tableType); + LZ4_clearHash(h, cctx->hashTable, tableType); + } + } + } else { + assert(outputDirective == limitedOutput); + return 0; /* cannot compress within `dst` budget. Stored indexes in hash table are nonetheless fine */ + } + } + if (matchCode >= ML_MASK) { + *token += ML_MASK; + matchCode -= ML_MASK; + LZ4_write32(op, 0xFFFFFFFF); + while (matchCode >= 4*255) { + op+=4; + LZ4_write32(op, 0xFFFFFFFF); + matchCode -= 4*255; + } + op += matchCode / 255; + *op++ = (BYTE)(matchCode % 255); + } else + *token += (BYTE)(matchCode); + } + /* Ensure we have enough space for the last literals. */ + assert(!(outputDirective == fillOutput && op + 1 + LASTLITERALS > olimit)); + + anchor = ip; + + /* Test end of chunk */ + if (ip >= mflimitPlusOne) break; + + /* Fill table */ + { U32 const h = LZ4_hashPosition(ip-2, tableType); + if (tableType == byPtr) { + LZ4_putPositionOnHash(ip-2, h, cctx->hashTable, byPtr); + } else { + U32 const idx = (U32)((ip-2) - base); + LZ4_putIndexOnHash(idx, h, cctx->hashTable, tableType); + } } + + /* Test next position */ + if (tableType == byPtr) { + + match = LZ4_getPosition(ip, cctx->hashTable, tableType); + LZ4_putPosition(ip, cctx->hashTable, tableType); + if ( (match+LZ4_DISTANCE_MAX >= ip) + && (LZ4_read32(match) == LZ4_read32(ip)) ) + { token=op++; *token=0; goto _next_match; } + + } else { /* byU32, byU16 */ + + U32 const h = LZ4_hashPosition(ip, tableType); + U32 const current = (U32)(ip-base); + U32 matchIndex = LZ4_getIndexOnHash(h, cctx->hashTable, tableType); + assert(matchIndex < current); + if (dictDirective == usingDictCtx) { + if (matchIndex < startIndex) { + /* there was no match, try the dictionary */ + assert(tableType == byU32); + matchIndex = LZ4_getIndexOnHash(h, dictCtx->hashTable, byU32); + match = dictBase + matchIndex; + lowLimit = dictionary; /* required for match length counter */ + matchIndex += dictDelta; + } else { + match = base + matchIndex; + lowLimit = (const BYTE*)source; /* required for match length counter */ + } + } else if (dictDirective==usingExtDict) { + if (matchIndex < startIndex) { + assert(dictBase); + match = dictBase + matchIndex; + lowLimit = dictionary; /* required for match length counter */ + } else { + match = base + matchIndex; + lowLimit = (const BYTE*)source; /* required for match length counter */ + } + } else { /* single memory segment */ + match = base + matchIndex; + } + LZ4_putIndexOnHash(current, h, cctx->hashTable, tableType); + assert(matchIndex < current); + if ( ((dictIssue==dictSmall) ? (matchIndex >= prefixIdxLimit) : 1) + && (((tableType==byU16) && (LZ4_DISTANCE_MAX == LZ4_DISTANCE_ABSOLUTE_MAX)) ? 1 : (matchIndex+LZ4_DISTANCE_MAX >= current)) + && (LZ4_read32(match) == LZ4_read32(ip)) ) { + token=op++; + *token=0; + if (maybe_extMem) offset = current - matchIndex; + DEBUGLOG(6, "seq.start:%i, literals=%u, match.start:%i", + (int)(anchor-(const BYTE*)source), 0, (int)(ip-(const BYTE*)source)); + goto _next_match; + } + } + + /* Prepare next loop */ + forwardH = LZ4_hashPosition(++ip, tableType); + + } + +_last_literals: + /* Encode Last Literals */ + { size_t lastRun = (size_t)(iend - anchor); + if ( (outputDirective) && /* Check output buffer overflow */ + (op + lastRun + 1 + ((lastRun+255-RUN_MASK)/255) > olimit)) { + if (outputDirective == fillOutput) { + /* adapt lastRun to fill 'dst' */ + assert(olimit >= op); + lastRun = (size_t)(olimit-op) - 1/*token*/; + lastRun -= (lastRun + 256 - RUN_MASK) / 256; /*additional length tokens*/ + } else { + assert(outputDirective == limitedOutput); + return 0; /* cannot compress within `dst` budget. Stored indexes in hash table are nonetheless fine */ + } + } + DEBUGLOG(6, "Final literal run : %i literals", (int)lastRun); + if (lastRun >= RUN_MASK) { + size_t accumulator = lastRun - RUN_MASK; + *op++ = RUN_MASK << ML_BITS; + for(; accumulator >= 255 ; accumulator-=255) *op++ = 255; + *op++ = (BYTE) accumulator; + } else { + *op++ = (BYTE)(lastRun< 0); + DEBUGLOG(5, "LZ4_compress_generic: compressed %i bytes into %i bytes", inputSize, result); + return result; +} + +/** LZ4_compress_generic() : + * inlined, to ensure branches are decided at compilation time; + * takes care of src == (NULL, 0) + * and forward the rest to LZ4_compress_generic_validated */ +LZ4_FORCE_INLINE int LZ4_compress_generic( + LZ4_stream_t_internal* const cctx, + const char* const src, + char* const dst, + const int srcSize, + int *inputConsumed, /* only written when outputDirective == fillOutput */ + const int dstCapacity, + const limitedOutput_directive outputDirective, + const tableType_t tableType, + const dict_directive dictDirective, + const dictIssue_directive dictIssue, + const int acceleration) +{ + DEBUGLOG(5, "LZ4_compress_generic: srcSize=%i, dstCapacity=%i", + srcSize, dstCapacity); + + if ((U32)srcSize > (U32)LZ4_MAX_INPUT_SIZE) { return 0; } /* Unsupported srcSize, too large (or negative) */ + if (srcSize == 0) { /* src == NULL supported if srcSize == 0 */ + if (outputDirective != notLimited && dstCapacity <= 0) return 0; /* no output, can't write anything */ + DEBUGLOG(5, "Generating an empty block"); + assert(outputDirective == notLimited || dstCapacity >= 1); + assert(dst != NULL); + dst[0] = 0; + if (outputDirective == fillOutput) { + assert (inputConsumed != NULL); + *inputConsumed = 0; + } + return 1; + } + assert(src != NULL); + + return LZ4_compress_generic_validated(cctx, src, dst, srcSize, + inputConsumed, /* only written into if outputDirective == fillOutput */ + dstCapacity, outputDirective, + tableType, dictDirective, dictIssue, acceleration); +} + + +int LZ4_compress_fast_extState(void* state, const char* source, char* dest, int inputSize, int maxOutputSize, int acceleration) +{ + LZ4_stream_t_internal* const ctx = & LZ4_initStream(state, sizeof(LZ4_stream_t)) -> internal_donotuse; + assert(ctx != NULL); + if (acceleration < 1) acceleration = LZ4_ACCELERATION_DEFAULT; + if (acceleration > LZ4_ACCELERATION_MAX) acceleration = LZ4_ACCELERATION_MAX; + if (maxOutputSize >= LZ4_compressBound(inputSize)) { + if (inputSize < LZ4_64Klimit) { + return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, 0, notLimited, byU16, noDict, noDictIssue, acceleration); + } else { + const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)source > LZ4_DISTANCE_MAX)) ? byPtr : byU32; + return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration); + } + } else { + if (inputSize < LZ4_64Klimit) { + return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration); + } else { + const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)source > LZ4_DISTANCE_MAX)) ? byPtr : byU32; + return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, noDict, noDictIssue, acceleration); + } + } +} + +/** + * LZ4_compress_fast_extState_fastReset() : + * A variant of LZ4_compress_fast_extState(). + * + * Using this variant avoids an expensive initialization step. It is only safe + * to call if the state buffer is known to be correctly initialized already + * (see comment in lz4.h on LZ4_resetStream_fast() for a definition of + * "correctly initialized"). + */ +int LZ4_compress_fast_extState_fastReset(void* state, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration) +{ + LZ4_stream_t_internal* const ctx = &((LZ4_stream_t*)state)->internal_donotuse; + if (acceleration < 1) acceleration = LZ4_ACCELERATION_DEFAULT; + if (acceleration > LZ4_ACCELERATION_MAX) acceleration = LZ4_ACCELERATION_MAX; + assert(ctx != NULL); + + if (dstCapacity >= LZ4_compressBound(srcSize)) { + if (srcSize < LZ4_64Klimit) { + const tableType_t tableType = byU16; + LZ4_prepareTable(ctx, srcSize, tableType); + if (ctx->currentOffset) { + return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, 0, notLimited, tableType, noDict, dictSmall, acceleration); + } else { + return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration); + } + } else { + const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)src > LZ4_DISTANCE_MAX)) ? byPtr : byU32; + LZ4_prepareTable(ctx, srcSize, tableType); + return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration); + } + } else { + if (srcSize < LZ4_64Klimit) { + const tableType_t tableType = byU16; + LZ4_prepareTable(ctx, srcSize, tableType); + if (ctx->currentOffset) { + return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, dstCapacity, limitedOutput, tableType, noDict, dictSmall, acceleration); + } else { + return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, dstCapacity, limitedOutput, tableType, noDict, noDictIssue, acceleration); + } + } else { + const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)src > LZ4_DISTANCE_MAX)) ? byPtr : byU32; + LZ4_prepareTable(ctx, srcSize, tableType); + return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, dstCapacity, limitedOutput, tableType, noDict, noDictIssue, acceleration); + } + } +} + + +int LZ4_compress_fast(const char* src, char* dest, int srcSize, int dstCapacity, int acceleration) +{ + int result; +#if (LZ4_HEAPMODE) + LZ4_stream_t* const ctxPtr = (LZ4_stream_t*)ALLOC(sizeof(LZ4_stream_t)); /* malloc-calloc always properly aligned */ + if (ctxPtr == NULL) return 0; +#else + LZ4_stream_t ctx; + LZ4_stream_t* const ctxPtr = &ctx; +#endif + result = LZ4_compress_fast_extState(ctxPtr, src, dest, srcSize, dstCapacity, acceleration); + +#if (LZ4_HEAPMODE) + FREEMEM(ctxPtr); +#endif + return result; +} + + +int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity) +{ + return LZ4_compress_fast(src, dst, srcSize, dstCapacity, 1); +} + + +/* Note!: This function leaves the stream in an unclean/broken state! + * It is not safe to subsequently use the same state with a _fastReset() or + * _continue() call without resetting it. */ +static int LZ4_compress_destSize_extState_internal(LZ4_stream_t* state, const char* src, char* dst, int* srcSizePtr, int targetDstSize, int acceleration) +{ + void* const s = LZ4_initStream(state, sizeof (*state)); + assert(s != NULL); (void)s; + + if (targetDstSize >= LZ4_compressBound(*srcSizePtr)) { /* compression success is guaranteed */ + return LZ4_compress_fast_extState(state, src, dst, *srcSizePtr, targetDstSize, acceleration); + } else { + if (*srcSizePtr < LZ4_64Klimit) { + return LZ4_compress_generic(&state->internal_donotuse, src, dst, *srcSizePtr, srcSizePtr, targetDstSize, fillOutput, byU16, noDict, noDictIssue, acceleration); + } else { + tableType_t const addrMode = ((sizeof(void*)==4) && ((uptrval)src > LZ4_DISTANCE_MAX)) ? byPtr : byU32; + return LZ4_compress_generic(&state->internal_donotuse, src, dst, *srcSizePtr, srcSizePtr, targetDstSize, fillOutput, addrMode, noDict, noDictIssue, acceleration); + } } +} + +int LZ4_compress_destSize_extState(void* state, const char* src, char* dst, int* srcSizePtr, int targetDstSize, int acceleration) +{ + int const r = LZ4_compress_destSize_extState_internal((LZ4_stream_t*)state, src, dst, srcSizePtr, targetDstSize, acceleration); + /* clean the state on exit */ + LZ4_initStream(state, sizeof (LZ4_stream_t)); + return r; +} + + +int LZ4_compress_destSize(const char* src, char* dst, int* srcSizePtr, int targetDstSize) +{ +#if (LZ4_HEAPMODE) + LZ4_stream_t* const ctx = (LZ4_stream_t*)ALLOC(sizeof(LZ4_stream_t)); /* malloc-calloc always properly aligned */ + if (ctx == NULL) return 0; +#else + LZ4_stream_t ctxBody; + LZ4_stream_t* const ctx = &ctxBody; +#endif + + int result = LZ4_compress_destSize_extState_internal(ctx, src, dst, srcSizePtr, targetDstSize, 1); + +#if (LZ4_HEAPMODE) + FREEMEM(ctx); +#endif + return result; +} + + + +/*-****************************** +* Streaming functions +********************************/ + +#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +LZ4_stream_t* LZ4_createStream(void) +{ + LZ4_stream_t* const lz4s = (LZ4_stream_t*)ALLOC(sizeof(LZ4_stream_t)); + LZ4_STATIC_ASSERT(sizeof(LZ4_stream_t) >= sizeof(LZ4_stream_t_internal)); + DEBUGLOG(4, "LZ4_createStream %p", (void*)lz4s); + if (lz4s == NULL) return NULL; + LZ4_initStream(lz4s, sizeof(*lz4s)); + return lz4s; +} +#endif + +static size_t LZ4_stream_t_alignment(void) +{ +#if LZ4_ALIGN_TEST + typedef struct { char c; LZ4_stream_t t; } t_a; + return sizeof(t_a) - sizeof(LZ4_stream_t); +#else + return 1; /* effectively disabled */ +#endif +} + +LZ4_stream_t* LZ4_initStream (void* buffer, size_t size) +{ + DEBUGLOG(5, "LZ4_initStream"); + if (buffer == NULL) { return NULL; } + if (size < sizeof(LZ4_stream_t)) { return NULL; } + if (!LZ4_isAligned(buffer, LZ4_stream_t_alignment())) return NULL; + MEM_INIT(buffer, 0, sizeof(LZ4_stream_t_internal)); + return (LZ4_stream_t*)buffer; +} + +/* resetStream is now deprecated, + * prefer initStream() which is more general */ +void LZ4_resetStream (LZ4_stream_t* LZ4_stream) +{ + DEBUGLOG(5, "LZ4_resetStream (ctx:%p)", (void*)LZ4_stream); + MEM_INIT(LZ4_stream, 0, sizeof(LZ4_stream_t_internal)); +} + +void LZ4_resetStream_fast(LZ4_stream_t* ctx) { + LZ4_prepareTable(&(ctx->internal_donotuse), 0, byU32); +} + +#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +int LZ4_freeStream (LZ4_stream_t* LZ4_stream) +{ + if (!LZ4_stream) return 0; /* support free on NULL */ + DEBUGLOG(5, "LZ4_freeStream %p", (void*)LZ4_stream); + FREEMEM(LZ4_stream); + return (0); +} +#endif + + +typedef enum { _ld_fast, _ld_slow } LoadDict_mode_e; +#define HASH_UNIT sizeof(reg_t) +static int LZ4_loadDict_internal(LZ4_stream_t* LZ4_dict, + const char* dictionary, int dictSize, + LoadDict_mode_e _ld) +{ + LZ4_stream_t_internal* const dict = &LZ4_dict->internal_donotuse; + const tableType_t tableType = byU32; + const BYTE* p = (const BYTE*)dictionary; + const BYTE* const dictEnd = p + dictSize; + U32 idx32; + + DEBUGLOG(4, "LZ4_loadDict (%i bytes from %p into %p)", dictSize, (void*)dictionary, (void*)LZ4_dict); + + /* It's necessary to reset the context, + * and not just continue it with prepareTable() + * to avoid any risk of generating overflowing matchIndex + * when compressing using this dictionary */ + LZ4_resetStream(LZ4_dict); + + /* We always increment the offset by 64 KB, since, if the dict is longer, + * we truncate it to the last 64k, and if it's shorter, we still want to + * advance by a whole window length so we can provide the guarantee that + * there are only valid offsets in the window, which allows an optimization + * in LZ4_compress_fast_continue() where it uses noDictIssue even when the + * dictionary isn't a full 64k. */ + dict->currentOffset += 64 KB; + + if (dictSize < (int)HASH_UNIT) { + return 0; + } + + if ((dictEnd - p) > 64 KB) p = dictEnd - 64 KB; + dict->dictionary = p; + dict->dictSize = (U32)(dictEnd - p); + dict->tableType = (U32)tableType; + idx32 = dict->currentOffset - dict->dictSize; + + while (p <= dictEnd-HASH_UNIT) { + U32 const h = LZ4_hashPosition(p, tableType); + /* Note: overwriting => favors positions end of dictionary */ + LZ4_putIndexOnHash(idx32, h, dict->hashTable, tableType); + p+=3; idx32+=3; + } + + if (_ld == _ld_slow) { + /* Fill hash table with additional references, to improve compression capability */ + p = dict->dictionary; + idx32 = dict->currentOffset - dict->dictSize; + while (p <= dictEnd-HASH_UNIT) { + U32 const h = LZ4_hashPosition(p, tableType); + U32 const limit = dict->currentOffset - 64 KB; + if (LZ4_getIndexOnHash(h, dict->hashTable, tableType) <= limit) { + /* Note: not overwriting => favors positions beginning of dictionary */ + LZ4_putIndexOnHash(idx32, h, dict->hashTable, tableType); + } + p++; idx32++; + } + } + + return (int)dict->dictSize; +} + +int LZ4_loadDict(LZ4_stream_t* LZ4_dict, const char* dictionary, int dictSize) +{ + return LZ4_loadDict_internal(LZ4_dict, dictionary, dictSize, _ld_fast); +} + +int LZ4_loadDictSlow(LZ4_stream_t* LZ4_dict, const char* dictionary, int dictSize) +{ + return LZ4_loadDict_internal(LZ4_dict, dictionary, dictSize, _ld_slow); +} + +void LZ4_attach_dictionary(LZ4_stream_t* workingStream, const LZ4_stream_t* dictionaryStream) +{ + const LZ4_stream_t_internal* dictCtx = (dictionaryStream == NULL) ? NULL : + &(dictionaryStream->internal_donotuse); + + DEBUGLOG(4, "LZ4_attach_dictionary (%p, %p, size %u)", + (void*)workingStream, (void*)dictionaryStream, + dictCtx != NULL ? dictCtx->dictSize : 0); + + if (dictCtx != NULL) { + /* If the current offset is zero, we will never look in the + * external dictionary context, since there is no value a table + * entry can take that indicate a miss. In that case, we need + * to bump the offset to something non-zero. + */ + if (workingStream->internal_donotuse.currentOffset == 0) { + workingStream->internal_donotuse.currentOffset = 64 KB; + } + + /* Don't actually attach an empty dictionary. + */ + if (dictCtx->dictSize == 0) { + dictCtx = NULL; + } + } + workingStream->internal_donotuse.dictCtx = dictCtx; +} + + +static void LZ4_renormDictT(LZ4_stream_t_internal* LZ4_dict, int nextSize) +{ + assert(nextSize >= 0); + if (LZ4_dict->currentOffset + (unsigned)nextSize > 0x80000000) { /* potential ptrdiff_t overflow (32-bits mode) */ + /* rescale hash table */ + U32 const delta = LZ4_dict->currentOffset - 64 KB; + const BYTE* dictEnd = LZ4_dict->dictionary + LZ4_dict->dictSize; + int i; + DEBUGLOG(4, "LZ4_renormDictT"); + for (i=0; ihashTable[i] < delta) LZ4_dict->hashTable[i]=0; + else LZ4_dict->hashTable[i] -= delta; + } + LZ4_dict->currentOffset = 64 KB; + if (LZ4_dict->dictSize > 64 KB) LZ4_dict->dictSize = 64 KB; + LZ4_dict->dictionary = dictEnd - LZ4_dict->dictSize; + } +} + + +int LZ4_compress_fast_continue (LZ4_stream_t* LZ4_stream, + const char* source, char* dest, + int inputSize, int maxOutputSize, + int acceleration) +{ + const tableType_t tableType = byU32; + LZ4_stream_t_internal* const streamPtr = &LZ4_stream->internal_donotuse; + const char* dictEnd = streamPtr->dictSize ? (const char*)streamPtr->dictionary + streamPtr->dictSize : NULL; + + DEBUGLOG(5, "LZ4_compress_fast_continue (inputSize=%i, dictSize=%u)", inputSize, streamPtr->dictSize); + + LZ4_renormDictT(streamPtr, inputSize); /* fix index overflow */ + if (acceleration < 1) acceleration = LZ4_ACCELERATION_DEFAULT; + if (acceleration > LZ4_ACCELERATION_MAX) acceleration = LZ4_ACCELERATION_MAX; + + /* invalidate tiny dictionaries */ + if ( (streamPtr->dictSize < 4) /* tiny dictionary : not enough for a hash */ + && (dictEnd != source) /* prefix mode */ + && (inputSize > 0) /* tolerance : don't lose history, in case next invocation would use prefix mode */ + && (streamPtr->dictCtx == NULL) /* usingDictCtx */ + ) { + DEBUGLOG(5, "LZ4_compress_fast_continue: dictSize(%u) at addr:%p is too small", streamPtr->dictSize, (void*)streamPtr->dictionary); + /* remove dictionary existence from history, to employ faster prefix mode */ + streamPtr->dictSize = 0; + streamPtr->dictionary = (const BYTE*)source; + dictEnd = source; + } + + /* Check overlapping input/dictionary space */ + { const char* const sourceEnd = source + inputSize; + if ((sourceEnd > (const char*)streamPtr->dictionary) && (sourceEnd < dictEnd)) { + streamPtr->dictSize = (U32)(dictEnd - sourceEnd); + if (streamPtr->dictSize > 64 KB) streamPtr->dictSize = 64 KB; + if (streamPtr->dictSize < 4) streamPtr->dictSize = 0; + streamPtr->dictionary = (const BYTE*)dictEnd - streamPtr->dictSize; + } + } + + /* prefix mode : source data follows dictionary */ + if (dictEnd == source) { + if ((streamPtr->dictSize < 64 KB) && (streamPtr->dictSize < streamPtr->currentOffset)) + return LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, withPrefix64k, dictSmall, acceleration); + else + return LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, withPrefix64k, noDictIssue, acceleration); + } + + /* external dictionary mode */ + { int result; + if (streamPtr->dictCtx) { + /* We depend here on the fact that dictCtx'es (produced by + * LZ4_loadDict) guarantee that their tables contain no references + * to offsets between dictCtx->currentOffset - 64 KB and + * dictCtx->currentOffset - dictCtx->dictSize. This makes it safe + * to use noDictIssue even when the dict isn't a full 64 KB. + */ + if (inputSize > 4 KB) { + /* For compressing large blobs, it is faster to pay the setup + * cost to copy the dictionary's tables into the active context, + * so that the compression loop is only looking into one table. + */ + LZ4_memcpy(streamPtr, streamPtr->dictCtx, sizeof(*streamPtr)); + result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingExtDict, noDictIssue, acceleration); + } else { + result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingDictCtx, noDictIssue, acceleration); + } + } else { /* small data <= 4 KB */ + if ((streamPtr->dictSize < 64 KB) && (streamPtr->dictSize < streamPtr->currentOffset)) { + result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingExtDict, dictSmall, acceleration); + } else { + result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingExtDict, noDictIssue, acceleration); + } + } + streamPtr->dictionary = (const BYTE*)source; + streamPtr->dictSize = (U32)inputSize; + return result; + } +} + + +/* Hidden debug function, to force-test external dictionary mode */ +int LZ4_compress_forceExtDict (LZ4_stream_t* LZ4_dict, const char* source, char* dest, int srcSize) +{ + LZ4_stream_t_internal* const streamPtr = &LZ4_dict->internal_donotuse; + int result; + + LZ4_renormDictT(streamPtr, srcSize); + + if ((streamPtr->dictSize < 64 KB) && (streamPtr->dictSize < streamPtr->currentOffset)) { + result = LZ4_compress_generic(streamPtr, source, dest, srcSize, NULL, 0, notLimited, byU32, usingExtDict, dictSmall, 1); + } else { + result = LZ4_compress_generic(streamPtr, source, dest, srcSize, NULL, 0, notLimited, byU32, usingExtDict, noDictIssue, 1); + } + + streamPtr->dictionary = (const BYTE*)source; + streamPtr->dictSize = (U32)srcSize; + + return result; +} + + +/*! LZ4_saveDict() : + * If previously compressed data block is not guaranteed to remain available at its memory location, + * save it into a safer place (char* safeBuffer). + * Note : no need to call LZ4_loadDict() afterwards, dictionary is immediately usable, + * one can therefore call LZ4_compress_fast_continue() right after. + * @return : saved dictionary size in bytes (necessarily <= dictSize), or 0 if error. + */ +int LZ4_saveDict (LZ4_stream_t* LZ4_dict, char* safeBuffer, int dictSize) +{ + LZ4_stream_t_internal* const dict = &LZ4_dict->internal_donotuse; + + DEBUGLOG(5, "LZ4_saveDict : dictSize=%i, safeBuffer=%p", dictSize, (void*)safeBuffer); + + if ((U32)dictSize > 64 KB) { dictSize = 64 KB; } /* useless to define a dictionary > 64 KB */ + if ((U32)dictSize > dict->dictSize) { dictSize = (int)dict->dictSize; } + + if (safeBuffer == NULL) assert(dictSize == 0); + if (dictSize > 0) { + const BYTE* const previousDictEnd = dict->dictionary + dict->dictSize; + assert(dict->dictionary); + LZ4_memmove(safeBuffer, previousDictEnd - dictSize, (size_t)dictSize); + } + + dict->dictionary = (const BYTE*)safeBuffer; + dict->dictSize = (U32)dictSize; + + return dictSize; +} + + + +/*-******************************* + * Decompression functions + ********************************/ + +typedef enum { decode_full_block = 0, partial_decode = 1 } earlyEnd_directive; + +#undef MIN +#define MIN(a,b) ( (a) < (b) ? (a) : (b) ) + + +/* variant for decompress_unsafe() + * does not know end of input + * presumes input is well formed + * note : will consume at least one byte */ +static size_t read_long_length_no_check(const BYTE** pp) +{ + size_t b, l = 0; + do { b = **pp; (*pp)++; l += b; } while (b==255); + DEBUGLOG(6, "read_long_length_no_check: +length=%zu using %zu input bytes", l, l/255 + 1) + return l; +} + +/* core decoder variant for LZ4_decompress_fast*() + * for legacy support only : these entry points are deprecated. + * - Presumes input is correctly formed (no defense vs malformed inputs) + * - Does not know input size (presume input buffer is "large enough") + * - Decompress a full block (only) + * @return : nb of bytes read from input. + * Note : this variant is not optimized for speed, just for maintenance. + * the goal is to remove support of decompress_fast*() variants by v2.0 +**/ +LZ4_FORCE_INLINE int +LZ4_decompress_unsafe_generic( + const BYTE* const istart, + BYTE* const ostart, + int decompressedSize, + + size_t prefixSize, + const BYTE* const dictStart, /* only if dict==usingExtDict */ + const size_t dictSize /* note: =0 if dictStart==NULL */ + ) +{ + const BYTE* ip = istart; + BYTE* op = (BYTE*)ostart; + BYTE* const oend = ostart + decompressedSize; + const BYTE* const prefixStart = ostart - prefixSize; + + DEBUGLOG(5, "LZ4_decompress_unsafe_generic"); + if (dictStart == NULL) assert(dictSize == 0); + + while (1) { + /* start new sequence */ + unsigned token = *ip++; + + /* literals */ + { size_t ll = token >> ML_BITS; + if (ll==15) { + /* long literal length */ + ll += read_long_length_no_check(&ip); + } + if ((size_t)(oend-op) < ll) return -1; /* output buffer overflow */ + LZ4_memmove(op, ip, ll); /* support in-place decompression */ + op += ll; + ip += ll; + if ((size_t)(oend-op) < MFLIMIT) { + if (op==oend) break; /* end of block */ + DEBUGLOG(5, "invalid: literals end at distance %zi from end of block", oend-op); + /* incorrect end of block : + * last match must start at least MFLIMIT==12 bytes before end of output block */ + return -1; + } } + + /* match */ + { size_t ml = token & 15; + size_t const offset = LZ4_readLE16(ip); + ip+=2; + + if (ml==15) { + /* long literal length */ + ml += read_long_length_no_check(&ip); + } + ml += MINMATCH; + + if ((size_t)(oend-op) < ml) return -1; /* output buffer overflow */ + + { const BYTE* match = op - offset; + + /* out of range */ + if (offset > (size_t)(op - prefixStart) + dictSize) { + DEBUGLOG(6, "offset out of range"); + return -1; + } + + /* check special case : extDict */ + if (offset > (size_t)(op - prefixStart)) { + /* extDict scenario */ + const BYTE* const dictEnd = dictStart + dictSize; + const BYTE* extMatch = dictEnd - (offset - (size_t)(op-prefixStart)); + size_t const extml = (size_t)(dictEnd - extMatch); + if (extml > ml) { + /* match entirely within extDict */ + LZ4_memmove(op, extMatch, ml); + op += ml; + ml = 0; + } else { + /* match split between extDict & prefix */ + LZ4_memmove(op, extMatch, extml); + op += extml; + ml -= extml; + } + match = prefixStart; + } + + /* match copy - slow variant, supporting overlap copy */ + { size_t u; + for (u=0; u= ipmax before start of loop. Returns initial_error if so. + * @error (output) - error code. Must be set to 0 before call. +**/ +typedef size_t Rvl_t; +static const Rvl_t rvl_error = (Rvl_t)(-1); +LZ4_FORCE_INLINE Rvl_t +read_variable_length(const BYTE** ip, const BYTE* ilimit, + int initial_check) +{ + Rvl_t s, length = 0; + assert(ip != NULL); + assert(*ip != NULL); + assert(ilimit != NULL); + if (initial_check && unlikely((*ip) >= ilimit)) { /* read limit reached */ + return rvl_error; + } + s = **ip; + (*ip)++; + length += s; + if (unlikely((*ip) > ilimit)) { /* read limit reached */ + return rvl_error; + } + /* accumulator overflow detection (32-bit mode only) */ + if ((sizeof(length) < 8) && unlikely(length > ((Rvl_t)(-1)/2)) ) { + return rvl_error; + } + if (likely(s != 255)) return length; + do { + s = **ip; + (*ip)++; + length += s; + if (unlikely((*ip) > ilimit)) { /* read limit reached */ + return rvl_error; + } + /* accumulator overflow detection (32-bit mode only) */ + if ((sizeof(length) < 8) && unlikely(length > ((Rvl_t)(-1)/2)) ) { + return rvl_error; + } + } while (s == 255); + + return length; +} + +/*! LZ4_decompress_generic() : + * This generic decompression function covers all use cases. + * It shall be instantiated several times, using different sets of directives. + * Note that it is important for performance that this function really get inlined, + * in order to remove useless branches during compilation optimization. + */ +LZ4_FORCE_INLINE int +LZ4_decompress_generic( + const char* const src, + char* const dst, + int srcSize, + int outputSize, /* If endOnInput==endOnInputSize, this value is `dstCapacity` */ + + earlyEnd_directive partialDecoding, /* full, partial */ + dict_directive dict, /* noDict, withPrefix64k, usingExtDict */ + const BYTE* const lowPrefix, /* always <= dst, == dst when no prefix */ + const BYTE* const dictStart, /* only if dict==usingExtDict */ + const size_t dictSize /* note : = 0 if noDict */ + ) +{ + if ((src == NULL) || (outputSize < 0)) { return -1; } + + { const BYTE* ip = (const BYTE*) src; + const BYTE* const iend = ip + srcSize; + + BYTE* op = (BYTE*) dst; + BYTE* const oend = op + outputSize; + BYTE* cpy; + + const BYTE* const dictEnd = (dictStart == NULL) ? NULL : dictStart + dictSize; + + const int checkOffset = (dictSize < (int)(64 KB)); + + + /* Set up the "end" pointers for the shortcut. */ + const BYTE* const shortiend = iend - 14 /*maxLL*/ - 2 /*offset*/; + const BYTE* const shortoend = oend - 14 /*maxLL*/ - 18 /*maxML*/; + + const BYTE* match; + size_t offset; + unsigned token; + size_t length; + + + DEBUGLOG(5, "LZ4_decompress_generic (srcSize:%i, dstSize:%i)", srcSize, outputSize); + + /* Special cases */ + assert(lowPrefix <= op); + if (unlikely(outputSize==0)) { + /* Empty output buffer */ + if (partialDecoding) return 0; + return ((srcSize==1) && (*ip==0)) ? 0 : -1; + } + if (unlikely(srcSize==0)) { return -1; } + + /* LZ4_FAST_DEC_LOOP: + * designed for modern OoO performance cpus, + * where copying reliably 32-bytes is preferable to an unpredictable branch. + * note : fast loop may show a regression for some client arm chips. */ +#if LZ4_FAST_DEC_LOOP + if ((oend - op) < FASTLOOP_SAFE_DISTANCE) { + DEBUGLOG(6, "move to safe decode loop"); + goto safe_decode; + } + + /* Fast loop : decode sequences as long as output < oend-FASTLOOP_SAFE_DISTANCE */ + DEBUGLOG(6, "using fast decode loop"); + while (1) { + /* Main fastloop assertion: We can always wildcopy FASTLOOP_SAFE_DISTANCE */ + assert(oend - op >= FASTLOOP_SAFE_DISTANCE); + assert(ip < iend); + token = *ip++; + length = token >> ML_BITS; /* literal length */ + DEBUGLOG(7, "blockPos%6u: litLength token = %u", (unsigned)(op-(BYTE*)dst), (unsigned)length); + + /* decode literal length */ + if (length == RUN_MASK) { + size_t const addl = read_variable_length(&ip, iend-RUN_MASK, 1); + if (addl == rvl_error) { + DEBUGLOG(6, "error reading long literal length"); + goto _output_error; + } + length += addl; + if (unlikely((uptrval)(op)+length<(uptrval)(op))) { goto _output_error; } /* overflow detection */ + if (unlikely((uptrval)(ip)+length<(uptrval)(ip))) { goto _output_error; } /* overflow detection */ + + /* copy literals */ + LZ4_STATIC_ASSERT(MFLIMIT >= WILDCOPYLENGTH); + if ((op+length>oend-32) || (ip+length>iend-32)) { goto safe_literal_copy; } + LZ4_wildCopy32(op, ip, op+length); + ip += length; op += length; + } else if (ip <= iend-(16 + 1/*max lit + offset + nextToken*/)) { + /* We don't need to check oend, since we check it once for each loop below */ + DEBUGLOG(7, "copy %u bytes in a 16-bytes stripe", (unsigned)length); + /* Literals can only be <= 14, but hope compilers optimize better when copy by a register size */ + LZ4_memcpy(op, ip, 16); + ip += length; op += length; + } else { + goto safe_literal_copy; + } + + /* get offset */ + offset = LZ4_readLE16(ip); ip+=2; + DEBUGLOG(6, "blockPos%6u: offset = %u", (unsigned)(op-(BYTE*)dst), (unsigned)offset); + match = op - offset; + assert(match <= op); /* overflow check */ + + /* get matchlength */ + length = token & ML_MASK; + DEBUGLOG(7, " match length token = %u (len==%u)", (unsigned)length, (unsigned)length+MINMATCH); + + if (length == ML_MASK) { + size_t const addl = read_variable_length(&ip, iend - LASTLITERALS + 1, 0); + if (addl == rvl_error) { + DEBUGLOG(5, "error reading long match length"); + goto _output_error; + } + length += addl; + length += MINMATCH; + DEBUGLOG(7, " long match length == %u", (unsigned)length); + if (unlikely((uptrval)(op)+length<(uptrval)op)) { goto _output_error; } /* overflow detection */ + if (op + length >= oend - FASTLOOP_SAFE_DISTANCE) { + goto safe_match_copy; + } + } else { + length += MINMATCH; + if (op + length >= oend - FASTLOOP_SAFE_DISTANCE) { + DEBUGLOG(7, "moving to safe_match_copy (ml==%u)", (unsigned)length); + goto safe_match_copy; + } + + /* Fastpath check: skip LZ4_wildCopy32 when true */ + if ((dict == withPrefix64k) || (match >= lowPrefix)) { + if (offset >= 8) { + assert(match >= lowPrefix); + assert(match <= op); + assert(op + 18 <= oend); + + LZ4_memcpy(op, match, 8); + LZ4_memcpy(op+8, match+8, 8); + LZ4_memcpy(op+16, match+16, 2); + op += length; + continue; + } } } + + if ( checkOffset && (unlikely(match + dictSize < lowPrefix)) ) { + DEBUGLOG(5, "Error : pos=%zi, offset=%zi => outside buffers", op-lowPrefix, op-match); + goto _output_error; + } + /* match starting within external dictionary */ + if ((dict==usingExtDict) && (match < lowPrefix)) { + assert(dictEnd != NULL); + if (unlikely(op+length > oend-LASTLITERALS)) { + if (partialDecoding) { + DEBUGLOG(7, "partialDecoding: dictionary match, close to dstEnd"); + length = MIN(length, (size_t)(oend-op)); + } else { + DEBUGLOG(6, "end-of-block condition violated") + goto _output_error; + } } + + if (length <= (size_t)(lowPrefix-match)) { + /* match fits entirely within external dictionary : just copy */ + LZ4_memmove(op, dictEnd - (lowPrefix-match), length); + op += length; + } else { + /* match stretches into both external dictionary and current block */ + size_t const copySize = (size_t)(lowPrefix - match); + size_t const restSize = length - copySize; + LZ4_memcpy(op, dictEnd - copySize, copySize); + op += copySize; + if (restSize > (size_t)(op - lowPrefix)) { /* overlap copy */ + BYTE* const endOfMatch = op + restSize; + const BYTE* copyFrom = lowPrefix; + while (op < endOfMatch) { *op++ = *copyFrom++; } + } else { + LZ4_memcpy(op, lowPrefix, restSize); + op += restSize; + } } + continue; + } + + /* copy match within block */ + cpy = op + length; + + assert((op <= oend) && (oend-op >= 32)); + if (unlikely(offset<16)) { + LZ4_memcpy_using_offset(op, match, cpy, offset); + } else { + LZ4_wildCopy32(op, match, cpy); + } + + op = cpy; /* wildcopy correction */ + } + safe_decode: +#endif + + /* Main Loop : decode remaining sequences where output < FASTLOOP_SAFE_DISTANCE */ + DEBUGLOG(6, "using safe decode loop"); + while (1) { + assert(ip < iend); + token = *ip++; + length = token >> ML_BITS; /* literal length */ + DEBUGLOG(7, "blockPos%6u: litLength token = %u", (unsigned)(op-(BYTE*)dst), (unsigned)length); + + /* A two-stage shortcut for the most common case: + * 1) If the literal length is 0..14, and there is enough space, + * enter the shortcut and copy 16 bytes on behalf of the literals + * (in the fast mode, only 8 bytes can be safely copied this way). + * 2) Further if the match length is 4..18, copy 18 bytes in a similar + * manner; but we ensure that there's enough space in the output for + * those 18 bytes earlier, upon entering the shortcut (in other words, + * there is a combined check for both stages). + */ + if ( (length != RUN_MASK) + /* strictly "less than" on input, to re-enter the loop with at least one byte */ + && likely((ip < shortiend) & (op <= shortoend)) ) { + /* Copy the literals */ + LZ4_memcpy(op, ip, 16); + op += length; ip += length; + + /* The second stage: prepare for match copying, decode full info. + * If it doesn't work out, the info won't be wasted. */ + length = token & ML_MASK; /* match length */ + DEBUGLOG(7, "blockPos%6u: matchLength token = %u (len=%u)", (unsigned)(op-(BYTE*)dst), (unsigned)length, (unsigned)length + 4); + offset = LZ4_readLE16(ip); ip += 2; + match = op - offset; + assert(match <= op); /* check overflow */ + + /* Do not deal with overlapping matches. */ + if ( (length != ML_MASK) + && (offset >= 8) + && (dict==withPrefix64k || match >= lowPrefix) ) { + /* Copy the match. */ + LZ4_memcpy(op + 0, match + 0, 8); + LZ4_memcpy(op + 8, match + 8, 8); + LZ4_memcpy(op +16, match +16, 2); + op += length + MINMATCH; + /* Both stages worked, load the next token. */ + continue; + } + + /* The second stage didn't work out, but the info is ready. + * Propel it right to the point of match copying. */ + goto _copy_match; + } + + /* decode literal length */ + if (length == RUN_MASK) { + size_t const addl = read_variable_length(&ip, iend-RUN_MASK, 1); + if (addl == rvl_error) { goto _output_error; } + length += addl; + if (unlikely((uptrval)(op)+length<(uptrval)(op))) { goto _output_error; } /* overflow detection */ + if (unlikely((uptrval)(ip)+length<(uptrval)(ip))) { goto _output_error; } /* overflow detection */ + } + +#if LZ4_FAST_DEC_LOOP + safe_literal_copy: +#endif + /* copy literals */ + cpy = op+length; + + LZ4_STATIC_ASSERT(MFLIMIT >= WILDCOPYLENGTH); + if ((cpy>oend-MFLIMIT) || (ip+length>iend-(2+1+LASTLITERALS))) { + /* We've either hit the input parsing restriction or the output parsing restriction. + * In the normal scenario, decoding a full block, it must be the last sequence, + * otherwise it's an error (invalid input or dimensions). + * In partialDecoding scenario, it's necessary to ensure there is no buffer overflow. + */ + if (partialDecoding) { + /* Since we are partial decoding we may be in this block because of the output parsing + * restriction, which is not valid since the output buffer is allowed to be undersized. + */ + DEBUGLOG(7, "partialDecoding: copying literals, close to input or output end") + DEBUGLOG(7, "partialDecoding: literal length = %u", (unsigned)length); + DEBUGLOG(7, "partialDecoding: remaining space in dstBuffer : %i", (int)(oend - op)); + DEBUGLOG(7, "partialDecoding: remaining space in srcBuffer : %i", (int)(iend - ip)); + /* Finishing in the middle of a literals segment, + * due to lack of input. + */ + if (ip+length > iend) { + length = (size_t)(iend-ip); + cpy = op + length; + } + /* Finishing in the middle of a literals segment, + * due to lack of output space. + */ + if (cpy > oend) { + cpy = oend; + assert(op<=oend); + length = (size_t)(oend-op); + } + } else { + /* We must be on the last sequence (or invalid) because of the parsing limitations + * so check that we exactly consume the input and don't overrun the output buffer. + */ + if ((ip+length != iend) || (cpy > oend)) { + DEBUGLOG(5, "should have been last run of literals") + DEBUGLOG(5, "ip(%p) + length(%i) = %p != iend (%p)", (void*)ip, (int)length, (void*)(ip+length), (void*)iend); + DEBUGLOG(5, "or cpy(%p) > (oend-MFLIMIT)(%p)", (void*)cpy, (void*)(oend-MFLIMIT)); + DEBUGLOG(5, "after writing %u bytes / %i bytes available", (unsigned)(op-(BYTE*)dst), outputSize); + goto _output_error; + } + } + LZ4_memmove(op, ip, length); /* supports overlapping memory regions, for in-place decompression scenarios */ + ip += length; + op += length; + /* Necessarily EOF when !partialDecoding. + * When partialDecoding, it is EOF if we've either + * filled the output buffer or + * can't proceed with reading an offset for following match. + */ + if (!partialDecoding || (cpy == oend) || (ip >= (iend-2))) { + break; + } + } else { + LZ4_wildCopy8(op, ip, cpy); /* can overwrite up to 8 bytes beyond cpy */ + ip += length; op = cpy; + } + + /* get offset */ + offset = LZ4_readLE16(ip); ip+=2; + match = op - offset; + + /* get matchlength */ + length = token & ML_MASK; + DEBUGLOG(7, "blockPos%6u: matchLength token = %u", (unsigned)(op-(BYTE*)dst), (unsigned)length); + + _copy_match: + if (length == ML_MASK) { + size_t const addl = read_variable_length(&ip, iend - LASTLITERALS + 1, 0); + if (addl == rvl_error) { goto _output_error; } + length += addl; + if (unlikely((uptrval)(op)+length<(uptrval)op)) goto _output_error; /* overflow detection */ + } + length += MINMATCH; + +#if LZ4_FAST_DEC_LOOP + safe_match_copy: +#endif + if ((checkOffset) && (unlikely(match + dictSize < lowPrefix))) goto _output_error; /* Error : offset outside buffers */ + /* match starting within external dictionary */ + if ((dict==usingExtDict) && (match < lowPrefix)) { + assert(dictEnd != NULL); + if (unlikely(op+length > oend-LASTLITERALS)) { + if (partialDecoding) length = MIN(length, (size_t)(oend-op)); + else goto _output_error; /* doesn't respect parsing restriction */ + } + + if (length <= (size_t)(lowPrefix-match)) { + /* match fits entirely within external dictionary : just copy */ + LZ4_memmove(op, dictEnd - (lowPrefix-match), length); + op += length; + } else { + /* match stretches into both external dictionary and current block */ + size_t const copySize = (size_t)(lowPrefix - match); + size_t const restSize = length - copySize; + LZ4_memcpy(op, dictEnd - copySize, copySize); + op += copySize; + if (restSize > (size_t)(op - lowPrefix)) { /* overlap copy */ + BYTE* const endOfMatch = op + restSize; + const BYTE* copyFrom = lowPrefix; + while (op < endOfMatch) *op++ = *copyFrom++; + } else { + LZ4_memcpy(op, lowPrefix, restSize); + op += restSize; + } } + continue; + } + assert(match >= lowPrefix); + + /* copy match within block */ + cpy = op + length; + + /* partialDecoding : may end anywhere within the block */ + assert(op<=oend); + if (partialDecoding && (cpy > oend-MATCH_SAFEGUARD_DISTANCE)) { + size_t const mlen = MIN(length, (size_t)(oend-op)); + const BYTE* const matchEnd = match + mlen; + BYTE* const copyEnd = op + mlen; + if (matchEnd > op) { /* overlap copy */ + while (op < copyEnd) { *op++ = *match++; } + } else { + LZ4_memcpy(op, match, mlen); + } + op = copyEnd; + if (op == oend) { break; } + continue; + } + + if (unlikely(offset<8)) { + LZ4_write32(op, 0); /* silence msan warning when offset==0 */ + op[0] = match[0]; + op[1] = match[1]; + op[2] = match[2]; + op[3] = match[3]; + match += inc32table[offset]; + LZ4_memcpy(op+4, match, 4); + match -= dec64table[offset]; + } else { + LZ4_memcpy(op, match, 8); + match += 8; + } + op += 8; + + if (unlikely(cpy > oend-MATCH_SAFEGUARD_DISTANCE)) { + BYTE* const oCopyLimit = oend - (WILDCOPYLENGTH-1); + if (cpy > oend-LASTLITERALS) { goto _output_error; } /* Error : last LASTLITERALS bytes must be literals (uncompressed) */ + if (op < oCopyLimit) { + LZ4_wildCopy8(op, match, oCopyLimit); + match += oCopyLimit - op; + op = oCopyLimit; + } + while (op < cpy) { *op++ = *match++; } + } else { + LZ4_memcpy(op, match, 8); + if (length > 16) { LZ4_wildCopy8(op+8, match+8, cpy); } + } + op = cpy; /* wildcopy correction */ + } + + /* end of decoding */ + DEBUGLOG(5, "decoded %i bytes", (int) (((char*)op)-dst)); + return (int) (((char*)op)-dst); /* Nb of output bytes decoded */ + + /* Overflow error detected */ + _output_error: + return (int) (-(((const char*)ip)-src))-1; + } +} + + +/*===== Instantiate the API decoding functions. =====*/ + +LZ4_FORCE_O2 +int LZ4_decompress_safe(const char* source, char* dest, int compressedSize, int maxDecompressedSize) +{ + return LZ4_decompress_generic(source, dest, compressedSize, maxDecompressedSize, + decode_full_block, noDict, + (BYTE*)dest, NULL, 0); +} + +LZ4_FORCE_O2 +int LZ4_decompress_safe_partial(const char* src, char* dst, int compressedSize, int targetOutputSize, int dstCapacity) +{ + dstCapacity = MIN(targetOutputSize, dstCapacity); + return LZ4_decompress_generic(src, dst, compressedSize, dstCapacity, + partial_decode, + noDict, (BYTE*)dst, NULL, 0); +} + +LZ4_FORCE_O2 +int LZ4_decompress_fast(const char* source, char* dest, int originalSize) +{ + DEBUGLOG(5, "LZ4_decompress_fast"); + return LZ4_decompress_unsafe_generic( + (const BYTE*)source, (BYTE*)dest, originalSize, + 0, NULL, 0); +} + +/*===== Instantiate a few more decoding cases, used more than once. =====*/ + +LZ4_FORCE_O2 /* Exported, an obsolete API function. */ +int LZ4_decompress_safe_withPrefix64k(const char* source, char* dest, int compressedSize, int maxOutputSize) +{ + return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, + decode_full_block, withPrefix64k, + (BYTE*)dest - 64 KB, NULL, 0); +} + +LZ4_FORCE_O2 +static int LZ4_decompress_safe_partial_withPrefix64k(const char* source, char* dest, int compressedSize, int targetOutputSize, int dstCapacity) +{ + dstCapacity = MIN(targetOutputSize, dstCapacity); + return LZ4_decompress_generic(source, dest, compressedSize, dstCapacity, + partial_decode, withPrefix64k, + (BYTE*)dest - 64 KB, NULL, 0); +} + +/* Another obsolete API function, paired with the previous one. */ +int LZ4_decompress_fast_withPrefix64k(const char* source, char* dest, int originalSize) +{ + return LZ4_decompress_unsafe_generic( + (const BYTE*)source, (BYTE*)dest, originalSize, + 64 KB, NULL, 0); +} + +LZ4_FORCE_O2 +static int LZ4_decompress_safe_withSmallPrefix(const char* source, char* dest, int compressedSize, int maxOutputSize, + size_t prefixSize) +{ + return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, + decode_full_block, noDict, + (BYTE*)dest-prefixSize, NULL, 0); +} + +LZ4_FORCE_O2 +static int LZ4_decompress_safe_partial_withSmallPrefix(const char* source, char* dest, int compressedSize, int targetOutputSize, int dstCapacity, + size_t prefixSize) +{ + dstCapacity = MIN(targetOutputSize, dstCapacity); + return LZ4_decompress_generic(source, dest, compressedSize, dstCapacity, + partial_decode, noDict, + (BYTE*)dest-prefixSize, NULL, 0); +} + +LZ4_FORCE_O2 +int LZ4_decompress_safe_forceExtDict(const char* source, char* dest, + int compressedSize, int maxOutputSize, + const void* dictStart, size_t dictSize) +{ + DEBUGLOG(5, "LZ4_decompress_safe_forceExtDict"); + return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, + decode_full_block, usingExtDict, + (BYTE*)dest, (const BYTE*)dictStart, dictSize); +} + +LZ4_FORCE_O2 +int LZ4_decompress_safe_partial_forceExtDict(const char* source, char* dest, + int compressedSize, int targetOutputSize, int dstCapacity, + const void* dictStart, size_t dictSize) +{ + dstCapacity = MIN(targetOutputSize, dstCapacity); + return LZ4_decompress_generic(source, dest, compressedSize, dstCapacity, + partial_decode, usingExtDict, + (BYTE*)dest, (const BYTE*)dictStart, dictSize); +} + +LZ4_FORCE_O2 +static int LZ4_decompress_fast_extDict(const char* source, char* dest, int originalSize, + const void* dictStart, size_t dictSize) +{ + return LZ4_decompress_unsafe_generic( + (const BYTE*)source, (BYTE*)dest, originalSize, + 0, (const BYTE*)dictStart, dictSize); +} + +/* The "double dictionary" mode, for use with e.g. ring buffers: the first part + * of the dictionary is passed as prefix, and the second via dictStart + dictSize. + * These routines are used only once, in LZ4_decompress_*_continue(). + */ +LZ4_FORCE_INLINE +int LZ4_decompress_safe_doubleDict(const char* source, char* dest, int compressedSize, int maxOutputSize, + size_t prefixSize, const void* dictStart, size_t dictSize) +{ + return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, + decode_full_block, usingExtDict, + (BYTE*)dest-prefixSize, (const BYTE*)dictStart, dictSize); +} + +/*===== streaming decompression functions =====*/ + +#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +LZ4_streamDecode_t* LZ4_createStreamDecode(void) +{ + LZ4_STATIC_ASSERT(sizeof(LZ4_streamDecode_t) >= sizeof(LZ4_streamDecode_t_internal)); + return (LZ4_streamDecode_t*) ALLOC_AND_ZERO(sizeof(LZ4_streamDecode_t)); +} + +int LZ4_freeStreamDecode (LZ4_streamDecode_t* LZ4_stream) +{ + if (LZ4_stream == NULL) { return 0; } /* support free on NULL */ + FREEMEM(LZ4_stream); + return 0; +} +#endif + +/*! LZ4_setStreamDecode() : + * Use this function to instruct where to find the dictionary. + * This function is not necessary if previous data is still available where it was decoded. + * Loading a size of 0 is allowed (same effect as no dictionary). + * @return : 1 if OK, 0 if error + */ +int LZ4_setStreamDecode (LZ4_streamDecode_t* LZ4_streamDecode, const char* dictionary, int dictSize) +{ + LZ4_streamDecode_t_internal* lz4sd = &LZ4_streamDecode->internal_donotuse; + lz4sd->prefixSize = (size_t)dictSize; + if (dictSize) { + assert(dictionary != NULL); + lz4sd->prefixEnd = (const BYTE*) dictionary + dictSize; + } else { + lz4sd->prefixEnd = (const BYTE*) dictionary; + } + lz4sd->externalDict = NULL; + lz4sd->extDictSize = 0; + return 1; +} + +/*! LZ4_decoderRingBufferSize() : + * when setting a ring buffer for streaming decompression (optional scenario), + * provides the minimum size of this ring buffer + * to be compatible with any source respecting maxBlockSize condition. + * Note : in a ring buffer scenario, + * blocks are presumed decompressed next to each other. + * When not enough space remains for next block (remainingSize < maxBlockSize), + * decoding resumes from beginning of ring buffer. + * @return : minimum ring buffer size, + * or 0 if there is an error (invalid maxBlockSize). + */ +int LZ4_decoderRingBufferSize(int maxBlockSize) +{ + if (maxBlockSize < 0) return 0; + if (maxBlockSize > LZ4_MAX_INPUT_SIZE) return 0; + if (maxBlockSize < 16) maxBlockSize = 16; + return LZ4_DECODER_RING_BUFFER_SIZE(maxBlockSize); +} + +/* +*_continue() : + These decoding functions allow decompression of multiple blocks in "streaming" mode. + Previously decoded blocks must still be available at the memory position where they were decoded. + If it's not possible, save the relevant part of decoded data into a safe buffer, + and indicate where it stands using LZ4_setStreamDecode() +*/ +LZ4_FORCE_O2 +int LZ4_decompress_safe_continue (LZ4_streamDecode_t* LZ4_streamDecode, const char* source, char* dest, int compressedSize, int maxOutputSize) +{ + LZ4_streamDecode_t_internal* lz4sd = &LZ4_streamDecode->internal_donotuse; + int result; + + if (lz4sd->prefixSize == 0) { + /* The first call, no dictionary yet. */ + assert(lz4sd->extDictSize == 0); + result = LZ4_decompress_safe(source, dest, compressedSize, maxOutputSize); + if (result <= 0) return result; + lz4sd->prefixSize = (size_t)result; + lz4sd->prefixEnd = (BYTE*)dest + result; + } else if (lz4sd->prefixEnd == (BYTE*)dest) { + /* They're rolling the current segment. */ + if (lz4sd->prefixSize >= 64 KB - 1) + result = LZ4_decompress_safe_withPrefix64k(source, dest, compressedSize, maxOutputSize); + else if (lz4sd->extDictSize == 0) + result = LZ4_decompress_safe_withSmallPrefix(source, dest, compressedSize, maxOutputSize, + lz4sd->prefixSize); + else + result = LZ4_decompress_safe_doubleDict(source, dest, compressedSize, maxOutputSize, + lz4sd->prefixSize, lz4sd->externalDict, lz4sd->extDictSize); + if (result <= 0) return result; + lz4sd->prefixSize += (size_t)result; + lz4sd->prefixEnd += result; + } else { + /* The buffer wraps around, or they're switching to another buffer. */ + lz4sd->extDictSize = lz4sd->prefixSize; + lz4sd->externalDict = lz4sd->prefixEnd - lz4sd->extDictSize; + result = LZ4_decompress_safe_forceExtDict(source, dest, compressedSize, maxOutputSize, + lz4sd->externalDict, lz4sd->extDictSize); + if (result <= 0) return result; + lz4sd->prefixSize = (size_t)result; + lz4sd->prefixEnd = (BYTE*)dest + result; + } + + return result; +} + +LZ4_FORCE_O2 int +LZ4_decompress_fast_continue (LZ4_streamDecode_t* LZ4_streamDecode, + const char* source, char* dest, int originalSize) +{ + LZ4_streamDecode_t_internal* const lz4sd = + (assert(LZ4_streamDecode!=NULL), &LZ4_streamDecode->internal_donotuse); + int result; + + DEBUGLOG(5, "LZ4_decompress_fast_continue (toDecodeSize=%i)", originalSize); + assert(originalSize >= 0); + + if (lz4sd->prefixSize == 0) { + DEBUGLOG(5, "first invocation : no prefix nor extDict"); + assert(lz4sd->extDictSize == 0); + result = LZ4_decompress_fast(source, dest, originalSize); + if (result <= 0) return result; + lz4sd->prefixSize = (size_t)originalSize; + lz4sd->prefixEnd = (BYTE*)dest + originalSize; + } else if (lz4sd->prefixEnd == (BYTE*)dest) { + DEBUGLOG(5, "continue using existing prefix"); + result = LZ4_decompress_unsafe_generic( + (const BYTE*)source, (BYTE*)dest, originalSize, + lz4sd->prefixSize, + lz4sd->externalDict, lz4sd->extDictSize); + if (result <= 0) return result; + lz4sd->prefixSize += (size_t)originalSize; + lz4sd->prefixEnd += originalSize; + } else { + DEBUGLOG(5, "prefix becomes extDict"); + lz4sd->extDictSize = lz4sd->prefixSize; + lz4sd->externalDict = lz4sd->prefixEnd - lz4sd->extDictSize; + result = LZ4_decompress_fast_extDict(source, dest, originalSize, + lz4sd->externalDict, lz4sd->extDictSize); + if (result <= 0) return result; + lz4sd->prefixSize = (size_t)originalSize; + lz4sd->prefixEnd = (BYTE*)dest + originalSize; + } + + return result; +} + + +/* +Advanced decoding functions : +*_usingDict() : + These decoding functions work the same as "_continue" ones, + the dictionary must be explicitly provided within parameters +*/ + +int LZ4_decompress_safe_usingDict(const char* source, char* dest, int compressedSize, int maxOutputSize, const char* dictStart, int dictSize) +{ + if (dictSize==0) + return LZ4_decompress_safe(source, dest, compressedSize, maxOutputSize); + if (dictStart+dictSize == dest) { + if (dictSize >= 64 KB - 1) { + return LZ4_decompress_safe_withPrefix64k(source, dest, compressedSize, maxOutputSize); + } + assert(dictSize >= 0); + return LZ4_decompress_safe_withSmallPrefix(source, dest, compressedSize, maxOutputSize, (size_t)dictSize); + } + assert(dictSize >= 0); + return LZ4_decompress_safe_forceExtDict(source, dest, compressedSize, maxOutputSize, dictStart, (size_t)dictSize); +} + +int LZ4_decompress_safe_partial_usingDict(const char* source, char* dest, int compressedSize, int targetOutputSize, int dstCapacity, const char* dictStart, int dictSize) +{ + if (dictSize==0) + return LZ4_decompress_safe_partial(source, dest, compressedSize, targetOutputSize, dstCapacity); + if (dictStart+dictSize == dest) { + if (dictSize >= 64 KB - 1) { + return LZ4_decompress_safe_partial_withPrefix64k(source, dest, compressedSize, targetOutputSize, dstCapacity); + } + assert(dictSize >= 0); + return LZ4_decompress_safe_partial_withSmallPrefix(source, dest, compressedSize, targetOutputSize, dstCapacity, (size_t)dictSize); + } + assert(dictSize >= 0); + return LZ4_decompress_safe_partial_forceExtDict(source, dest, compressedSize, targetOutputSize, dstCapacity, dictStart, (size_t)dictSize); +} + +int LZ4_decompress_fast_usingDict(const char* source, char* dest, int originalSize, const char* dictStart, int dictSize) +{ + if (dictSize==0 || dictStart+dictSize == dest) + return LZ4_decompress_unsafe_generic( + (const BYTE*)source, (BYTE*)dest, originalSize, + (size_t)dictSize, NULL, 0); + assert(dictSize >= 0); + return LZ4_decompress_fast_extDict(source, dest, originalSize, dictStart, (size_t)dictSize); +} + + +/*=************************************************* +* Obsolete Functions +***************************************************/ +/* obsolete compression functions */ +int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) +{ + return LZ4_compress_default(source, dest, inputSize, maxOutputSize); +} +int LZ4_compress(const char* src, char* dest, int srcSize) +{ + return LZ4_compress_default(src, dest, srcSize, LZ4_compressBound(srcSize)); +} +int LZ4_compress_limitedOutput_withState (void* state, const char* src, char* dst, int srcSize, int dstSize) +{ + return LZ4_compress_fast_extState(state, src, dst, srcSize, dstSize, 1); +} +int LZ4_compress_withState (void* state, const char* src, char* dst, int srcSize) +{ + return LZ4_compress_fast_extState(state, src, dst, srcSize, LZ4_compressBound(srcSize), 1); +} +int LZ4_compress_limitedOutput_continue (LZ4_stream_t* LZ4_stream, const char* src, char* dst, int srcSize, int dstCapacity) +{ + return LZ4_compress_fast_continue(LZ4_stream, src, dst, srcSize, dstCapacity, 1); +} +int LZ4_compress_continue (LZ4_stream_t* LZ4_stream, const char* source, char* dest, int inputSize) +{ + return LZ4_compress_fast_continue(LZ4_stream, source, dest, inputSize, LZ4_compressBound(inputSize), 1); +} + +/* +These decompression functions are deprecated and should no longer be used. +They are only provided here for compatibility with older user programs. +- LZ4_uncompress is totally equivalent to LZ4_decompress_fast +- LZ4_uncompress_unknownOutputSize is totally equivalent to LZ4_decompress_safe +*/ +int LZ4_uncompress (const char* source, char* dest, int outputSize) +{ + return LZ4_decompress_fast(source, dest, outputSize); +} +int LZ4_uncompress_unknownOutputSize (const char* source, char* dest, int isize, int maxOutputSize) +{ + return LZ4_decompress_safe(source, dest, isize, maxOutputSize); +} + +/* Obsolete Streaming functions */ + +int LZ4_sizeofStreamState(void) { return sizeof(LZ4_stream_t); } + +int LZ4_resetStreamState(void* state, char* inputBuffer) +{ + (void)inputBuffer; + LZ4_resetStream((LZ4_stream_t*)state); + return 0; +} + +#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +void* LZ4_create (char* inputBuffer) +{ + (void)inputBuffer; + return LZ4_createStream(); +} +#endif + +char* LZ4_slideInputBuffer (void* state) +{ + /* avoid const char * -> char * conversion warning */ + return (char *)(uptrval)((LZ4_stream_t*)state)->internal_donotuse.dictionary; +} + +#endif /* LZ4_COMMONDEFS_ONLY */ diff --git a/code/server/lz4.h b/code/server/lz4.h new file mode 100644 index 0000000..5b147b2 --- /dev/null +++ b/code/server/lz4.h @@ -0,0 +1,886 @@ +/* + * LZ4 - Fast LZ compression algorithm + * Header File + * Copyright (c) Yann Collet. All rights reserved. + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - LZ4 homepage : http://www.lz4.org + - LZ4 source repository : https://github.com/lz4/lz4 +*/ +#if defined (__cplusplus) +extern "C" { +#endif + +#ifndef LZ4_H_2983827168210 +#define LZ4_H_2983827168210 + +/* --- Dependency --- */ +#include /* size_t */ + + +/** + Introduction + + LZ4 is lossless compression algorithm, providing compression speed >500 MB/s per core, + scalable with multi-cores CPU. It features an extremely fast decoder, with speed in + multiple GB/s per core, typically reaching RAM speed limits on multi-core systems. + + The LZ4 compression library provides in-memory compression and decompression functions. + It gives full buffer control to user. + Compression can be done in: + - a single step (described as Simple Functions) + - a single step, reusing a context (described in Advanced Functions) + - unbounded multiple steps (described as Streaming compression) + + lz4.h generates and decodes LZ4-compressed blocks (doc/lz4_Block_format.md). + Decompressing such a compressed block requires additional metadata. + Exact metadata depends on exact decompression function. + For the typical case of LZ4_decompress_safe(), + metadata includes block's compressed size, and maximum bound of decompressed size. + Each application is free to encode and pass such metadata in whichever way it wants. + + lz4.h only handle blocks, it can not generate Frames. + + Blocks are different from Frames (doc/lz4_Frame_format.md). + Frames bundle both blocks and metadata in a specified manner. + Embedding metadata is required for compressed data to be self-contained and portable. + Frame format is delivered through a companion API, declared in lz4frame.h. + The `lz4` CLI can only manage frames. +*/ + +/*^*************************************************************** +* Export parameters +*****************************************************************/ +/* +* LZ4_DLL_EXPORT : +* Enable exporting of functions when building a Windows DLL +* LZ4LIB_VISIBILITY : +* Control library symbols visibility. +*/ +#ifndef LZ4LIB_VISIBILITY +# if defined(__GNUC__) && (__GNUC__ >= 4) +# define LZ4LIB_VISIBILITY __attribute__ ((visibility ("default"))) +# else +# define LZ4LIB_VISIBILITY +# endif +#endif +#if defined(LZ4_DLL_EXPORT) && (LZ4_DLL_EXPORT==1) +# define LZ4LIB_API __declspec(dllexport) LZ4LIB_VISIBILITY +#elif defined(LZ4_DLL_IMPORT) && (LZ4_DLL_IMPORT==1) +# define LZ4LIB_API __declspec(dllimport) LZ4LIB_VISIBILITY /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/ +#else +# define LZ4LIB_API LZ4LIB_VISIBILITY +#endif + +/*! LZ4_FREESTANDING : + * When this macro is set to 1, it enables "freestanding mode" that is + * suitable for typical freestanding environment which doesn't support + * standard C library. + * + * - LZ4_FREESTANDING is a compile-time switch. + * - It requires the following macros to be defined: + * LZ4_memcpy, LZ4_memmove, LZ4_memset. + * - It only enables LZ4/HC functions which don't use heap. + * All LZ4F_* functions are not supported. + * - See tests/freestanding.c to check its basic setup. + */ +#if defined(LZ4_FREESTANDING) && (LZ4_FREESTANDING == 1) +# define LZ4_HEAPMODE 0 +# define LZ4HC_HEAPMODE 0 +# define LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION 1 +# if !defined(LZ4_memcpy) +# error "LZ4_FREESTANDING requires macro 'LZ4_memcpy'." +# endif +# if !defined(LZ4_memset) +# error "LZ4_FREESTANDING requires macro 'LZ4_memset'." +# endif +# if !defined(LZ4_memmove) +# error "LZ4_FREESTANDING requires macro 'LZ4_memmove'." +# endif +#elif ! defined(LZ4_FREESTANDING) +# define LZ4_FREESTANDING 0 +#endif + + +/*------ Version ------*/ +#define LZ4_VERSION_MAJOR 1 /* for breaking interface changes */ +#define LZ4_VERSION_MINOR 10 /* for new (non-breaking) interface capabilities */ +#define LZ4_VERSION_RELEASE 0 /* for tweaks, bug-fixes, or development */ + +#define LZ4_VERSION_NUMBER (LZ4_VERSION_MAJOR *100*100 + LZ4_VERSION_MINOR *100 + LZ4_VERSION_RELEASE) + +#define LZ4_LIB_VERSION LZ4_VERSION_MAJOR.LZ4_VERSION_MINOR.LZ4_VERSION_RELEASE +#define LZ4_QUOTE(str) #str +#define LZ4_EXPAND_AND_QUOTE(str) LZ4_QUOTE(str) +#define LZ4_VERSION_STRING LZ4_EXPAND_AND_QUOTE(LZ4_LIB_VERSION) /* requires v1.7.3+ */ + +LZ4LIB_API int LZ4_versionNumber (void); /**< library version number; useful to check dll version; requires v1.3.0+ */ +LZ4LIB_API const char* LZ4_versionString (void); /**< library version string; useful to check dll version; requires v1.7.5+ */ + + +/*-************************************ +* Tuning memory usage +**************************************/ +/*! + * LZ4_MEMORY_USAGE : + * Can be selected at compile time, by setting LZ4_MEMORY_USAGE. + * Memory usage formula : N->2^N Bytes (examples : 10 -> 1KB; 12 -> 4KB ; 16 -> 64KB; 20 -> 1MB) + * Increasing memory usage improves compression ratio, generally at the cost of speed. + * Reduced memory usage may improve speed at the cost of ratio, thanks to better cache locality. + * Default value is 14, for 16KB, which nicely fits into most L1 caches. + */ +#ifndef LZ4_MEMORY_USAGE +# define LZ4_MEMORY_USAGE LZ4_MEMORY_USAGE_DEFAULT +#endif + +/* These are absolute limits, they should not be changed by users */ +#define LZ4_MEMORY_USAGE_MIN 10 +#define LZ4_MEMORY_USAGE_DEFAULT 14 +#define LZ4_MEMORY_USAGE_MAX 20 + +#if (LZ4_MEMORY_USAGE < LZ4_MEMORY_USAGE_MIN) +# error "LZ4_MEMORY_USAGE is too small !" +#endif + +#if (LZ4_MEMORY_USAGE > LZ4_MEMORY_USAGE_MAX) +# error "LZ4_MEMORY_USAGE is too large !" +#endif + +/*-************************************ +* Simple Functions +**************************************/ +/*! LZ4_compress_default() : + * Compresses 'srcSize' bytes from buffer 'src' + * into already allocated 'dst' buffer of size 'dstCapacity'. + * Compression is guaranteed to succeed if 'dstCapacity' >= LZ4_compressBound(srcSize). + * It also runs faster, so it's a recommended setting. + * If the function cannot compress 'src' into a more limited 'dst' budget, + * compression stops *immediately*, and the function result is zero. + * In which case, 'dst' content is undefined (invalid). + * srcSize : max supported value is LZ4_MAX_INPUT_SIZE. + * dstCapacity : size of buffer 'dst' (which must be already allocated) + * @return : the number of bytes written into buffer 'dst' (necessarily <= dstCapacity) + * or 0 if compression fails + * Note : This function is protected against buffer overflow scenarios (never writes outside 'dst' buffer, nor read outside 'source' buffer). + */ +LZ4LIB_API int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity); + +/*! LZ4_decompress_safe() : + * @compressedSize : is the exact complete size of the compressed block. + * @dstCapacity : is the size of destination buffer (which must be already allocated), + * presumed an upper bound of decompressed size. + * @return : the number of bytes decompressed into destination buffer (necessarily <= dstCapacity) + * If destination buffer is not large enough, decoding will stop and output an error code (negative value). + * If the source stream is detected malformed, the function will stop decoding and return a negative result. + * Note 1 : This function is protected against malicious data packets : + * it will never writes outside 'dst' buffer, nor read outside 'source' buffer, + * even if the compressed block is maliciously modified to order the decoder to do these actions. + * In such case, the decoder stops immediately, and considers the compressed block malformed. + * Note 2 : compressedSize and dstCapacity must be provided to the function, the compressed block does not contain them. + * The implementation is free to send / store / derive this information in whichever way is most beneficial. + * If there is a need for a different format which bundles together both compressed data and its metadata, consider looking at lz4frame.h instead. + */ +LZ4LIB_API int LZ4_decompress_safe (const char* src, char* dst, int compressedSize, int dstCapacity); + + +/*-************************************ +* Advanced Functions +**************************************/ +#define LZ4_MAX_INPUT_SIZE 0x7E000000 /* 2 113 929 216 bytes */ +#define LZ4_COMPRESSBOUND(isize) ((unsigned)(isize) > (unsigned)LZ4_MAX_INPUT_SIZE ? 0 : (isize) + ((isize)/255) + 16) + +/*! LZ4_compressBound() : + Provides the maximum size that LZ4 compression may output in a "worst case" scenario (input data not compressible) + This function is primarily useful for memory allocation purposes (destination buffer size). + Macro LZ4_COMPRESSBOUND() is also provided for compilation-time evaluation (stack memory allocation for example). + Note that LZ4_compress_default() compresses faster when dstCapacity is >= LZ4_compressBound(srcSize) + inputSize : max supported value is LZ4_MAX_INPUT_SIZE + return : maximum output size in a "worst case" scenario + or 0, if input size is incorrect (too large or negative) +*/ +LZ4LIB_API int LZ4_compressBound(int inputSize); + +/*! LZ4_compress_fast() : + Same as LZ4_compress_default(), but allows selection of "acceleration" factor. + The larger the acceleration value, the faster the algorithm, but also the lesser the compression. + It's a trade-off. It can be fine tuned, with each successive value providing roughly +~3% to speed. + An acceleration value of "1" is the same as regular LZ4_compress_default() + Values <= 0 will be replaced by LZ4_ACCELERATION_DEFAULT (currently == 1, see lz4.c). + Values > LZ4_ACCELERATION_MAX will be replaced by LZ4_ACCELERATION_MAX (currently == 65537, see lz4.c). +*/ +LZ4LIB_API int LZ4_compress_fast (const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); + + +/*! LZ4_compress_fast_extState() : + * Same as LZ4_compress_fast(), using an externally allocated memory space for its state. + * Use LZ4_sizeofState() to know how much memory must be allocated, + * and allocate it on 8-bytes boundaries (using `malloc()` typically). + * Then, provide this buffer as `void* state` to compression function. + */ +LZ4LIB_API int LZ4_sizeofState(void); +LZ4LIB_API int LZ4_compress_fast_extState (void* state, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); + +/*! LZ4_compress_destSize() : + * Reverse the logic : compresses as much data as possible from 'src' buffer + * into already allocated buffer 'dst', of size >= 'dstCapacity'. + * This function either compresses the entire 'src' content into 'dst' if it's large enough, + * or fill 'dst' buffer completely with as much data as possible from 'src'. + * note: acceleration parameter is fixed to "default". + * + * *srcSizePtr : in+out parameter. Initially contains size of input. + * Will be modified to indicate how many bytes where read from 'src' to fill 'dst'. + * New value is necessarily <= input value. + * @return : Nb bytes written into 'dst' (necessarily <= dstCapacity) + * or 0 if compression fails. + * + * Note : 'targetDstSize' must be >= 1, because it's the smallest valid lz4 payload. + * + * Note 2:from v1.8.2 to v1.9.1, this function had a bug (fixed in v1.9.2+): + * the produced compressed content could, in rare circumstances, + * require to be decompressed into a destination buffer + * larger by at least 1 byte than decompressesSize. + * If an application uses `LZ4_compress_destSize()`, + * it's highly recommended to update liblz4 to v1.9.2 or better. + * If this can't be done or ensured, + * the receiving decompression function should provide + * a dstCapacity which is > decompressedSize, by at least 1 byte. + * See https://github.com/lz4/lz4/issues/859 for details + */ +LZ4LIB_API int LZ4_compress_destSize(const char* src, char* dst, int* srcSizePtr, int targetDstSize); + +/*! LZ4_decompress_safe_partial() : + * Decompress an LZ4 compressed block, of size 'srcSize' at position 'src', + * into destination buffer 'dst' of size 'dstCapacity'. + * Up to 'targetOutputSize' bytes will be decoded. + * The function stops decoding on reaching this objective. + * This can be useful to boost performance + * whenever only the beginning of a block is required. + * + * @return : the number of bytes decoded in `dst` (necessarily <= targetOutputSize) + * If source stream is detected malformed, function returns a negative result. + * + * Note 1 : @return can be < targetOutputSize, if compressed block contains less data. + * + * Note 2 : targetOutputSize must be <= dstCapacity + * + * Note 3 : this function effectively stops decoding on reaching targetOutputSize, + * so dstCapacity is kind of redundant. + * This is because in older versions of this function, + * decoding operation would still write complete sequences. + * Therefore, there was no guarantee that it would stop writing at exactly targetOutputSize, + * it could write more bytes, though only up to dstCapacity. + * Some "margin" used to be required for this operation to work properly. + * Thankfully, this is no longer necessary. + * The function nonetheless keeps the same signature, in an effort to preserve API compatibility. + * + * Note 4 : If srcSize is the exact size of the block, + * then targetOutputSize can be any value, + * including larger than the block's decompressed size. + * The function will, at most, generate block's decompressed size. + * + * Note 5 : If srcSize is _larger_ than block's compressed size, + * then targetOutputSize **MUST** be <= block's decompressed size. + * Otherwise, *silent corruption will occur*. + */ +LZ4LIB_API int LZ4_decompress_safe_partial (const char* src, char* dst, int srcSize, int targetOutputSize, int dstCapacity); + + +/*-********************************************* +* Streaming Compression Functions +***********************************************/ +typedef union LZ4_stream_u LZ4_stream_t; /* incomplete type (defined later) */ + +/*! + Note about RC_INVOKED + + - RC_INVOKED is predefined symbol of rc.exe (the resource compiler which is part of MSVC/Visual Studio). + https://docs.microsoft.com/en-us/windows/win32/menurc/predefined-macros + + - Since rc.exe is a legacy compiler, it truncates long symbol (> 30 chars) + and reports warning "RC4011: identifier truncated". + + - To eliminate the warning, we surround long preprocessor symbol with + "#if !defined(RC_INVOKED) ... #endif" block that means + "skip this block when rc.exe is trying to read it". +*/ +#if !defined(RC_INVOKED) /* https://docs.microsoft.com/en-us/windows/win32/menurc/predefined-macros */ +#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +LZ4LIB_API LZ4_stream_t* LZ4_createStream(void); +LZ4LIB_API int LZ4_freeStream (LZ4_stream_t* streamPtr); +#endif /* !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) */ +#endif + +/*! LZ4_resetStream_fast() : v1.9.0+ + * Use this to prepare an LZ4_stream_t for a new chain of dependent blocks + * (e.g., LZ4_compress_fast_continue()). + * + * An LZ4_stream_t must be initialized once before usage. + * This is automatically done when created by LZ4_createStream(). + * However, should the LZ4_stream_t be simply declared on stack (for example), + * it's necessary to initialize it first, using LZ4_initStream(). + * + * After init, start any new stream with LZ4_resetStream_fast(). + * A same LZ4_stream_t can be re-used multiple times consecutively + * and compress multiple streams, + * provided that it starts each new stream with LZ4_resetStream_fast(). + * + * LZ4_resetStream_fast() is much faster than LZ4_initStream(), + * but is not compatible with memory regions containing garbage data. + * + * Note: it's only useful to call LZ4_resetStream_fast() + * in the context of streaming compression. + * The *extState* functions perform their own resets. + * Invoking LZ4_resetStream_fast() before is redundant, and even counterproductive. + */ +LZ4LIB_API void LZ4_resetStream_fast (LZ4_stream_t* streamPtr); + +/*! LZ4_loadDict() : + * Use this function to reference a static dictionary into LZ4_stream_t. + * The dictionary must remain available during compression. + * LZ4_loadDict() triggers a reset, so any previous data will be forgotten. + * The same dictionary will have to be loaded on decompression side for successful decoding. + * Dictionary are useful for better compression of small data (KB range). + * While LZ4 itself accepts any input as dictionary, dictionary efficiency is also a topic. + * When in doubt, employ the Zstandard's Dictionary Builder. + * Loading a size of 0 is allowed, and is the same as reset. + * @return : loaded dictionary size, in bytes (note: only the last 64 KB are loaded) + */ +LZ4LIB_API int LZ4_loadDict (LZ4_stream_t* streamPtr, const char* dictionary, int dictSize); + +/*! LZ4_loadDictSlow() : v1.10.0+ + * Same as LZ4_loadDict(), + * but uses a bit more cpu to reference the dictionary content more thoroughly. + * This is expected to slightly improve compression ratio. + * The extra-cpu cost is likely worth it if the dictionary is re-used across multiple sessions. + * @return : loaded dictionary size, in bytes (note: only the last 64 KB are loaded) + */ +LZ4LIB_API int LZ4_loadDictSlow(LZ4_stream_t* streamPtr, const char* dictionary, int dictSize); + +/*! LZ4_attach_dictionary() : stable since v1.10.0 + * + * This allows efficient re-use of a static dictionary multiple times. + * + * Rather than re-loading the dictionary buffer into a working context before + * each compression, or copying a pre-loaded dictionary's LZ4_stream_t into a + * working LZ4_stream_t, this function introduces a no-copy setup mechanism, + * in which the working stream references @dictionaryStream in-place. + * + * Several assumptions are made about the state of @dictionaryStream. + * Currently, only states which have been prepared by LZ4_loadDict() or + * LZ4_loadDictSlow() should be expected to work. + * + * Alternatively, the provided @dictionaryStream may be NULL, + * in which case any existing dictionary stream is unset. + * + * If a dictionary is provided, it replaces any pre-existing stream history. + * The dictionary contents are the only history that can be referenced and + * logically immediately precede the data compressed in the first subsequent + * compression call. + * + * The dictionary will only remain attached to the working stream through the + * first compression call, at the end of which it is cleared. + * @dictionaryStream stream (and source buffer) must remain in-place / accessible / unchanged + * through the completion of the compression session. + * + * Note: there is no equivalent LZ4_attach_*() method on the decompression side + * because there is no initialization cost, hence no need to share the cost across multiple sessions. + * To decompress LZ4 blocks using dictionary, attached or not, + * just employ the regular LZ4_setStreamDecode() for streaming, + * or the stateless LZ4_decompress_safe_usingDict() for one-shot decompression. + */ +LZ4LIB_API void +LZ4_attach_dictionary(LZ4_stream_t* workingStream, + const LZ4_stream_t* dictionaryStream); + +/*! LZ4_compress_fast_continue() : + * Compress 'src' content using data from previously compressed blocks, for better compression ratio. + * 'dst' buffer must be already allocated. + * If dstCapacity >= LZ4_compressBound(srcSize), compression is guaranteed to succeed, and runs faster. + * + * @return : size of compressed block + * or 0 if there is an error (typically, cannot fit into 'dst'). + * + * Note 1 : Each invocation to LZ4_compress_fast_continue() generates a new block. + * Each block has precise boundaries. + * Each block must be decompressed separately, calling LZ4_decompress_*() with relevant metadata. + * It's not possible to append blocks together and expect a single invocation of LZ4_decompress_*() to decompress them together. + * + * Note 2 : The previous 64KB of source data is __assumed__ to remain present, unmodified, at same address in memory ! + * + * Note 3 : When input is structured as a double-buffer, each buffer can have any size, including < 64 KB. + * Make sure that buffers are separated, by at least one byte. + * This construction ensures that each block only depends on previous block. + * + * Note 4 : If input buffer is a ring-buffer, it can have any size, including < 64 KB. + * + * Note 5 : After an error, the stream status is undefined (invalid), it can only be reset or freed. + */ +LZ4LIB_API int LZ4_compress_fast_continue (LZ4_stream_t* streamPtr, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); + +/*! LZ4_saveDict() : + * If last 64KB data cannot be guaranteed to remain available at its current memory location, + * save it into a safer place (char* safeBuffer). + * This is schematically equivalent to a memcpy() followed by LZ4_loadDict(), + * but is much faster, because LZ4_saveDict() doesn't need to rebuild tables. + * @return : saved dictionary size in bytes (necessarily <= maxDictSize), or 0 if error. + */ +LZ4LIB_API int LZ4_saveDict (LZ4_stream_t* streamPtr, char* safeBuffer, int maxDictSize); + + +/*-********************************************** +* Streaming Decompression Functions +* Bufferless synchronous API +************************************************/ +typedef union LZ4_streamDecode_u LZ4_streamDecode_t; /* tracking context */ + +/*! LZ4_createStreamDecode() and LZ4_freeStreamDecode() : + * creation / destruction of streaming decompression tracking context. + * A tracking context can be re-used multiple times. + */ +#if !defined(RC_INVOKED) /* https://docs.microsoft.com/en-us/windows/win32/menurc/predefined-macros */ +#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) +LZ4LIB_API LZ4_streamDecode_t* LZ4_createStreamDecode(void); +LZ4LIB_API int LZ4_freeStreamDecode (LZ4_streamDecode_t* LZ4_stream); +#endif /* !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) */ +#endif + +/*! LZ4_setStreamDecode() : + * An LZ4_streamDecode_t context can be allocated once and re-used multiple times. + * Use this function to start decompression of a new stream of blocks. + * A dictionary can optionally be set. Use NULL or size 0 for a reset order. + * Dictionary is presumed stable : it must remain accessible and unmodified during next decompression. + * @return : 1 if OK, 0 if error + */ +LZ4LIB_API int LZ4_setStreamDecode (LZ4_streamDecode_t* LZ4_streamDecode, const char* dictionary, int dictSize); + +/*! LZ4_decoderRingBufferSize() : v1.8.2+ + * Note : in a ring buffer scenario (optional), + * blocks are presumed decompressed next to each other + * up to the moment there is not enough remaining space for next block (remainingSize < maxBlockSize), + * at which stage it resumes from beginning of ring buffer. + * When setting such a ring buffer for streaming decompression, + * provides the minimum size of this ring buffer + * to be compatible with any source respecting maxBlockSize condition. + * @return : minimum ring buffer size, + * or 0 if there is an error (invalid maxBlockSize). + */ +LZ4LIB_API int LZ4_decoderRingBufferSize(int maxBlockSize); +#define LZ4_DECODER_RING_BUFFER_SIZE(maxBlockSize) (65536 + 14 + (maxBlockSize)) /* for static allocation; maxBlockSize presumed valid */ + +/*! LZ4_decompress_safe_continue() : + * This decoding function allows decompression of consecutive blocks in "streaming" mode. + * The difference with the usual independent blocks is that + * new blocks are allowed to find references into former blocks. + * A block is an unsplittable entity, and must be presented entirely to the decompression function. + * LZ4_decompress_safe_continue() only accepts one block at a time. + * It's modeled after `LZ4_decompress_safe()` and behaves similarly. + * + * @LZ4_streamDecode : decompression state, tracking the position in memory of past data + * @compressedSize : exact complete size of one compressed block. + * @dstCapacity : size of destination buffer (which must be already allocated), + * must be an upper bound of decompressed size. + * @return : number of bytes decompressed into destination buffer (necessarily <= dstCapacity) + * If destination buffer is not large enough, decoding will stop and output an error code (negative value). + * If the source stream is detected malformed, the function will stop decoding and return a negative result. + * + * The last 64KB of previously decoded data *must* remain available and unmodified + * at the memory position where they were previously decoded. + * If less than 64KB of data has been decoded, all the data must be present. + * + * Special : if decompression side sets a ring buffer, it must respect one of the following conditions : + * - Decompression buffer size is _at least_ LZ4_decoderRingBufferSize(maxBlockSize). + * maxBlockSize is the maximum size of any single block. It can have any value > 16 bytes. + * In which case, encoding and decoding buffers do not need to be synchronized. + * Actually, data can be produced by any source compliant with LZ4 format specification, and respecting maxBlockSize. + * - Synchronized mode : + * Decompression buffer size is _exactly_ the same as compression buffer size, + * and follows exactly same update rule (block boundaries at same positions), + * and decoding function is provided with exact decompressed size of each block (exception for last block of the stream), + * _then_ decoding & encoding ring buffer can have any size, including small ones ( < 64 KB). + * - Decompression buffer is larger than encoding buffer, by a minimum of maxBlockSize more bytes. + * In which case, encoding and decoding buffers do not need to be synchronized, + * and encoding ring buffer can have any size, including small ones ( < 64 KB). + * + * Whenever these conditions are not possible, + * save the last 64KB of decoded data into a safe buffer where it can't be modified during decompression, + * then indicate where this data is saved using LZ4_setStreamDecode(), before decompressing next block. +*/ +LZ4LIB_API int +LZ4_decompress_safe_continue (LZ4_streamDecode_t* LZ4_streamDecode, + const char* src, char* dst, + int srcSize, int dstCapacity); + + +/*! LZ4_decompress_safe_usingDict() : + * Works the same as + * a combination of LZ4_setStreamDecode() followed by LZ4_decompress_safe_continue() + * However, it's stateless: it doesn't need any LZ4_streamDecode_t state. + * Dictionary is presumed stable : it must remain accessible and unmodified during decompression. + * Performance tip : Decompression speed can be substantially increased + * when dst == dictStart + dictSize. + */ +LZ4LIB_API int +LZ4_decompress_safe_usingDict(const char* src, char* dst, + int srcSize, int dstCapacity, + const char* dictStart, int dictSize); + +/*! LZ4_decompress_safe_partial_usingDict() : + * Behaves the same as LZ4_decompress_safe_partial() + * with the added ability to specify a memory segment for past data. + * Performance tip : Decompression speed can be substantially increased + * when dst == dictStart + dictSize. + */ +LZ4LIB_API int +LZ4_decompress_safe_partial_usingDict(const char* src, char* dst, + int compressedSize, + int targetOutputSize, int maxOutputSize, + const char* dictStart, int dictSize); + +#endif /* LZ4_H_2983827168210 */ + + +/*^************************************* + * !!!!!! STATIC LINKING ONLY !!!!!! + ***************************************/ + +/*-**************************************************************************** + * Experimental section + * + * Symbols declared in this section must be considered unstable. Their + * signatures or semantics may change, or they may be removed altogether in the + * future. They are therefore only safe to depend on when the caller is + * statically linked against the library. + * + * To protect against unsafe usage, not only are the declarations guarded, + * the definitions are hidden by default + * when building LZ4 as a shared/dynamic library. + * + * In order to access these declarations, + * define LZ4_STATIC_LINKING_ONLY in your application + * before including LZ4's headers. + * + * In order to make their implementations accessible dynamically, you must + * define LZ4_PUBLISH_STATIC_FUNCTIONS when building the LZ4 library. + ******************************************************************************/ + +#ifdef LZ4_STATIC_LINKING_ONLY + +#ifndef LZ4_STATIC_3504398509 +#define LZ4_STATIC_3504398509 + +#ifdef LZ4_PUBLISH_STATIC_FUNCTIONS +# define LZ4LIB_STATIC_API LZ4LIB_API +#else +# define LZ4LIB_STATIC_API +#endif + + +/*! LZ4_compress_fast_extState_fastReset() : + * A variant of LZ4_compress_fast_extState(). + * + * Using this variant avoids an expensive initialization step. + * It is only safe to call if the state buffer is known to be correctly initialized already + * (see above comment on LZ4_resetStream_fast() for a definition of "correctly initialized"). + * From a high level, the difference is that + * this function initializes the provided state with a call to something like LZ4_resetStream_fast() + * while LZ4_compress_fast_extState() starts with a call to LZ4_resetStream(). + */ +LZ4LIB_STATIC_API int LZ4_compress_fast_extState_fastReset (void* state, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); + +/*! LZ4_compress_destSize_extState() : introduced in v1.10.0 + * Same as LZ4_compress_destSize(), but using an externally allocated state. + * Also: exposes @acceleration + */ +int LZ4_compress_destSize_extState(void* state, const char* src, char* dst, int* srcSizePtr, int targetDstSize, int acceleration); + +/*! In-place compression and decompression + * + * It's possible to have input and output sharing the same buffer, + * for highly constrained memory environments. + * In both cases, it requires input to lay at the end of the buffer, + * and decompression to start at beginning of the buffer. + * Buffer size must feature some margin, hence be larger than final size. + * + * |<------------------------buffer--------------------------------->| + * |<-----------compressed data--------->| + * |<-----------decompressed size------------------>| + * |<----margin---->| + * + * This technique is more useful for decompression, + * since decompressed size is typically larger, + * and margin is short. + * + * In-place decompression will work inside any buffer + * which size is >= LZ4_DECOMPRESS_INPLACE_BUFFER_SIZE(decompressedSize). + * This presumes that decompressedSize > compressedSize. + * Otherwise, it means compression actually expanded data, + * and it would be more efficient to store such data with a flag indicating it's not compressed. + * This can happen when data is not compressible (already compressed, or encrypted). + * + * For in-place compression, margin is larger, as it must be able to cope with both + * history preservation, requiring input data to remain unmodified up to LZ4_DISTANCE_MAX, + * and data expansion, which can happen when input is not compressible. + * As a consequence, buffer size requirements are much higher, + * and memory savings offered by in-place compression are more limited. + * + * There are ways to limit this cost for compression : + * - Reduce history size, by modifying LZ4_DISTANCE_MAX. + * Note that it is a compile-time constant, so all compressions will apply this limit. + * Lower values will reduce compression ratio, except when input_size < LZ4_DISTANCE_MAX, + * so it's a reasonable trick when inputs are known to be small. + * - Require the compressor to deliver a "maximum compressed size". + * This is the `dstCapacity` parameter in `LZ4_compress*()`. + * When this size is < LZ4_COMPRESSBOUND(inputSize), then compression can fail, + * in which case, the return code will be 0 (zero). + * The caller must be ready for these cases to happen, + * and typically design a backup scheme to send data uncompressed. + * The combination of both techniques can significantly reduce + * the amount of margin required for in-place compression. + * + * In-place compression can work in any buffer + * which size is >= (maxCompressedSize) + * with maxCompressedSize == LZ4_COMPRESSBOUND(srcSize) for guaranteed compression success. + * LZ4_COMPRESS_INPLACE_BUFFER_SIZE() depends on both maxCompressedSize and LZ4_DISTANCE_MAX, + * so it's possible to reduce memory requirements by playing with them. + */ + +#define LZ4_DECOMPRESS_INPLACE_MARGIN(compressedSize) (((compressedSize) >> 8) + 32) +#define LZ4_DECOMPRESS_INPLACE_BUFFER_SIZE(decompressedSize) ((decompressedSize) + LZ4_DECOMPRESS_INPLACE_MARGIN(decompressedSize)) /**< note: presumes that compressedSize < decompressedSize. note2: margin is overestimated a bit, since it could use compressedSize instead */ + +#ifndef LZ4_DISTANCE_MAX /* history window size; can be user-defined at compile time */ +# define LZ4_DISTANCE_MAX 65535 /* set to maximum value by default */ +#endif + +#define LZ4_COMPRESS_INPLACE_MARGIN (LZ4_DISTANCE_MAX + 32) /* LZ4_DISTANCE_MAX can be safely replaced by srcSize when it's smaller */ +#define LZ4_COMPRESS_INPLACE_BUFFER_SIZE(maxCompressedSize) ((maxCompressedSize) + LZ4_COMPRESS_INPLACE_MARGIN) /**< maxCompressedSize is generally LZ4_COMPRESSBOUND(inputSize), but can be set to any lower value, with the risk that compression can fail (return code 0(zero)) */ + +#endif /* LZ4_STATIC_3504398509 */ +#endif /* LZ4_STATIC_LINKING_ONLY */ + + + +#ifndef LZ4_H_98237428734687 +#define LZ4_H_98237428734687 + +/*-************************************************************ + * Private Definitions + ************************************************************** + * Do not use these definitions directly. + * They are only exposed to allow static allocation of `LZ4_stream_t` and `LZ4_streamDecode_t`. + * Accessing members will expose user code to API and/or ABI break in future versions of the library. + **************************************************************/ +#define LZ4_HASHLOG (LZ4_MEMORY_USAGE-2) +#define LZ4_HASHTABLESIZE (1 << LZ4_MEMORY_USAGE) +#define LZ4_HASH_SIZE_U32 (1 << LZ4_HASHLOG) /* required as macro for static allocation */ + +#if defined(__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# include + typedef int8_t LZ4_i8; + typedef unsigned char LZ4_byte; + typedef uint16_t LZ4_u16; + typedef uint32_t LZ4_u32; +#else + typedef signed char LZ4_i8; + typedef unsigned char LZ4_byte; + typedef unsigned short LZ4_u16; + typedef unsigned int LZ4_u32; +#endif + +/*! LZ4_stream_t : + * Never ever use below internal definitions directly ! + * These definitions are not API/ABI safe, and may change in future versions. + * If you need static allocation, declare or allocate an LZ4_stream_t object. +**/ + +typedef struct LZ4_stream_t_internal LZ4_stream_t_internal; +struct LZ4_stream_t_internal { + LZ4_u32 hashTable[LZ4_HASH_SIZE_U32]; + const LZ4_byte* dictionary; + const LZ4_stream_t_internal* dictCtx; + LZ4_u32 currentOffset; + LZ4_u32 tableType; + LZ4_u32 dictSize; + /* Implicit padding to ensure structure is aligned */ +}; + +#define LZ4_STREAM_MINSIZE ((1UL << (LZ4_MEMORY_USAGE)) + 32) /* static size, for inter-version compatibility */ +union LZ4_stream_u { + char minStateSize[LZ4_STREAM_MINSIZE]; + LZ4_stream_t_internal internal_donotuse; +}; /* previously typedef'd to LZ4_stream_t */ + + +/*! LZ4_initStream() : v1.9.0+ + * An LZ4_stream_t structure must be initialized at least once. + * This is automatically done when invoking LZ4_createStream(), + * but it's not when the structure is simply declared on stack (for example). + * + * Use LZ4_initStream() to properly initialize a newly declared LZ4_stream_t. + * It can also initialize any arbitrary buffer of sufficient size, + * and will @return a pointer of proper type upon initialization. + * + * Note : initialization fails if size and alignment conditions are not respected. + * In which case, the function will @return NULL. + * Note2: An LZ4_stream_t structure guarantees correct alignment and size. + * Note3: Before v1.9.0, use LZ4_resetStream() instead +**/ +LZ4LIB_API LZ4_stream_t* LZ4_initStream (void* stateBuffer, size_t size); + + +/*! LZ4_streamDecode_t : + * Never ever use below internal definitions directly ! + * These definitions are not API/ABI safe, and may change in future versions. + * If you need static allocation, declare or allocate an LZ4_streamDecode_t object. +**/ +typedef struct { + const LZ4_byte* externalDict; + const LZ4_byte* prefixEnd; + size_t extDictSize; + size_t prefixSize; +} LZ4_streamDecode_t_internal; + +#define LZ4_STREAMDECODE_MINSIZE 32 +union LZ4_streamDecode_u { + char minStateSize[LZ4_STREAMDECODE_MINSIZE]; + LZ4_streamDecode_t_internal internal_donotuse; +} ; /* previously typedef'd to LZ4_streamDecode_t */ + + + +/*-************************************ +* Obsolete Functions +**************************************/ + +/*! Deprecation warnings + * + * Deprecated functions make the compiler generate a warning when invoked. + * This is meant to invite users to update their source code. + * Should deprecation warnings be a problem, it is generally possible to disable them, + * typically with -Wno-deprecated-declarations for gcc + * or _CRT_SECURE_NO_WARNINGS in Visual. + * + * Another method is to define LZ4_DISABLE_DEPRECATE_WARNINGS + * before including the header file. + */ +#ifdef LZ4_DISABLE_DEPRECATE_WARNINGS +# define LZ4_DEPRECATED(message) /* disable deprecation warnings */ +#else +# if defined (__cplusplus) && (__cplusplus >= 201402) /* C++14 or greater */ +# define LZ4_DEPRECATED(message) [[deprecated(message)]] +# elif defined(_MSC_VER) +# define LZ4_DEPRECATED(message) __declspec(deprecated(message)) +# elif defined(__clang__) || (defined(__GNUC__) && (__GNUC__ * 10 + __GNUC_MINOR__ >= 45)) +# define LZ4_DEPRECATED(message) __attribute__((deprecated(message))) +# elif defined(__GNUC__) && (__GNUC__ * 10 + __GNUC_MINOR__ >= 31) +# define LZ4_DEPRECATED(message) __attribute__((deprecated)) +# else +# pragma message("WARNING: LZ4_DEPRECATED needs custom implementation for this compiler") +# define LZ4_DEPRECATED(message) /* disabled */ +# endif +#endif /* LZ4_DISABLE_DEPRECATE_WARNINGS */ + +/*! Obsolete compression functions (since v1.7.3) */ +LZ4_DEPRECATED("use LZ4_compress_default() instead") LZ4LIB_API int LZ4_compress (const char* src, char* dest, int srcSize); +LZ4_DEPRECATED("use LZ4_compress_default() instead") LZ4LIB_API int LZ4_compress_limitedOutput (const char* src, char* dest, int srcSize, int maxOutputSize); +LZ4_DEPRECATED("use LZ4_compress_fast_extState() instead") LZ4LIB_API int LZ4_compress_withState (void* state, const char* source, char* dest, int inputSize); +LZ4_DEPRECATED("use LZ4_compress_fast_extState() instead") LZ4LIB_API int LZ4_compress_limitedOutput_withState (void* state, const char* source, char* dest, int inputSize, int maxOutputSize); +LZ4_DEPRECATED("use LZ4_compress_fast_continue() instead") LZ4LIB_API int LZ4_compress_continue (LZ4_stream_t* LZ4_streamPtr, const char* source, char* dest, int inputSize); +LZ4_DEPRECATED("use LZ4_compress_fast_continue() instead") LZ4LIB_API int LZ4_compress_limitedOutput_continue (LZ4_stream_t* LZ4_streamPtr, const char* source, char* dest, int inputSize, int maxOutputSize); + +/*! Obsolete decompression functions (since v1.8.0) */ +LZ4_DEPRECATED("use LZ4_decompress_fast() instead") LZ4LIB_API int LZ4_uncompress (const char* source, char* dest, int outputSize); +LZ4_DEPRECATED("use LZ4_decompress_safe() instead") LZ4LIB_API int LZ4_uncompress_unknownOutputSize (const char* source, char* dest, int isize, int maxOutputSize); + +/* Obsolete streaming functions (since v1.7.0) + * degraded functionality; do not use! + * + * In order to perform streaming compression, these functions depended on data + * that is no longer tracked in the state. They have been preserved as well as + * possible: using them will still produce a correct output. However, they don't + * actually retain any history between compression calls. The compression ratio + * achieved will therefore be no better than compressing each chunk + * independently. + */ +LZ4_DEPRECATED("Use LZ4_createStream() instead") LZ4LIB_API void* LZ4_create (char* inputBuffer); +LZ4_DEPRECATED("Use LZ4_createStream() instead") LZ4LIB_API int LZ4_sizeofStreamState(void); +LZ4_DEPRECATED("Use LZ4_resetStream() instead") LZ4LIB_API int LZ4_resetStreamState(void* state, char* inputBuffer); +LZ4_DEPRECATED("Use LZ4_saveDict() instead") LZ4LIB_API char* LZ4_slideInputBuffer (void* state); + +/*! Obsolete streaming decoding functions (since v1.7.0) */ +LZ4_DEPRECATED("use LZ4_decompress_safe_usingDict() instead") LZ4LIB_API int LZ4_decompress_safe_withPrefix64k (const char* src, char* dst, int compressedSize, int maxDstSize); +LZ4_DEPRECATED("use LZ4_decompress_fast_usingDict() instead") LZ4LIB_API int LZ4_decompress_fast_withPrefix64k (const char* src, char* dst, int originalSize); + +/*! Obsolete LZ4_decompress_fast variants (since v1.9.0) : + * These functions used to be faster than LZ4_decompress_safe(), + * but this is no longer the case. They are now slower. + * This is because LZ4_decompress_fast() doesn't know the input size, + * and therefore must progress more cautiously into the input buffer to not read beyond the end of block. + * On top of that `LZ4_decompress_fast()` is not protected vs malformed or malicious inputs, making it a security liability. + * As a consequence, LZ4_decompress_fast() is strongly discouraged, and deprecated. + * + * The last remaining LZ4_decompress_fast() specificity is that + * it can decompress a block without knowing its compressed size. + * Such functionality can be achieved in a more secure manner + * by employing LZ4_decompress_safe_partial(). + * + * Parameters: + * originalSize : is the uncompressed size to regenerate. + * `dst` must be already allocated, its size must be >= 'originalSize' bytes. + * @return : number of bytes read from source buffer (== compressed size). + * The function expects to finish at block's end exactly. + * If the source stream is detected malformed, the function stops decoding and returns a negative result. + * note : LZ4_decompress_fast*() requires originalSize. Thanks to this information, it never writes past the output buffer. + * However, since it doesn't know its 'src' size, it may read an unknown amount of input, past input buffer bounds. + * Also, since match offsets are not validated, match reads from 'src' may underflow too. + * These issues never happen if input (compressed) data is correct. + * But they may happen if input data is invalid (error or intentional tampering). + * As a consequence, use these functions in trusted environments with trusted data **only**. + */ +LZ4_DEPRECATED("This function is deprecated and unsafe. Consider using LZ4_decompress_safe_partial() instead") +LZ4LIB_API int LZ4_decompress_fast (const char* src, char* dst, int originalSize); +LZ4_DEPRECATED("This function is deprecated and unsafe. Consider migrating towards LZ4_decompress_safe_continue() instead. " + "Note that the contract will change (requires block's compressed size, instead of decompressed size)") +LZ4LIB_API int LZ4_decompress_fast_continue (LZ4_streamDecode_t* LZ4_streamDecode, const char* src, char* dst, int originalSize); +LZ4_DEPRECATED("This function is deprecated and unsafe. Consider using LZ4_decompress_safe_partial_usingDict() instead") +LZ4LIB_API int LZ4_decompress_fast_usingDict (const char* src, char* dst, int originalSize, const char* dictStart, int dictSize); + +/*! LZ4_resetStream() : + * An LZ4_stream_t structure must be initialized at least once. + * This is done with LZ4_initStream(), or LZ4_resetStream(). + * Consider switching to LZ4_initStream(), + * invoking LZ4_resetStream() will trigger deprecation warnings in the future. + */ +LZ4LIB_API void LZ4_resetStream (LZ4_stream_t* streamPtr); + + +#endif /* LZ4_H_98237428734687 */ + + +#if defined (__cplusplus) +} +#endif diff --git a/code/server/server.h b/code/server/server.h index c32b2dc..6d59686 100644 --- a/code/server/server.h +++ b/code/server/server.h @@ -341,6 +341,29 @@ int SV_BotGetConsoleMessage( int client, char *buf, int size ); int BotImport_DebugPolygonCreate(int color, int numPoints, vec3_t *points); void BotImport_DebugPolygonDelete(int id); +// +// sv_netdemo.c +// +void SVD_Record_f( void ); +void SVD_StopRecord_f( void ); +void SVD_RecordFrame( void ); +void SVD_ResetDeltaState( void ); +void SVD_AutoRecord( void ); +void SVD_CaptureServerCommand( const char *cmd ); +void SVD_Play_f( void ); +void SVD_StopPlay_f( void ); +void SVD_CleanupPlayback( void ); +void SVD_Stop_f( void ); +void SVD_Pause_f( void ); +void SVD_Seek_f( void ); +void SVD_SeekExact_f( void ); +qboolean SVD_PlaybackFrame( void ); +qboolean SVD_IsRecording( void ); +qboolean SVD_IsPlaying( void ); +qboolean SVD_IsPaused( void ); +qboolean SVD_IsStarting( void ); +qboolean SVD_ShouldPause( void ); + //============================================================ // // high level object sorting to reduce interaction tests diff --git a/code/server/sv_ccmds.c b/code/server/sv_ccmds.c index 62ae179..7f1370f 100644 --- a/code/server/sv_ccmds.c +++ b/code/server/sv_ccmds.c @@ -280,6 +280,9 @@ static void SV_MapRestart_f( void ) { sv.state = SS_GAME; sv.restarting = qfalse; + // reset demo delta state so next frame writes full entities + SVD_ResetDeltaState(); + // connect and begin all the clients for (i=0 ; iinteger ; i++) { client = &svs.clients[i]; @@ -736,6 +739,14 @@ void SV_AddOperatorCommands( void ) { if( com_dedicated->integer ) { Cmd_AddCommand ("say", SV_ConSay_f); } + + // server-side demo recording/playback + Cmd_AddCommand ("svdemo_record", SVD_Record_f); + Cmd_AddCommand ("svdemo_stop", SVD_Stop_f); + Cmd_AddCommand ("svdemo_play", SVD_Play_f); + Cmd_AddCommand ("svdemo_pause", SVD_Pause_f); + Cmd_AddCommand ("svdemo_seek", SVD_Seek_f); + Cmd_AddCommand ("svdemo_seekexact", SVD_SeekExact_f); } /* diff --git a/code/server/sv_client.c b/code/server/sv_client.c index c3fdc92..84c2e3b 100644 --- a/code/server/sv_client.c +++ b/code/server/sv_client.c @@ -1419,7 +1419,12 @@ static void SV_UserMove( client_t *cl, msg_t *msg, qboolean delta ) { // don't execute if this is an old cmd which is already executed // these old cmds are included when cl_packetdup > 0 if ( cmds[i].serverTime <= cl->lastUsercmd.serverTime ) { - continue; + // demo playback: still process the LAST cmd even with duplicate + // serverTime (paused = frozen time, all cmds have same time). + // Need buttons and origin from fresh usercmds. + if ( !SVD_IsPlaying() || i != cmdCount - 1 ) { + continue; + } } SV_ClientThink (cl, &cmds[ i ]); } diff --git a/code/server/sv_game.c b/code/server/sv_game.c index 202994e..b227f3c 100644 --- a/code/server/sv_game.c +++ b/code/server/sv_game.c @@ -83,13 +83,20 @@ Sends a command string to a client =============== */ void SV_GameSendServerCommand( int clientNum, const char *text ) { + // capture for demo recording: broadcasts and per-client chat/tchat + if ( clientNum == -1 ) { + SVD_CaptureServerCommand( text ); + } else if ( !strncmp( text, "chat", 4 ) || !strncmp( text, "tchat", 5 ) ) { + SVD_CaptureServerCommand( text ); + } + if ( clientNum == -1 ) { SV_SendServerCommand( NULL, "%s", text ); } else { if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) { return; } - SV_SendServerCommand( svs.clients + clientNum, "%s", text ); + SV_SendServerCommand( svs.clients + clientNum, "%s", text ); } } diff --git a/code/server/sv_init.c b/code/server/sv_init.c index b3d0c4d..c59568c 100644 --- a/code/server/sv_init.c +++ b/code/server/sv_init.c @@ -1,695 +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; - - // 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"); -} - -/* -=============== -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 ); - - // 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" ); - - 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 98c74ce..b521dd0 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -1,855 +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; - svs.time += frameMsec; - - // let everything in the world think and move - VM_Call( gvm, GAME_RUN_FRAME, svs.time ); - } - - if ( com_speeds->integer ) { - time_game = Sys_Milliseconds () - startTime; - } - - // check timeouts - 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 new file mode 100644 index 0000000..cf31610 --- /dev/null +++ b/code/server/sv_netdemo.c @@ -0,0 +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 +} + diff --git a/netdemo-full.patch b/netdemo-full.patch new file mode 100644 index 0000000..14f788a --- /dev/null +++ b/netdemo-full.patch @@ -0,0 +1,16297 @@ +diff --git a/code/cgame/cg_draw.c b/code/cgame/cg_draw.c +index e29c10f..7d7126e 100644 +--- a/code/cgame/cg_draw.c ++++ b/code/cgame/cg_draw.c +@@ -1650,6 +1650,16 @@ static void CG_DrawDisconnect( void ) { + const char *s; + int w; // bk010215 - FIXME char message[1024]; + ++ // server demo playback: detect pause from frozen snapshot time ++ if ( cg.svDemoPlayback ) { ++ if ( cg.nextSnap && cg.nextSnap->serverTime == cg.snap->serverTime ) { ++ s = "Playback Paused"; ++ w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; ++ CG_DrawBigString( 320 - w/2, 100, s, 1.0F ); ++ } ++ return; ++ } ++ + // draw the phone jack if we are completely past our buffers + cmdNum = trap_GetCurrentCmdNumber() - CMD_BACKUP + 1; + trap_GetUserCmd( cmdNum, &cmd ); +diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h +index 4dd5c9d..0c8e98a 100644 +--- a/code/cgame/cg_local.h ++++ b/code/cgame/cg_local.h +@@ -457,6 +457,10 @@ typedef struct { + int clientNum; + + qboolean demoPlayback; ++ qboolean svDemoPlayback; // server-side demo playback mode ++ qboolean svDemoFreeCamera; // free camera (client-owned movement) ++ playerState_t svDemoCameraPs; // local playerState for free camera ++ int svDemoCameraTime; // real time of last camera update + qboolean levelShot; // taking a level menu screenshot + int deferredPlayerLoading; + qboolean loading; // don't defer players at initial startup +@@ -1618,6 +1622,7 @@ qboolean trap_GetUserCmd( int cmdNumber, usercmd_t *ucmd ); + + // used for the weapon select and zoom + void trap_SetUserCmdValue( int stateValue, float sensitivityScale ); ++void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z ); + + // aids for VM testing + void testPrintInt( char *string, int i ); +diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c +index aa9ec27..97f65a2 100644 +--- a/code/cgame/cg_main.c ++++ b/code/cgame/cg_main.c +@@ -1896,6 +1896,13 @@ void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ) { + // get the gamestate from the client system + trap_GetGameState( &cgs.gameState ); + ++ // detect server-side demo playback from configstring ++ cg.svDemoPlayback = ( atoi( CG_ConfigString( CS_SVDEMO ) ) != 0 ); ++ if ( cg.svDemoPlayback ) { ++ cg.svDemoFreeCamera = qtrue; ++ // camera ps will be initialized from first snapshot in CG_PredictPlayerState ++ } ++ + // check version + s = CG_ConfigString( CS_GAME_VERSION ); + if ( strcmp( s, GAME_VERSION ) ) { +diff --git a/code/cgame/cg_predict.c b/code/cgame/cg_predict.c +index fec2cfb..ed0d72d 100644 +--- a/code/cgame/cg_predict.c ++++ b/code/cgame/cg_predict.c +@@ -432,6 +432,59 @@ void CG_PredictPlayerState( void ) { + // demo playback just copies the moves + if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) ) { + CG_InterpolatePlayerState( qfalse ); ++ // send followed player's origin for PVS during server demo follow mode ++ if ( cg.svDemoPlayback && (cg.snap->ps.pm_flags & PMF_FOLLOW) ) { ++ trap_SetClientOrigin( qtrue, ++ cg.snap->ps.origin[0], ++ cg.snap->ps.origin[1], ++ cg.snap->ps.origin[2] ); ++ cg.svDemoFreeCamera = qfalse; ++ } ++ return; ++ } ++ ++ // server-side demo free camera: run local PmoveSingle ++ if ( cg.svDemoPlayback && cg.svDemoFreeCamera ) { ++ int realTime = trap_Milliseconds(); ++ int dt = realTime - cg.svDemoCameraTime; ++ pmove_t pm; ++ ++ // lazy init: place camera at spectator spawn from first snapshot ++ if ( cg.svDemoCameraPs.speed == 0 ) { ++ cg.svDemoCameraPs = cg.snap->ps; ++ cg.svDemoCameraPs.pm_type = PM_SPECTATOR; ++ cg.svDemoCameraPs.speed = 480; ++ cg.svDemoCameraTime = realTime; ++ dt = 1; ++ } ++ ++ if ( dt < 1 ) dt = 1; ++ if ( dt > 200 ) dt = 200; ++ cg.svDemoCameraTime = realTime; ++ ++ // get latest usercmd for input ++ current = trap_GetCurrentCmdNumber(); ++ trap_GetUserCmd( current, &latestCmd ); ++ ++ // set up pmove ++ memset( &pm, 0, sizeof(pm) ); ++ pm.ps = &cg.svDemoCameraPs; ++ pm.cmd = latestCmd; ++ pm.cmd.serverTime = cg.svDemoCameraPs.commandTime + dt; ++ pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; ++ pm.trace = CG_Trace; ++ pm.pointcontents = CG_PointContents; ++ ++ Pmove( &pm ); ++ ++ // use as the predicted state for rendering ++ cg.predictedPlayerState = cg.svDemoCameraPs; ++ ++ // send origin for PVS ++ trap_SetClientOrigin( qtrue, ++ cg.svDemoCameraPs.origin[0], ++ cg.svDemoCameraPs.origin[1], ++ cg.svDemoCameraPs.origin[2] ); + return; + } + +diff --git a/code/cgame/cg_public.h b/code/cgame/cg_public.h +index 8514143..d667ce2 100644 +--- a/code/cgame/cg_public.h ++++ b/code/cgame/cg_public.h +@@ -164,6 +164,7 @@ typedef enum { + CG_R_INPVS, + // 1.32 + CG_FS_SEEK, ++ CG_SETCLIENTORIGIN, // set client-owned origin for demo spectator PVS + + /* + CG_LOADCAMERA, +diff --git a/code/cgame/cg_snapshot.c b/code/cgame/cg_snapshot.c +index add49f0..f10c9ab 100644 +--- a/code/cgame/cg_snapshot.c ++++ b/code/cgame/cg_snapshot.c +@@ -1,403 +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 ); +- } +- } +- +-} +- +- +-/* +-=================== +-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; +- +- 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; +- } +- +- // 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 +- if ( cg.nextSnap->serverTime < cg.snap->serverTime ) { +- 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/cgame/cg_syscalls.c b/code/cgame/cg_syscalls.c +index a7bf659..2446b4b 100644 +--- a/code/cgame/cg_syscalls.c ++++ b/code/cgame/cg_syscalls.c +@@ -333,6 +333,10 @@ void trap_SetUserCmdValue( int stateValue, float sensitivityScale ) { + syscall( CG_SETUSERCMDVALUE, stateValue, PASSFLOAT(sensitivityScale) ); + } + ++void trap_SetClientOrigin( qboolean hasOrigin, float x, float y, float z ) { ++ syscall( CG_SETCLIENTORIGIN, hasOrigin, PASSFLOAT(x), PASSFLOAT(y), PASSFLOAT(z) ); ++} ++ + void testPrintInt( char *string, int i ) { + syscall( CG_TESTPRINTINT, string, i ); + } +diff --git a/code/client/cl_cgame.c b/code/client/cl_cgame.c +index 0b259d3..e399cef 100644 +--- a/code/client/cl_cgame.c ++++ b/code/client/cl_cgame.c +@@ -457,6 +457,14 @@ int CL_CgameSystemCalls( int *args ) { + return 0; + case CG_FS_SEEK: + return FS_Seek( args[1], args[2], args[3] ); ++ case CG_SETCLIENTORIGIN: ++ cl.cgameHasOrigin = args[1]; ++ if ( args[1] ) { ++ cl.cgameOrigin[0] = VMF(2); ++ cl.cgameOrigin[1] = VMF(3); ++ cl.cgameOrigin[2] = VMF(4); ++ } ++ return 0; + case CG_SENDCONSOLECOMMAND: + Cbuf_AddText( VMA(1) ); + return 0; +diff --git a/code/client/cl_input.c b/code/client/cl_input.c +index f450ee7..c3b4778 100644 +--- a/code/client/cl_input.c ++++ b/code/client/cl_input.c +@@ -516,6 +516,12 @@ void CL_FinishMove( usercmd_t *cmd ) { + for (i=0 ; i<3 ; i++) { + cmd->angles[i] = ANGLE2SHORT(cl.viewangles[i]); + } ++ ++ // client-owned origin for demo spectator PVS ++ cmd->hasOrigin = cl.cgameHasOrigin; ++ if ( cl.cgameHasOrigin ) { ++ VectorCopy( cl.cgameOrigin, cmd->origin ); ++ } + } + + +diff --git a/code/client/cl_parse.c b/code/client/cl_parse.c +index 9de3588..8626b3f 100644 +--- a/code/client/cl_parse.c ++++ b/code/client/cl_parse.c +@@ -288,17 +288,30 @@ void CL_ParseSnapshot( msg_t *msg ) { + cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse; + } + +- // copy to the current good spot +- cl.snap = newSnap; +- cl.snap.ping = 999; +- // calculate ping time +- for ( i = 0 ; i < PACKET_BACKUP ; i++ ) { +- packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK; +- if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) { +- cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime; +- break; ++ { ++ int oldSnapFlags = cl.snap.snapFlags; ++ ++ // copy to the current good spot ++ cl.snap = newSnap; ++ cl.snap.ping = 999; ++ // calculate ping time ++ for ( i = 0 ; i < PACKET_BACKUP ; i++ ) { ++ packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK; ++ if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) { ++ cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime; ++ break; ++ } ++ } ++ // if server count changed (map_restart or demo unpause), force-reset ++ // the time delta so the client doesn't slowly drift back to sync ++ if ( ( oldSnapFlags ^ newSnap.snapFlags ) & SNAPFLAG_SERVERCOUNT ) { ++ cl.serverTimeDelta = cl.snap.serverTime - cls.realtime; ++ cl.oldServerTime = cl.snap.serverTime; ++ cl.serverTime = cl.snap.serverTime; ++ cl.oldFrameServerTime = cl.snap.serverTime; // prevent backwards time error on seek + } + } ++ + // save the frame off in the backup array for later delta comparisons + cl.snapshots[cl.snap.messageNum & PACKET_MASK] = cl.snap; + +diff --git a/code/client/client.h b/code/client/client.h +index 09e02a8..fe36445 100644 +--- a/code/client/client.h ++++ b/code/client/client.h +@@ -106,6 +106,8 @@ typedef struct { + // cgame communicates a few values to the client system + int cgameUserCmdValue; // current weapon to add to usercmd_t + float cgameSensitivity; ++ qboolean cgameHasOrigin; // client-owned origin for demo spectator PVS ++ vec3_t cgameOrigin; // the origin to send + + // cmds[cmdNumber] is the predicted command, [cmdNumber-1] is the last + // properly generated command +diff --git a/code/game/bg_public.h b/code/game/bg_public.h +index 0dd59db..5dc1cf2 100644 +--- a/code/game/bg_public.h ++++ b/code/game/bg_public.h +@@ -79,6 +79,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + #define CS_FLAGSTATUS 23 // string indicating flag status in CTF + #define CS_SHADERSTATE 24 + #define CS_BOTINFO 25 ++#define CS_SVDEMO 26 // "1" during server-side demo playback + + #define CS_ITEMS 27 // string of 0's and 1's that tell which items are present + +diff --git a/code/game/g_active.c b/code/game/g_active.c +index 853f5fb..f78dac8 100644 +--- a/code/game/g_active.c ++++ b/code/game/g_active.c +@@ -1,1191 +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; +- +- 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_client.c b/code/game/g_client.c +index 5a584d4..6d43d7c 100644 +--- a/code/game/g_client.c ++++ b/code/game/g_client.c +@@ -1012,6 +1012,12 @@ void ClientBegin( int clientNum ) { + client->pers.enterTime = level.time; + client->pers.teamState.state = TEAM_BEGIN; + ++ // demo playback: force all clients to spectator ++ if ( g_svDemoPlaying.integer ) { ++ client->sess.sessionTeam = TEAM_SPECTATOR; ++ ClientUserinfoChanged( ent->client - level.clients ); ++ } ++ + // save eflags around this, because changing teams will + // cause this to happen with a valid entity, and we + // want to make sure the teleport bit is set right +diff --git a/code/game/g_cmds.c b/code/game/g_cmds.c +index e72c80e..dc87e58 100644 +--- a/code/game/g_cmds.c ++++ b/code/game/g_cmds.c +@@ -661,6 +661,20 @@ void Cmd_Team_f( gentity_t *ent ) { + int oldTeam; + char s[MAX_TOKEN_CHARS]; + ++ // demo playback: only allow "team spectator" (to exit follow mode) ++ if ( g_svDemoPlaying.integer ) { ++ char s2[MAX_TOKEN_CHARS]; ++ if ( trap_Argc() == 2 ) { ++ trap_Argv( 1, s2, sizeof(s2) ); ++ if ( !Q_stricmp( s2, "spectator" ) || !Q_stricmp( s2, "s" ) ) { ++ StopFollowing( ent ); ++ return; ++ } ++ } ++ trap_SendServerCommand( ent-g_entities, "print \"Only spectating allowed during demo playback.\n\"" ); ++ return; ++ } ++ + if ( trap_Argc() != 2 ) { + oldTeam = ent->client->sess.sessionTeam; + switch ( oldTeam ) { +diff --git a/code/game/g_local.h b/code/game/g_local.h +index 1a55955..057722c 100644 +--- a/code/game/g_local.h ++++ b/code/game/g_local.h +@@ -641,6 +641,7 @@ void ClientCommand( int clientNum ); + // g_active.c + // + void ClientThink( int clientNum ); ++void ClientThink_real( gentity_t *ent ); + void ClientEndFrame( gentity_t *ent ); + void G_RunClient( gentity_t *ent ); + +@@ -715,6 +716,7 @@ extern gentity_t g_entities[MAX_GENTITIES]; + #define FOFS(x) ((int)&(((gentity_t *)0)->x)) + + extern vmCvar_t g_gametype; ++extern vmCvar_t g_svDemoPlaying; + extern vmCvar_t g_dedicated; + extern vmCvar_t g_cheats; + extern vmCvar_t g_maxclients; // allow this many total, including spectators +diff --git a/code/game/g_main.c b/code/game/g_main.c +index 9f60272..b6303f1 100644 +--- a/code/game/g_main.c ++++ b/code/game/g_main.c +@@ -1,1832 +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_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_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(); +- +- 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; +- 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(); +- +- // +- // 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 e65c3e1..7de9374 100644 +--- a/code/game/g_session.c ++++ b/code/game/g_session.c +@@ -1,193 +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; +- +- 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/game/q_shared.h b/code/game/q_shared.h +index 99e5a67..32a772f 100644 +--- a/code/game/q_shared.h ++++ b/code/game/q_shared.h +@@ -1085,6 +1085,7 @@ typedef enum { + #define SNAPFLAG_RATE_DELAYED 1 + #define SNAPFLAG_NOT_ACTIVE 2 // snapshot used during connection and for zombies + #define SNAPFLAG_SERVERCOUNT 4 // toggled every map_restart so transitions can be detected ++#define SNAPFLAG_RESET_ENTITIES 16 // snap all entities to current position, no interpolation + + // + // per-level limits +@@ -1252,8 +1253,11 @@ typedef struct usercmd_s { + int serverTime; + int angles[3]; + int buttons; +- byte weapon; // weapon ++ byte weapon; // weapon + signed char forwardmove, rightmove, upmove; ++ // client-owned origin for demo spectator PVS (optional) ++ qboolean hasOrigin; ++ float origin[3]; + } usercmd_t; + + //=================================================================== +diff --git a/code/qcommon/msg.c b/code/qcommon/msg.c +index 97110f8..e4f6696 100644 +--- a/code/qcommon/msg.c ++++ b/code/qcommon/msg.c +@@ -702,18 +702,27 @@ void MSG_WriteDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t * + from->weapon == to->weapon) { + MSG_WriteBits( msg, 0, 1 ); // no change + oldsize += 7; +- return; ++ } else { ++ key ^= to->serverTime; ++ MSG_WriteBits( msg, 1, 1 ); ++ MSG_WriteDeltaKey( msg, key, from->angles[0], to->angles[0], 16 ); ++ MSG_WriteDeltaKey( msg, key, from->angles[1], to->angles[1], 16 ); ++ MSG_WriteDeltaKey( msg, key, from->angles[2], to->angles[2], 16 ); ++ MSG_WriteDeltaKey( msg, key, from->forwardmove, to->forwardmove, 8 ); ++ MSG_WriteDeltaKey( msg, key, from->rightmove, to->rightmove, 8 ); ++ MSG_WriteDeltaKey( msg, key, from->upmove, to->upmove, 8 ); ++ MSG_WriteDeltaKey( msg, key, from->buttons, to->buttons, 16 ); ++ MSG_WriteDeltaKey( msg, key, from->weapon, to->weapon, 8 ); ++ } ++ // optional client-owned origin (always written, independent of field changes) ++ if ( to->hasOrigin ) { ++ MSG_WriteBits( msg, 1, 1 ); ++ MSG_WriteFloat( msg, to->origin[0] ); ++ MSG_WriteFloat( msg, to->origin[1] ); ++ MSG_WriteFloat( msg, to->origin[2] ); ++ } else { ++ MSG_WriteBits( msg, 0, 1 ); + } +- key ^= to->serverTime; +- MSG_WriteBits( msg, 1, 1 ); +- MSG_WriteDeltaKey( msg, key, from->angles[0], to->angles[0], 16 ); +- MSG_WriteDeltaKey( msg, key, from->angles[1], to->angles[1], 16 ); +- MSG_WriteDeltaKey( msg, key, from->angles[2], to->angles[2], 16 ); +- MSG_WriteDeltaKey( msg, key, from->forwardmove, to->forwardmove, 8 ); +- MSG_WriteDeltaKey( msg, key, from->rightmove, to->rightmove, 8 ); +- MSG_WriteDeltaKey( msg, key, from->upmove, to->upmove, 8 ); +- MSG_WriteDeltaKey( msg, key, from->buttons, to->buttons, 16 ); +- MSG_WriteDeltaKey( msg, key, from->weapon, to->weapon, 8 ); + } + + +@@ -748,6 +757,15 @@ void MSG_ReadDeltaUsercmdKey( msg_t *msg, int key, usercmd_t *from, usercmd_t *t + to->buttons = from->buttons; + to->weapon = from->weapon; + } ++ // optional client-owned origin ++ if ( MSG_ReadBits( msg, 1 ) ) { ++ to->hasOrigin = qtrue; ++ to->origin[0] = MSG_ReadFloat( msg ); ++ to->origin[1] = MSG_ReadFloat( msg ); ++ to->origin[2] = MSG_ReadFloat( msg ); ++ } else { ++ to->hasOrigin = qfalse; ++ } + } + + /* +diff --git a/code/quake3.vcxproj b/code/quake3.vcxproj +index 340ba18..25d6f18 100644 +--- a/code/quake3.vcxproj ++++ b/code/quake3.vcxproj +@@ -995,6 +995,8 @@ + MaxSpeed + MaxSpeed + ++ ++ + + Disabled + true +diff --git a/code/server/lz4.c b/code/server/lz4.c +new file mode 100644 +index 0000000..1af8ea4 +--- /dev/null ++++ b/code/server/lz4.c +@@ -0,0 +1,2825 @@ ++/* ++ LZ4 - Fast LZ compression algorithm ++ Copyright (c) Yann Collet. All rights reserved. ++ ++ BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) ++ ++ Redistribution and use in source and binary forms, with or without ++ modification, are permitted provided that the following conditions are ++ met: ++ ++ * Redistributions of source code must retain the above copyright ++ notice, this list of conditions and the following disclaimer. ++ * Redistributions in binary form must reproduce the above ++ copyright notice, this list of conditions and the following disclaimer ++ in the documentation and/or other materials provided with the ++ distribution. ++ ++ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ ++ You can contact the author at : ++ - LZ4 homepage : http://www.lz4.org ++ - LZ4 source repository : https://github.com/lz4/lz4 ++*/ ++ ++/*-************************************ ++* Tuning parameters ++**************************************/ ++/* ++ * LZ4_HEAPMODE : ++ * Select how stateless compression functions like `LZ4_compress_default()` ++ * allocate memory for their hash table, ++ * in memory stack (0:default, fastest), or in memory heap (1:requires malloc()). ++ */ ++#ifndef LZ4_HEAPMODE ++# define LZ4_HEAPMODE 0 ++#endif ++ ++/* ++ * LZ4_ACCELERATION_DEFAULT : ++ * Select "acceleration" for LZ4_compress_fast() when parameter value <= 0 ++ */ ++#define LZ4_ACCELERATION_DEFAULT 1 ++/* ++ * LZ4_ACCELERATION_MAX : ++ * Any "acceleration" value higher than this threshold ++ * get treated as LZ4_ACCELERATION_MAX instead (fix #876) ++ */ ++#define LZ4_ACCELERATION_MAX 65537 ++ ++ ++/*-************************************ ++* CPU Feature Detection ++**************************************/ ++/* LZ4_FORCE_MEMORY_ACCESS ++ * By default, access to unaligned memory is controlled by `memcpy()`, which is safe and portable. ++ * Unfortunately, on some target/compiler combinations, the generated assembly is sub-optimal. ++ * The below switch allow to select different access method for improved performance. ++ * Method 0 (default) : use `memcpy()`. Safe and portable. ++ * Method 1 : `__packed` statement. It depends on compiler extension (ie, not portable). ++ * This method is safe if your compiler supports it, and *generally* as fast or faster than `memcpy`. ++ * Method 2 : direct access. This method is portable but violate C standard. ++ * It can generate buggy code on targets which assembly generation depends on alignment. ++ * But in some circumstances, it's the only known way to get the most performance (ie GCC + ARMv6) ++ * See https://fastcompression.blogspot.fr/2015/08/accessing-unaligned-memory.html for details. ++ * Prefer these methods in priority order (0 > 1 > 2) ++ */ ++#ifndef LZ4_FORCE_MEMORY_ACCESS /* can be defined externally */ ++# if defined(__GNUC__) && \ ++ ( defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_6J__) || defined(__ARM_ARCH_6K__) \ ++ || defined(__ARM_ARCH_6Z__) || defined(__ARM_ARCH_6ZK__) || defined(__ARM_ARCH_6T2__) \ ++ || (defined(__riscv) && defined(__riscv_zicclsm)) ) ++# define LZ4_FORCE_MEMORY_ACCESS 2 ++# elif (defined(__INTEL_COMPILER) && !defined(_WIN32)) || defined(__GNUC__) || defined(_MSC_VER) ++# define LZ4_FORCE_MEMORY_ACCESS 1 ++# endif ++#endif ++ ++/* ++ * LZ4_FORCE_SW_BITCOUNT ++ * Define this parameter if your target system or compiler does not support hardware bit count ++ */ ++#if defined(_MSC_VER) && defined(_WIN32_WCE) /* Visual Studio for WinCE doesn't support Hardware bit count */ ++# undef LZ4_FORCE_SW_BITCOUNT /* avoid double def */ ++# define LZ4_FORCE_SW_BITCOUNT ++#endif ++ ++ ++ ++/*-************************************ ++* Dependency ++**************************************/ ++/* ++ * LZ4_SRC_INCLUDED: ++ * Amalgamation flag, whether lz4.c is included ++ */ ++#ifndef LZ4_SRC_INCLUDED ++# define LZ4_SRC_INCLUDED 1 ++#endif ++ ++#ifndef LZ4_DISABLE_DEPRECATE_WARNINGS ++# define LZ4_DISABLE_DEPRECATE_WARNINGS /* due to LZ4_decompress_safe_withPrefix64k */ ++#endif ++ ++#ifndef LZ4_STATIC_LINKING_ONLY ++# define LZ4_STATIC_LINKING_ONLY ++#endif ++#include "lz4.h" ++/* see also "memory routines" below */ ++ ++ ++/*-************************************ ++* Compiler Options ++**************************************/ ++#if defined(_MSC_VER) && (_MSC_VER >= 1400) /* Visual Studio 2005+ */ ++# include /* only present in VS2005+ */ ++# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ ++# pragma warning(disable : 6237) /* disable: C6237: conditional expression is always 0 */ ++# pragma warning(disable : 6239) /* disable: C6239: ( && ) always evaluates to the result of */ ++# pragma warning(disable : 6240) /* disable: C6240: ( && ) always evaluates to the result of */ ++# pragma warning(disable : 6326) /* disable: C6326: Potential comparison of a constant with another constant */ ++#endif /* _MSC_VER */ ++ ++#ifndef LZ4_FORCE_INLINE ++# if defined (_MSC_VER) && !defined (__clang__) /* MSVC */ ++# define LZ4_FORCE_INLINE static __forceinline ++# else ++# if defined (__cplusplus) || defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 */ ++# if defined (__GNUC__) || defined (__clang__) ++# define LZ4_FORCE_INLINE static inline __attribute__((always_inline)) ++# else ++# define LZ4_FORCE_INLINE static inline ++# endif ++# else ++# define LZ4_FORCE_INLINE static ++# endif /* __STDC_VERSION__ */ ++# endif /* _MSC_VER */ ++#endif /* LZ4_FORCE_INLINE */ ++ ++/* LZ4_FORCE_O2 and LZ4_FORCE_INLINE ++ * gcc on ppc64le generates an unrolled SIMDized loop for LZ4_wildCopy8, ++ * together with a simple 8-byte copy loop as a fall-back path. ++ * However, this optimization hurts the decompression speed by >30%, ++ * because the execution does not go to the optimized loop ++ * for typical compressible data, and all of the preamble checks ++ * before going to the fall-back path become useless overhead. ++ * This optimization happens only with the -O3 flag, and -O2 generates ++ * a simple 8-byte copy loop. ++ * With gcc on ppc64le, all of the LZ4_decompress_* and LZ4_wildCopy8 ++ * functions are annotated with __attribute__((optimize("O2"))), ++ * and also LZ4_wildCopy8 is forcibly inlined, so that the O2 attribute ++ * of LZ4_wildCopy8 does not affect the compression speed. ++ */ ++#if defined(__PPC64__) && defined(__LITTLE_ENDIAN__) && defined(__GNUC__) && !defined(__clang__) ++# define LZ4_FORCE_O2 __attribute__((optimize("O2"))) ++# undef LZ4_FORCE_INLINE ++# define LZ4_FORCE_INLINE static __inline __attribute__((optimize("O2"),always_inline)) ++#else ++# define LZ4_FORCE_O2 ++#endif ++ ++#if (defined(__GNUC__) && (__GNUC__ >= 3)) || (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 800)) || defined(__clang__) ++# define expect(expr,value) (__builtin_expect ((expr),(value)) ) ++#else ++# define expect(expr,value) (expr) ++#endif ++ ++#ifndef likely ++#define likely(expr) expect((expr) != 0, 1) ++#endif ++#ifndef unlikely ++#define unlikely(expr) expect((expr) != 0, 0) ++#endif ++ ++/* Should the alignment test prove unreliable, for some reason, ++ * it can be disabled by setting LZ4_ALIGN_TEST to 0 */ ++#ifndef LZ4_ALIGN_TEST /* can be externally provided */ ++# define LZ4_ALIGN_TEST 1 ++#endif ++ ++ ++/*-************************************ ++* Memory routines ++**************************************/ ++ ++/*! LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION : ++ * Disable relatively high-level LZ4/HC functions that use dynamic memory ++ * allocation functions (malloc(), calloc(), free()). ++ * ++ * Note that this is a compile-time switch. And since it disables ++ * public/stable LZ4 v1 API functions, we don't recommend using this ++ * symbol to generate a library for distribution. ++ * ++ * The following public functions are removed when this symbol is defined. ++ * - lz4 : LZ4_createStream, LZ4_freeStream, ++ * LZ4_createStreamDecode, LZ4_freeStreamDecode, LZ4_create (deprecated) ++ * - lz4hc : LZ4_createStreamHC, LZ4_freeStreamHC, ++ * LZ4_createHC (deprecated), LZ4_freeHC (deprecated) ++ * - lz4frame, lz4file : All LZ4F_* functions ++ */ ++#if defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++# define ALLOC(s) lz4_error_memory_allocation_is_disabled ++# define ALLOC_AND_ZERO(s) lz4_error_memory_allocation_is_disabled ++# define FREEMEM(p) lz4_error_memory_allocation_is_disabled ++#elif defined(LZ4_USER_MEMORY_FUNCTIONS) ++/* memory management functions can be customized by user project. ++ * Below functions must exist somewhere in the Project ++ * and be available at link time */ ++void* LZ4_malloc(size_t s); ++void* LZ4_calloc(size_t n, size_t s); ++void LZ4_free(void* p); ++# define ALLOC(s) LZ4_malloc(s) ++# define ALLOC_AND_ZERO(s) LZ4_calloc(1,s) ++# define FREEMEM(p) LZ4_free(p) ++#else ++# include /* malloc, calloc, free */ ++# define ALLOC(s) malloc(s) ++# define ALLOC_AND_ZERO(s) calloc(1,s) ++# define FREEMEM(p) free(p) ++#endif ++ ++#if ! LZ4_FREESTANDING ++# include /* memset, memcpy */ ++#endif ++#if !defined(LZ4_memset) ++# define LZ4_memset(p,v,s) memset((p),(v),(s)) ++#endif ++#define MEM_INIT(p,v,s) LZ4_memset((p),(v),(s)) ++ ++ ++/*-************************************ ++* Common Constants ++**************************************/ ++#define MINMATCH 4 ++ ++#define WILDCOPYLENGTH 8 ++#define LASTLITERALS 5 /* see ../doc/lz4_Block_format.md#parsing-restrictions */ ++#define MFLIMIT 12 /* see ../doc/lz4_Block_format.md#parsing-restrictions */ ++#define MATCH_SAFEGUARD_DISTANCE ((2*WILDCOPYLENGTH) - MINMATCH) /* ensure it's possible to write 2 x wildcopyLength without overflowing output buffer */ ++#define FASTLOOP_SAFE_DISTANCE 64 ++static const int LZ4_minLength = (MFLIMIT+1); ++ ++#define KB *(1 <<10) ++#define MB *(1 <<20) ++#define GB *(1U<<30) ++ ++#define LZ4_DISTANCE_ABSOLUTE_MAX 65535 ++#if (LZ4_DISTANCE_MAX > LZ4_DISTANCE_ABSOLUTE_MAX) /* max supported by LZ4 format */ ++# error "LZ4_DISTANCE_MAX is too big : must be <= 65535" ++#endif ++ ++#define ML_BITS 4 ++#define ML_MASK ((1U<=1) ++# include ++#else ++# ifndef assert ++# define assert(condition) ((void)0) ++# endif ++#endif ++ ++#define LZ4_STATIC_ASSERT(c) { enum { LZ4_static_assert = 1/(int)(!!(c)) }; } /* use after variable declarations */ ++ ++#if defined(LZ4_DEBUG) && (LZ4_DEBUG>=2) ++# include ++ static int g_debuglog_enable = 1; ++# define DEBUGLOG(l, ...) { \ ++ if ((g_debuglog_enable) && (l<=LZ4_DEBUG)) { \ ++ fprintf(stderr, __FILE__ " %i: ", __LINE__); \ ++ fprintf(stderr, __VA_ARGS__); \ ++ fprintf(stderr, " \n"); \ ++ } } ++#else ++# define DEBUGLOG(l, ...) {} /* disabled */ ++#endif ++ ++static int LZ4_isAligned(const void* ptr, size_t alignment) ++{ ++ return ((size_t)ptr & (alignment -1)) == 0; ++} ++ ++ ++/*-************************************ ++* Types ++**************************************/ ++#include ++#if defined(__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ++# include ++ typedef unsigned char BYTE; /*uint8_t not necessarily blessed to alias arbitrary type*/ ++ typedef uint16_t U16; ++ typedef uint32_t U32; ++ typedef int32_t S32; ++ typedef uint64_t U64; ++ typedef uintptr_t uptrval; ++#else ++# if UINT_MAX != 4294967295UL ++# error "LZ4 code (when not C++ or C99) assumes that sizeof(int) == 4" ++# endif ++ typedef unsigned char BYTE; ++ typedef unsigned short U16; ++ typedef unsigned int U32; ++ typedef signed int S32; ++ typedef unsigned long long U64; ++ typedef size_t uptrval; /* generally true, except OpenVMS-64 */ ++#endif ++ ++#if defined(__x86_64__) ++ typedef U64 reg_t; /* 64-bits in x32 mode */ ++#else ++ typedef size_t reg_t; /* 32-bits in x32 mode */ ++#endif ++ ++typedef enum { ++ notLimited = 0, ++ limitedOutput = 1, ++ fillOutput = 2 ++} limitedOutput_directive; ++ ++ ++/*-************************************ ++* Reading and writing into memory ++**************************************/ ++ ++/** ++ * LZ4 relies on memcpy with a constant size being inlined. In freestanding ++ * environments, the compiler can't assume the implementation of memcpy() is ++ * standard compliant, so it can't apply its specialized memcpy() inlining ++ * logic. When possible, use __builtin_memcpy() to tell the compiler to analyze ++ * memcpy() as if it were standard compliant, so it can inline it in freestanding ++ * environments. This is needed when decompressing the Linux Kernel, for example. ++ */ ++#if !defined(LZ4_memcpy) ++# if defined(__GNUC__) && (__GNUC__ >= 4) ++# define LZ4_memcpy(dst, src, size) __builtin_memcpy(dst, src, size) ++# else ++# define LZ4_memcpy(dst, src, size) memcpy(dst, src, size) ++# endif ++#endif ++ ++#if !defined(LZ4_memmove) ++# if defined(__GNUC__) && (__GNUC__ >= 4) ++# define LZ4_memmove __builtin_memmove ++# else ++# define LZ4_memmove memmove ++# endif ++#endif ++ ++static unsigned LZ4_isLittleEndian(void) ++{ ++ const union { U32 u; BYTE c[4]; } one = { 1 }; /* don't use static : performance detrimental */ ++ return one.c[0]; ++} ++ ++#if defined(__GNUC__) || defined(__INTEL_COMPILER) ++#define LZ4_PACK( __Declaration__ ) __Declaration__ __attribute__((__packed__)) ++#elif defined(_MSC_VER) ++#define LZ4_PACK( __Declaration__ ) __pragma( pack(push, 1) ) __Declaration__ __pragma( pack(pop)) ++#endif ++ ++#if defined(LZ4_FORCE_MEMORY_ACCESS) && (LZ4_FORCE_MEMORY_ACCESS==2) ++/* lie to the compiler about data alignment; use with caution */ ++ ++static U16 LZ4_read16(const void* memPtr) { return *(const U16*) memPtr; } ++static U32 LZ4_read32(const void* memPtr) { return *(const U32*) memPtr; } ++static reg_t LZ4_read_ARCH(const void* memPtr) { return *(const reg_t*) memPtr; } ++ ++static void LZ4_write16(void* memPtr, U16 value) { *(U16*)memPtr = value; } ++static void LZ4_write32(void* memPtr, U32 value) { *(U32*)memPtr = value; } ++ ++#elif defined(LZ4_FORCE_MEMORY_ACCESS) && (LZ4_FORCE_MEMORY_ACCESS==1) ++ ++/* __pack instructions are safer, but compiler specific, hence potentially problematic for some compilers */ ++/* currently only defined for gcc and icc */ ++LZ4_PACK(typedef struct { U16 u16; }) LZ4_unalign16; ++LZ4_PACK(typedef struct { U32 u32; }) LZ4_unalign32; ++LZ4_PACK(typedef struct { reg_t uArch; }) LZ4_unalignST; ++ ++static U16 LZ4_read16(const void* ptr) { return ((const LZ4_unalign16*)ptr)->u16; } ++static U32 LZ4_read32(const void* ptr) { return ((const LZ4_unalign32*)ptr)->u32; } ++static reg_t LZ4_read_ARCH(const void* ptr) { return ((const LZ4_unalignST*)ptr)->uArch; } ++ ++static void LZ4_write16(void* memPtr, U16 value) { ((LZ4_unalign16*)memPtr)->u16 = value; } ++static void LZ4_write32(void* memPtr, U32 value) { ((LZ4_unalign32*)memPtr)->u32 = value; } ++ ++#else /* safe and portable access using memcpy() */ ++ ++static U16 LZ4_read16(const void* memPtr) ++{ ++ U16 val; LZ4_memcpy(&val, memPtr, sizeof(val)); return val; ++} ++ ++static U32 LZ4_read32(const void* memPtr) ++{ ++ U32 val; LZ4_memcpy(&val, memPtr, sizeof(val)); return val; ++} ++ ++static reg_t LZ4_read_ARCH(const void* memPtr) ++{ ++ reg_t val; LZ4_memcpy(&val, memPtr, sizeof(val)); return val; ++} ++ ++static void LZ4_write16(void* memPtr, U16 value) ++{ ++ LZ4_memcpy(memPtr, &value, sizeof(value)); ++} ++ ++static void LZ4_write32(void* memPtr, U32 value) ++{ ++ LZ4_memcpy(memPtr, &value, sizeof(value)); ++} ++ ++#endif /* LZ4_FORCE_MEMORY_ACCESS */ ++ ++ ++static U16 LZ4_readLE16(const void* memPtr) ++{ ++ if (LZ4_isLittleEndian()) { ++ return LZ4_read16(memPtr); ++ } else { ++ const BYTE* p = (const BYTE*)memPtr; ++ return (U16)((U16)p[0] | (p[1]<<8)); ++ } ++} ++ ++#ifdef LZ4_STATIC_LINKING_ONLY_ENDIANNESS_INDEPENDENT_OUTPUT ++static U32 LZ4_readLE32(const void* memPtr) ++{ ++ if (LZ4_isLittleEndian()) { ++ return LZ4_read32(memPtr); ++ } else { ++ const BYTE* p = (const BYTE*)memPtr; ++ return (U32)p[0] | (p[1]<<8) | (p[2]<<16) | (p[3]<<24); ++ } ++} ++#endif ++ ++static void LZ4_writeLE16(void* memPtr, U16 value) ++{ ++ if (LZ4_isLittleEndian()) { ++ LZ4_write16(memPtr, value); ++ } else { ++ BYTE* p = (BYTE*)memPtr; ++ p[0] = (BYTE) value; ++ p[1] = (BYTE)(value>>8); ++ } ++} ++ ++/* customized variant of memcpy, which can overwrite up to 8 bytes beyond dstEnd */ ++LZ4_FORCE_INLINE ++void LZ4_wildCopy8(void* dstPtr, const void* srcPtr, void* dstEnd) ++{ ++ BYTE* d = (BYTE*)dstPtr; ++ const BYTE* s = (const BYTE*)srcPtr; ++ BYTE* const e = (BYTE*)dstEnd; ++ ++ do { LZ4_memcpy(d,s,8); d+=8; s+=8; } while (d= 16. */ ++LZ4_FORCE_INLINE void ++LZ4_wildCopy32(void* dstPtr, const void* srcPtr, void* dstEnd) ++{ ++ BYTE* d = (BYTE*)dstPtr; ++ const BYTE* s = (const BYTE*)srcPtr; ++ BYTE* const e = (BYTE*)dstEnd; ++ ++ do { LZ4_memcpy(d,s,16); LZ4_memcpy(d+16,s+16,16); d+=32; s+=32; } while (d= dstPtr + MINMATCH ++ * - there is at least 12 bytes available to write after dstEnd */ ++LZ4_FORCE_INLINE void ++LZ4_memcpy_using_offset(BYTE* dstPtr, const BYTE* srcPtr, BYTE* dstEnd, const size_t offset) ++{ ++ BYTE v[8]; ++ ++ assert(dstEnd >= dstPtr + MINMATCH); ++ ++ switch(offset) { ++ case 1: ++ MEM_INIT(v, *srcPtr, 8); ++ break; ++ case 2: ++ LZ4_memcpy(v, srcPtr, 2); ++ LZ4_memcpy(&v[2], srcPtr, 2); ++#if defined(_MSC_VER) && (_MSC_VER <= 1937) /* MSVC 2022 ver 17.7 or earlier */ ++# pragma warning(push) ++# pragma warning(disable : 6385) /* warning C6385: Reading invalid data from 'v'. */ ++#endif ++ LZ4_memcpy(&v[4], v, 4); ++#if defined(_MSC_VER) && (_MSC_VER <= 1937) /* MSVC 2022 ver 17.7 or earlier */ ++# pragma warning(pop) ++#endif ++ break; ++ case 4: ++ LZ4_memcpy(v, srcPtr, 4); ++ LZ4_memcpy(&v[4], srcPtr, 4); ++ break; ++ default: ++ LZ4_memcpy_using_offset_base(dstPtr, srcPtr, dstEnd, offset); ++ return; ++ } ++ ++ LZ4_memcpy(dstPtr, v, 8); ++ dstPtr += 8; ++ while (dstPtr < dstEnd) { ++ LZ4_memcpy(dstPtr, v, 8); ++ dstPtr += 8; ++ } ++} ++#endif ++ ++ ++/*-************************************ ++* Common functions ++**************************************/ ++static unsigned LZ4_NbCommonBytes (reg_t val) ++{ ++ assert(val != 0); ++ if (LZ4_isLittleEndian()) { ++ if (sizeof(val) == 8) { ++# if defined(_MSC_VER) && (_MSC_VER >= 1800) && (defined(_M_AMD64) && !defined(_M_ARM64EC)) && !defined(LZ4_FORCE_SW_BITCOUNT) ++/*-************************************************************************************************* ++* ARM64EC is a Microsoft-designed ARM64 ABI compatible with AMD64 applications on ARM64 Windows 11. ++* The ARM64EC ABI does not support AVX/AVX2/AVX512 instructions, nor their relevant intrinsics ++* including _tzcnt_u64. Therefore, we need to neuter the _tzcnt_u64 code path for ARM64EC. ++****************************************************************************************************/ ++# if defined(__clang__) && (__clang_major__ < 10) ++ /* Avoid undefined clang-cl intrinsics issue. ++ * See https://github.com/lz4/lz4/pull/1017 for details. */ ++ return (unsigned)__builtin_ia32_tzcnt_u64(val) >> 3; ++# else ++ /* x64 CPUS without BMI support interpret `TZCNT` as `REP BSF` */ ++ return (unsigned)_tzcnt_u64(val) >> 3; ++# endif ++# elif defined(_MSC_VER) && defined(_WIN64) && !defined(LZ4_FORCE_SW_BITCOUNT) ++ unsigned long r = 0; ++ _BitScanForward64(&r, (U64)val); ++ return (unsigned)r >> 3; ++# elif (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ ++ ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ ++ !defined(LZ4_FORCE_SW_BITCOUNT) ++ return (unsigned)__builtin_ctzll((U64)val) >> 3; ++# else ++ const U64 m = 0x0101010101010101ULL; ++ val ^= val - 1; ++ return (unsigned)(((U64)((val & (m - 1)) * m)) >> 56); ++# endif ++ } else /* 32 bits */ { ++# if defined(_MSC_VER) && (_MSC_VER >= 1400) && !defined(LZ4_FORCE_SW_BITCOUNT) ++ unsigned long r; ++ _BitScanForward(&r, (U32)val); ++ return (unsigned)r >> 3; ++# elif (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ ++ ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ ++ !defined(__TINYC__) && !defined(LZ4_FORCE_SW_BITCOUNT) ++ return (unsigned)__builtin_ctz((U32)val) >> 3; ++# else ++ const U32 m = 0x01010101; ++ return (unsigned)((((val - 1) ^ val) & (m - 1)) * m) >> 24; ++# endif ++ } ++ } else /* Big Endian CPU */ { ++ if (sizeof(val)==8) { ++# if (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ ++ ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ ++ !defined(__TINYC__) && !defined(LZ4_FORCE_SW_BITCOUNT) ++ return (unsigned)__builtin_clzll((U64)val) >> 3; ++# else ++#if 1 ++ /* this method is probably faster, ++ * but adds a 128 bytes lookup table */ ++ static const unsigned char ctz7_tab[128] = { ++ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, ++ }; ++ U64 const mask = 0x0101010101010101ULL; ++ U64 const t = (((val >> 8) - mask) | val) & mask; ++ return ctz7_tab[(t * 0x0080402010080402ULL) >> 57]; ++#else ++ /* this method doesn't consume memory space like the previous one, ++ * but it contains several branches, ++ * that may end up slowing execution */ ++ static const U32 by32 = sizeof(val)*4; /* 32 on 64 bits (goal), 16 on 32 bits. ++ Just to avoid some static analyzer complaining about shift by 32 on 32-bits target. ++ Note that this code path is never triggered in 32-bits mode. */ ++ unsigned r; ++ if (!(val>>by32)) { r=4; } else { r=0; val>>=by32; } ++ if (!(val>>16)) { r+=2; val>>=8; } else { val>>=24; } ++ r += (!val); ++ return r; ++#endif ++# endif ++ } else /* 32 bits */ { ++# if (defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 3) || \ ++ ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))))) && \ ++ !defined(LZ4_FORCE_SW_BITCOUNT) ++ return (unsigned)__builtin_clz((U32)val) >> 3; ++# else ++ val >>= 8; ++ val = ((((val + 0x00FFFF00) | 0x00FFFFFF) + val) | ++ (val + 0x00FF0000)) >> 24; ++ return (unsigned)val ^ 3; ++# endif ++ } ++ } ++} ++ ++ ++#define STEPSIZE sizeof(reg_t) ++LZ4_FORCE_INLINE ++unsigned LZ4_count(const BYTE* pIn, const BYTE* pMatch, const BYTE* pInLimit) ++{ ++ const BYTE* const pStart = pIn; ++ ++ if (likely(pIn < pInLimit-(STEPSIZE-1))) { ++ reg_t const diff = LZ4_read_ARCH(pMatch) ^ LZ4_read_ARCH(pIn); ++ if (!diff) { ++ pIn+=STEPSIZE; pMatch+=STEPSIZE; ++ } else { ++ return LZ4_NbCommonBytes(diff); ++ } } ++ ++ while (likely(pIn < pInLimit-(STEPSIZE-1))) { ++ reg_t const diff = LZ4_read_ARCH(pMatch) ^ LZ4_read_ARCH(pIn); ++ if (!diff) { pIn+=STEPSIZE; pMatch+=STEPSIZE; continue; } ++ pIn += LZ4_NbCommonBytes(diff); ++ return (unsigned)(pIn - pStart); ++ } ++ ++ if ((STEPSIZE==8) && (pIn<(pInLimit-3)) && (LZ4_read32(pMatch) == LZ4_read32(pIn))) { pIn+=4; pMatch+=4; } ++ if ((pIn<(pInLimit-1)) && (LZ4_read16(pMatch) == LZ4_read16(pIn))) { pIn+=2; pMatch+=2; } ++ if ((pIn compression run slower on incompressible data */ ++ ++ ++/*-************************************ ++* Local Structures and types ++**************************************/ ++typedef enum { clearedTable = 0, byPtr, byU32, byU16 } tableType_t; ++ ++/** ++ * This enum distinguishes several different modes of accessing previous ++ * content in the stream. ++ * ++ * - noDict : There is no preceding content. ++ * - withPrefix64k : Table entries up to ctx->dictSize before the current blob ++ * blob being compressed are valid and refer to the preceding ++ * content (of length ctx->dictSize), which is available ++ * contiguously preceding in memory the content currently ++ * being compressed. ++ * - usingExtDict : Like withPrefix64k, but the preceding content is somewhere ++ * else in memory, starting at ctx->dictionary with length ++ * ctx->dictSize. ++ * - usingDictCtx : Everything concerning the preceding content is ++ * in a separate context, pointed to by ctx->dictCtx. ++ * ctx->dictionary, ctx->dictSize, and table entries ++ * in the current context that refer to positions ++ * preceding the beginning of the current compression are ++ * ignored. Instead, ctx->dictCtx->dictionary and ctx->dictCtx ++ * ->dictSize describe the location and size of the preceding ++ * content, and matches are found by looking in the ctx ++ * ->dictCtx->hashTable. ++ */ ++typedef enum { noDict = 0, withPrefix64k, usingExtDict, usingDictCtx } dict_directive; ++typedef enum { noDictIssue = 0, dictSmall } dictIssue_directive; ++ ++ ++/*-************************************ ++* Local Utils ++**************************************/ ++int LZ4_versionNumber (void) { return LZ4_VERSION_NUMBER; } ++const char* LZ4_versionString(void) { return LZ4_VERSION_STRING; } ++int LZ4_compressBound(int isize) { return LZ4_COMPRESSBOUND(isize); } ++int LZ4_sizeofState(void) { return sizeof(LZ4_stream_t); } ++ ++ ++/*-**************************************** ++* Internal Definitions, used only in Tests ++*******************************************/ ++#if defined (__cplusplus) ++extern "C" { ++#endif ++ ++int LZ4_compress_forceExtDict (LZ4_stream_t* LZ4_dict, const char* source, char* dest, int srcSize); ++ ++int LZ4_decompress_safe_forceExtDict(const char* source, char* dest, ++ int compressedSize, int maxOutputSize, ++ const void* dictStart, size_t dictSize); ++int LZ4_decompress_safe_partial_forceExtDict(const char* source, char* dest, ++ int compressedSize, int targetOutputSize, int dstCapacity, ++ const void* dictStart, size_t dictSize); ++#if defined (__cplusplus) ++} ++#endif ++ ++/*-****************************** ++* Compression functions ++********************************/ ++LZ4_FORCE_INLINE U32 LZ4_hash4(U32 sequence, tableType_t const tableType) ++{ ++ if (tableType == byU16) ++ return ((sequence * 2654435761U) >> ((MINMATCH*8)-(LZ4_HASHLOG+1))); ++ else ++ return ((sequence * 2654435761U) >> ((MINMATCH*8)-LZ4_HASHLOG)); ++} ++ ++LZ4_FORCE_INLINE U32 LZ4_hash5(U64 sequence, tableType_t const tableType) ++{ ++ const U32 hashLog = (tableType == byU16) ? LZ4_HASHLOG+1 : LZ4_HASHLOG; ++ if (LZ4_isLittleEndian()) { ++ const U64 prime5bytes = 889523592379ULL; ++ return (U32)(((sequence << 24) * prime5bytes) >> (64 - hashLog)); ++ } else { ++ const U64 prime8bytes = 11400714785074694791ULL; ++ return (U32)(((sequence >> 24) * prime8bytes) >> (64 - hashLog)); ++ } ++} ++ ++LZ4_FORCE_INLINE U32 LZ4_hashPosition(const void* const p, tableType_t const tableType) ++{ ++ if ((sizeof(reg_t)==8) && (tableType != byU16)) return LZ4_hash5(LZ4_read_ARCH(p), tableType); ++ ++#ifdef LZ4_STATIC_LINKING_ONLY_ENDIANNESS_INDEPENDENT_OUTPUT ++ return LZ4_hash4(LZ4_readLE32(p), tableType); ++#else ++ return LZ4_hash4(LZ4_read32(p), tableType); ++#endif ++} ++ ++LZ4_FORCE_INLINE void LZ4_clearHash(U32 h, void* tableBase, tableType_t const tableType) ++{ ++ switch (tableType) ++ { ++ default: /* fallthrough */ ++ case clearedTable: { /* illegal! */ assert(0); return; } ++ case byPtr: { const BYTE** hashTable = (const BYTE**)tableBase; hashTable[h] = NULL; return; } ++ case byU32: { U32* hashTable = (U32*) tableBase; hashTable[h] = 0; return; } ++ case byU16: { U16* hashTable = (U16*) tableBase; hashTable[h] = 0; return; } ++ } ++} ++ ++LZ4_FORCE_INLINE void LZ4_putIndexOnHash(U32 idx, U32 h, void* tableBase, tableType_t const tableType) ++{ ++ switch (tableType) ++ { ++ default: /* fallthrough */ ++ case clearedTable: /* fallthrough */ ++ case byPtr: { /* illegal! */ assert(0); return; } ++ case byU32: { U32* hashTable = (U32*) tableBase; hashTable[h] = idx; return; } ++ case byU16: { U16* hashTable = (U16*) tableBase; assert(idx < 65536); hashTable[h] = (U16)idx; return; } ++ } ++} ++ ++/* LZ4_putPosition*() : only used in byPtr mode */ ++LZ4_FORCE_INLINE void LZ4_putPositionOnHash(const BYTE* p, U32 h, ++ void* tableBase, tableType_t const tableType) ++{ ++ const BYTE** const hashTable = (const BYTE**)tableBase; ++ assert(tableType == byPtr); (void)tableType; ++ hashTable[h] = p; ++} ++ ++LZ4_FORCE_INLINE void LZ4_putPosition(const BYTE* p, void* tableBase, tableType_t tableType) ++{ ++ U32 const h = LZ4_hashPosition(p, tableType); ++ LZ4_putPositionOnHash(p, h, tableBase, tableType); ++} ++ ++/* LZ4_getIndexOnHash() : ++ * Index of match position registered in hash table. ++ * hash position must be calculated by using base+index, or dictBase+index. ++ * Assumption 1 : only valid if tableType == byU32 or byU16. ++ * Assumption 2 : h is presumed valid (within limits of hash table) ++ */ ++LZ4_FORCE_INLINE U32 LZ4_getIndexOnHash(U32 h, const void* tableBase, tableType_t tableType) ++{ ++ LZ4_STATIC_ASSERT(LZ4_MEMORY_USAGE > 2); ++ if (tableType == byU32) { ++ const U32* const hashTable = (const U32*) tableBase; ++ assert(h < (1U << (LZ4_MEMORY_USAGE-2))); ++ return hashTable[h]; ++ } ++ if (tableType == byU16) { ++ const U16* const hashTable = (const U16*) tableBase; ++ assert(h < (1U << (LZ4_MEMORY_USAGE-1))); ++ return hashTable[h]; ++ } ++ assert(0); return 0; /* forbidden case */ ++} ++ ++static const BYTE* LZ4_getPositionOnHash(U32 h, const void* tableBase, tableType_t tableType) ++{ ++ assert(tableType == byPtr); (void)tableType; ++ { const BYTE* const* hashTable = (const BYTE* const*) tableBase; return hashTable[h]; } ++} ++ ++LZ4_FORCE_INLINE const BYTE* ++LZ4_getPosition(const BYTE* p, ++ const void* tableBase, tableType_t tableType) ++{ ++ U32 const h = LZ4_hashPosition(p, tableType); ++ return LZ4_getPositionOnHash(h, tableBase, tableType); ++} ++ ++LZ4_FORCE_INLINE void ++LZ4_prepareTable(LZ4_stream_t_internal* const cctx, ++ const int inputSize, ++ const tableType_t tableType) { ++ /* If the table hasn't been used, it's guaranteed to be zeroed out, and is ++ * therefore safe to use no matter what mode we're in. Otherwise, we figure ++ * out if it's safe to leave as is or whether it needs to be reset. ++ */ ++ if ((tableType_t)cctx->tableType != clearedTable) { ++ assert(inputSize >= 0); ++ if ((tableType_t)cctx->tableType != tableType ++ || ((tableType == byU16) && cctx->currentOffset + (unsigned)inputSize >= 0xFFFFU) ++ || ((tableType == byU32) && cctx->currentOffset > 1 GB) ++ || tableType == byPtr ++ || inputSize >= 4 KB) ++ { ++ DEBUGLOG(4, "LZ4_prepareTable: Resetting table in %p", (void*)cctx); ++ MEM_INIT(cctx->hashTable, 0, LZ4_HASHTABLESIZE); ++ cctx->currentOffset = 0; ++ cctx->tableType = (U32)clearedTable; ++ } else { ++ DEBUGLOG(4, "LZ4_prepareTable: Re-use hash table (no reset)"); ++ } ++ } ++ ++ /* Adding a gap, so all previous entries are > LZ4_DISTANCE_MAX back, ++ * is faster than compressing without a gap. ++ * However, compressing with currentOffset == 0 is faster still, ++ * so we preserve that case. ++ */ ++ if (cctx->currentOffset != 0 && tableType == byU32) { ++ DEBUGLOG(5, "LZ4_prepareTable: adding 64KB to currentOffset"); ++ cctx->currentOffset += 64 KB; ++ } ++ ++ /* Finally, clear history */ ++ cctx->dictCtx = NULL; ++ cctx->dictionary = NULL; ++ cctx->dictSize = 0; ++} ++ ++/** LZ4_compress_generic_validated() : ++ * inlined, to ensure branches are decided at compilation time. ++ * The following conditions are presumed already validated: ++ * - source != NULL ++ * - inputSize > 0 ++ */ ++LZ4_FORCE_INLINE int LZ4_compress_generic_validated( ++ LZ4_stream_t_internal* const cctx, ++ const char* const source, ++ char* const dest, ++ const int inputSize, ++ int* inputConsumed, /* only written when outputDirective == fillOutput */ ++ const int maxOutputSize, ++ const limitedOutput_directive outputDirective, ++ const tableType_t tableType, ++ const dict_directive dictDirective, ++ const dictIssue_directive dictIssue, ++ const int acceleration) ++{ ++ int result; ++ const BYTE* ip = (const BYTE*)source; ++ ++ U32 const startIndex = cctx->currentOffset; ++ const BYTE* base = (const BYTE*)source - startIndex; ++ const BYTE* lowLimit; ++ ++ const LZ4_stream_t_internal* dictCtx = (const LZ4_stream_t_internal*) cctx->dictCtx; ++ const BYTE* const dictionary = ++ dictDirective == usingDictCtx ? dictCtx->dictionary : cctx->dictionary; ++ const U32 dictSize = ++ dictDirective == usingDictCtx ? dictCtx->dictSize : cctx->dictSize; ++ const U32 dictDelta = ++ (dictDirective == usingDictCtx) ? startIndex - dictCtx->currentOffset : 0; /* make indexes in dictCtx comparable with indexes in current context */ ++ ++ int const maybe_extMem = (dictDirective == usingExtDict) || (dictDirective == usingDictCtx); ++ U32 const prefixIdxLimit = startIndex - dictSize; /* used when dictDirective == dictSmall */ ++ const BYTE* const dictEnd = dictionary ? dictionary + dictSize : dictionary; ++ const BYTE* anchor = (const BYTE*) source; ++ const BYTE* const iend = ip + inputSize; ++ const BYTE* const mflimitPlusOne = iend - MFLIMIT + 1; ++ const BYTE* const matchlimit = iend - LASTLITERALS; ++ ++ /* the dictCtx currentOffset is indexed on the start of the dictionary, ++ * while a dictionary in the current context precedes the currentOffset */ ++ const BYTE* dictBase = (dictionary == NULL) ? NULL : ++ (dictDirective == usingDictCtx) ? ++ dictionary + dictSize - dictCtx->currentOffset : ++ dictionary + dictSize - startIndex; ++ ++ BYTE* op = (BYTE*) dest; ++ BYTE* const olimit = op + maxOutputSize; ++ ++ U32 offset = 0; ++ U32 forwardH; ++ ++ DEBUGLOG(5, "LZ4_compress_generic_validated: srcSize=%i, tableType=%u", inputSize, tableType); ++ assert(ip != NULL); ++ if (tableType == byU16) assert(inputSize= 1); ++ ++ lowLimit = (const BYTE*)source - (dictDirective == withPrefix64k ? dictSize : 0); ++ ++ /* Update context state */ ++ if (dictDirective == usingDictCtx) { ++ /* Subsequent linked blocks can't use the dictionary. */ ++ /* Instead, they use the block we just compressed. */ ++ cctx->dictCtx = NULL; ++ cctx->dictSize = (U32)inputSize; ++ } else { ++ cctx->dictSize += (U32)inputSize; ++ } ++ cctx->currentOffset += (U32)inputSize; ++ cctx->tableType = (U32)tableType; ++ ++ if (inputSizehashTable, byPtr); ++ } else { ++ LZ4_putIndexOnHash(startIndex, h, cctx->hashTable, tableType); ++ } } ++ ip++; forwardH = LZ4_hashPosition(ip, tableType); ++ ++ /* Main Loop */ ++ for ( ; ; ) { ++ const BYTE* match; ++ BYTE* token; ++ const BYTE* filledIp; ++ ++ /* Find a match */ ++ if (tableType == byPtr) { ++ const BYTE* forwardIp = ip; ++ int step = 1; ++ int searchMatchNb = acceleration << LZ4_skipTrigger; ++ do { ++ U32 const h = forwardH; ++ ip = forwardIp; ++ forwardIp += step; ++ step = (searchMatchNb++ >> LZ4_skipTrigger); ++ ++ if (unlikely(forwardIp > mflimitPlusOne)) goto _last_literals; ++ assert(ip < mflimitPlusOne); ++ ++ match = LZ4_getPositionOnHash(h, cctx->hashTable, tableType); ++ forwardH = LZ4_hashPosition(forwardIp, tableType); ++ LZ4_putPositionOnHash(ip, h, cctx->hashTable, tableType); ++ ++ } while ( (match+LZ4_DISTANCE_MAX < ip) ++ || (LZ4_read32(match) != LZ4_read32(ip)) ); ++ ++ } else { /* byU32, byU16 */ ++ ++ const BYTE* forwardIp = ip; ++ int step = 1; ++ int searchMatchNb = acceleration << LZ4_skipTrigger; ++ do { ++ U32 const h = forwardH; ++ U32 const current = (U32)(forwardIp - base); ++ U32 matchIndex = LZ4_getIndexOnHash(h, cctx->hashTable, tableType); ++ assert(matchIndex <= current); ++ assert(forwardIp - base < (ptrdiff_t)(2 GB - 1)); ++ ip = forwardIp; ++ forwardIp += step; ++ step = (searchMatchNb++ >> LZ4_skipTrigger); ++ ++ if (unlikely(forwardIp > mflimitPlusOne)) goto _last_literals; ++ assert(ip < mflimitPlusOne); ++ ++ if (dictDirective == usingDictCtx) { ++ if (matchIndex < startIndex) { ++ /* there was no match, try the dictionary */ ++ assert(tableType == byU32); ++ matchIndex = LZ4_getIndexOnHash(h, dictCtx->hashTable, byU32); ++ match = dictBase + matchIndex; ++ matchIndex += dictDelta; /* make dictCtx index comparable with current context */ ++ lowLimit = dictionary; ++ } else { ++ match = base + matchIndex; ++ lowLimit = (const BYTE*)source; ++ } ++ } else if (dictDirective == usingExtDict) { ++ if (matchIndex < startIndex) { ++ DEBUGLOG(7, "extDict candidate: matchIndex=%5u < startIndex=%5u", matchIndex, startIndex); ++ assert(startIndex - matchIndex >= MINMATCH); ++ assert(dictBase); ++ match = dictBase + matchIndex; ++ lowLimit = dictionary; ++ } else { ++ match = base + matchIndex; ++ lowLimit = (const BYTE*)source; ++ } ++ } else { /* single continuous memory segment */ ++ match = base + matchIndex; ++ } ++ forwardH = LZ4_hashPosition(forwardIp, tableType); ++ LZ4_putIndexOnHash(current, h, cctx->hashTable, tableType); ++ ++ DEBUGLOG(7, "candidate at pos=%u (offset=%u \n", matchIndex, current - matchIndex); ++ if ((dictIssue == dictSmall) && (matchIndex < prefixIdxLimit)) { continue; } /* match outside of valid area */ ++ assert(matchIndex < current); ++ if ( ((tableType != byU16) || (LZ4_DISTANCE_MAX < LZ4_DISTANCE_ABSOLUTE_MAX)) ++ && (matchIndex+LZ4_DISTANCE_MAX < current)) { ++ continue; ++ } /* too far */ ++ assert((current - matchIndex) <= LZ4_DISTANCE_MAX); /* match now expected within distance */ ++ ++ if (LZ4_read32(match) == LZ4_read32(ip)) { ++ if (maybe_extMem) offset = current - matchIndex; ++ break; /* match found */ ++ } ++ ++ } while(1); ++ } ++ ++ /* Catch up */ ++ filledIp = ip; ++ assert(ip > anchor); /* this is always true as ip has been advanced before entering the main loop */ ++ if ((match > lowLimit) && unlikely(ip[-1] == match[-1])) { ++ do { ip--; match--; } while (((ip > anchor) & (match > lowLimit)) && (unlikely(ip[-1] == match[-1]))); ++ } ++ ++ /* Encode Literals */ ++ { unsigned const litLength = (unsigned)(ip - anchor); ++ token = op++; ++ if ((outputDirective == limitedOutput) && /* Check output buffer overflow */ ++ (unlikely(op + litLength + (2 + 1 + LASTLITERALS) + (litLength/255) > olimit)) ) { ++ return 0; /* cannot compress within `dst` budget. Stored indexes in hash table are nonetheless fine */ ++ } ++ if ((outputDirective == fillOutput) && ++ (unlikely(op + (litLength+240)/255 /* litlen */ + litLength /* literals */ + 2 /* offset */ + 1 /* token */ + MFLIMIT - MINMATCH /* min last literals so last match is <= end - MFLIMIT */ > olimit))) { ++ op--; ++ goto _last_literals; ++ } ++ if (litLength >= RUN_MASK) { ++ unsigned len = litLength - RUN_MASK; ++ *token = (RUN_MASK<= 255 ; len-=255) *op++ = 255; ++ *op++ = (BYTE)len; ++ } ++ else *token = (BYTE)(litLength< olimit)) { ++ /* the match was too close to the end, rewind and go to last literals */ ++ op = token; ++ goto _last_literals; ++ } ++ ++ /* Encode Offset */ ++ if (maybe_extMem) { /* static test */ ++ DEBUGLOG(6, " with offset=%u (ext if > %i)", offset, (int)(ip - (const BYTE*)source)); ++ assert(offset <= LZ4_DISTANCE_MAX && offset > 0); ++ LZ4_writeLE16(op, (U16)offset); op+=2; ++ } else { ++ DEBUGLOG(6, " with offset=%u (same segment)", (U32)(ip - match)); ++ assert(ip-match <= LZ4_DISTANCE_MAX); ++ LZ4_writeLE16(op, (U16)(ip - match)); op+=2; ++ } ++ ++ /* Encode MatchLength */ ++ { unsigned matchCode; ++ ++ if ( (dictDirective==usingExtDict || dictDirective==usingDictCtx) ++ && (lowLimit==dictionary) /* match within extDict */ ) { ++ const BYTE* limit = ip + (dictEnd-match); ++ assert(dictEnd > match); ++ if (limit > matchlimit) limit = matchlimit; ++ matchCode = LZ4_count(ip+MINMATCH, match+MINMATCH, limit); ++ ip += (size_t)matchCode + MINMATCH; ++ if (ip==limit) { ++ unsigned const more = LZ4_count(limit, (const BYTE*)source, matchlimit); ++ matchCode += more; ++ ip += more; ++ } ++ DEBUGLOG(6, " with matchLength=%u starting in extDict", matchCode+MINMATCH); ++ } else { ++ matchCode = LZ4_count(ip+MINMATCH, match+MINMATCH, matchlimit); ++ ip += (size_t)matchCode + MINMATCH; ++ DEBUGLOG(6, " with matchLength=%u", matchCode+MINMATCH); ++ } ++ ++ if ((outputDirective) && /* Check output buffer overflow */ ++ (unlikely(op + (1 + LASTLITERALS) + (matchCode+240)/255 > olimit)) ) { ++ if (outputDirective == fillOutput) { ++ /* Match description too long : reduce it */ ++ U32 newMatchCode = 15 /* in token */ - 1 /* to avoid needing a zero byte */ + ((U32)(olimit - op) - 1 - LASTLITERALS) * 255; ++ ip -= matchCode - newMatchCode; ++ assert(newMatchCode < matchCode); ++ matchCode = newMatchCode; ++ if (unlikely(ip <= filledIp)) { ++ /* We have already filled up to filledIp so if ip ends up less than filledIp ++ * we have positions in the hash table beyond the current position. This is ++ * a problem if we reuse the hash table. So we have to remove these positions ++ * from the hash table. ++ */ ++ const BYTE* ptr; ++ DEBUGLOG(5, "Clearing %u positions", (U32)(filledIp - ip)); ++ for (ptr = ip; ptr <= filledIp; ++ptr) { ++ U32 const h = LZ4_hashPosition(ptr, tableType); ++ LZ4_clearHash(h, cctx->hashTable, tableType); ++ } ++ } ++ } else { ++ assert(outputDirective == limitedOutput); ++ return 0; /* cannot compress within `dst` budget. Stored indexes in hash table are nonetheless fine */ ++ } ++ } ++ if (matchCode >= ML_MASK) { ++ *token += ML_MASK; ++ matchCode -= ML_MASK; ++ LZ4_write32(op, 0xFFFFFFFF); ++ while (matchCode >= 4*255) { ++ op+=4; ++ LZ4_write32(op, 0xFFFFFFFF); ++ matchCode -= 4*255; ++ } ++ op += matchCode / 255; ++ *op++ = (BYTE)(matchCode % 255); ++ } else ++ *token += (BYTE)(matchCode); ++ } ++ /* Ensure we have enough space for the last literals. */ ++ assert(!(outputDirective == fillOutput && op + 1 + LASTLITERALS > olimit)); ++ ++ anchor = ip; ++ ++ /* Test end of chunk */ ++ if (ip >= mflimitPlusOne) break; ++ ++ /* Fill table */ ++ { U32 const h = LZ4_hashPosition(ip-2, tableType); ++ if (tableType == byPtr) { ++ LZ4_putPositionOnHash(ip-2, h, cctx->hashTable, byPtr); ++ } else { ++ U32 const idx = (U32)((ip-2) - base); ++ LZ4_putIndexOnHash(idx, h, cctx->hashTable, tableType); ++ } } ++ ++ /* Test next position */ ++ if (tableType == byPtr) { ++ ++ match = LZ4_getPosition(ip, cctx->hashTable, tableType); ++ LZ4_putPosition(ip, cctx->hashTable, tableType); ++ if ( (match+LZ4_DISTANCE_MAX >= ip) ++ && (LZ4_read32(match) == LZ4_read32(ip)) ) ++ { token=op++; *token=0; goto _next_match; } ++ ++ } else { /* byU32, byU16 */ ++ ++ U32 const h = LZ4_hashPosition(ip, tableType); ++ U32 const current = (U32)(ip-base); ++ U32 matchIndex = LZ4_getIndexOnHash(h, cctx->hashTable, tableType); ++ assert(matchIndex < current); ++ if (dictDirective == usingDictCtx) { ++ if (matchIndex < startIndex) { ++ /* there was no match, try the dictionary */ ++ assert(tableType == byU32); ++ matchIndex = LZ4_getIndexOnHash(h, dictCtx->hashTable, byU32); ++ match = dictBase + matchIndex; ++ lowLimit = dictionary; /* required for match length counter */ ++ matchIndex += dictDelta; ++ } else { ++ match = base + matchIndex; ++ lowLimit = (const BYTE*)source; /* required for match length counter */ ++ } ++ } else if (dictDirective==usingExtDict) { ++ if (matchIndex < startIndex) { ++ assert(dictBase); ++ match = dictBase + matchIndex; ++ lowLimit = dictionary; /* required for match length counter */ ++ } else { ++ match = base + matchIndex; ++ lowLimit = (const BYTE*)source; /* required for match length counter */ ++ } ++ } else { /* single memory segment */ ++ match = base + matchIndex; ++ } ++ LZ4_putIndexOnHash(current, h, cctx->hashTable, tableType); ++ assert(matchIndex < current); ++ if ( ((dictIssue==dictSmall) ? (matchIndex >= prefixIdxLimit) : 1) ++ && (((tableType==byU16) && (LZ4_DISTANCE_MAX == LZ4_DISTANCE_ABSOLUTE_MAX)) ? 1 : (matchIndex+LZ4_DISTANCE_MAX >= current)) ++ && (LZ4_read32(match) == LZ4_read32(ip)) ) { ++ token=op++; ++ *token=0; ++ if (maybe_extMem) offset = current - matchIndex; ++ DEBUGLOG(6, "seq.start:%i, literals=%u, match.start:%i", ++ (int)(anchor-(const BYTE*)source), 0, (int)(ip-(const BYTE*)source)); ++ goto _next_match; ++ } ++ } ++ ++ /* Prepare next loop */ ++ forwardH = LZ4_hashPosition(++ip, tableType); ++ ++ } ++ ++_last_literals: ++ /* Encode Last Literals */ ++ { size_t lastRun = (size_t)(iend - anchor); ++ if ( (outputDirective) && /* Check output buffer overflow */ ++ (op + lastRun + 1 + ((lastRun+255-RUN_MASK)/255) > olimit)) { ++ if (outputDirective == fillOutput) { ++ /* adapt lastRun to fill 'dst' */ ++ assert(olimit >= op); ++ lastRun = (size_t)(olimit-op) - 1/*token*/; ++ lastRun -= (lastRun + 256 - RUN_MASK) / 256; /*additional length tokens*/ ++ } else { ++ assert(outputDirective == limitedOutput); ++ return 0; /* cannot compress within `dst` budget. Stored indexes in hash table are nonetheless fine */ ++ } ++ } ++ DEBUGLOG(6, "Final literal run : %i literals", (int)lastRun); ++ if (lastRun >= RUN_MASK) { ++ size_t accumulator = lastRun - RUN_MASK; ++ *op++ = RUN_MASK << ML_BITS; ++ for(; accumulator >= 255 ; accumulator-=255) *op++ = 255; ++ *op++ = (BYTE) accumulator; ++ } else { ++ *op++ = (BYTE)(lastRun< 0); ++ DEBUGLOG(5, "LZ4_compress_generic: compressed %i bytes into %i bytes", inputSize, result); ++ return result; ++} ++ ++/** LZ4_compress_generic() : ++ * inlined, to ensure branches are decided at compilation time; ++ * takes care of src == (NULL, 0) ++ * and forward the rest to LZ4_compress_generic_validated */ ++LZ4_FORCE_INLINE int LZ4_compress_generic( ++ LZ4_stream_t_internal* const cctx, ++ const char* const src, ++ char* const dst, ++ const int srcSize, ++ int *inputConsumed, /* only written when outputDirective == fillOutput */ ++ const int dstCapacity, ++ const limitedOutput_directive outputDirective, ++ const tableType_t tableType, ++ const dict_directive dictDirective, ++ const dictIssue_directive dictIssue, ++ const int acceleration) ++{ ++ DEBUGLOG(5, "LZ4_compress_generic: srcSize=%i, dstCapacity=%i", ++ srcSize, dstCapacity); ++ ++ if ((U32)srcSize > (U32)LZ4_MAX_INPUT_SIZE) { return 0; } /* Unsupported srcSize, too large (or negative) */ ++ if (srcSize == 0) { /* src == NULL supported if srcSize == 0 */ ++ if (outputDirective != notLimited && dstCapacity <= 0) return 0; /* no output, can't write anything */ ++ DEBUGLOG(5, "Generating an empty block"); ++ assert(outputDirective == notLimited || dstCapacity >= 1); ++ assert(dst != NULL); ++ dst[0] = 0; ++ if (outputDirective == fillOutput) { ++ assert (inputConsumed != NULL); ++ *inputConsumed = 0; ++ } ++ return 1; ++ } ++ assert(src != NULL); ++ ++ return LZ4_compress_generic_validated(cctx, src, dst, srcSize, ++ inputConsumed, /* only written into if outputDirective == fillOutput */ ++ dstCapacity, outputDirective, ++ tableType, dictDirective, dictIssue, acceleration); ++} ++ ++ ++int LZ4_compress_fast_extState(void* state, const char* source, char* dest, int inputSize, int maxOutputSize, int acceleration) ++{ ++ LZ4_stream_t_internal* const ctx = & LZ4_initStream(state, sizeof(LZ4_stream_t)) -> internal_donotuse; ++ assert(ctx != NULL); ++ if (acceleration < 1) acceleration = LZ4_ACCELERATION_DEFAULT; ++ if (acceleration > LZ4_ACCELERATION_MAX) acceleration = LZ4_ACCELERATION_MAX; ++ if (maxOutputSize >= LZ4_compressBound(inputSize)) { ++ if (inputSize < LZ4_64Klimit) { ++ return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, 0, notLimited, byU16, noDict, noDictIssue, acceleration); ++ } else { ++ const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)source > LZ4_DISTANCE_MAX)) ? byPtr : byU32; ++ return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration); ++ } ++ } else { ++ if (inputSize < LZ4_64Klimit) { ++ return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration); ++ } else { ++ const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)source > LZ4_DISTANCE_MAX)) ? byPtr : byU32; ++ return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, noDict, noDictIssue, acceleration); ++ } ++ } ++} ++ ++/** ++ * LZ4_compress_fast_extState_fastReset() : ++ * A variant of LZ4_compress_fast_extState(). ++ * ++ * Using this variant avoids an expensive initialization step. It is only safe ++ * to call if the state buffer is known to be correctly initialized already ++ * (see comment in lz4.h on LZ4_resetStream_fast() for a definition of ++ * "correctly initialized"). ++ */ ++int LZ4_compress_fast_extState_fastReset(void* state, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration) ++{ ++ LZ4_stream_t_internal* const ctx = &((LZ4_stream_t*)state)->internal_donotuse; ++ if (acceleration < 1) acceleration = LZ4_ACCELERATION_DEFAULT; ++ if (acceleration > LZ4_ACCELERATION_MAX) acceleration = LZ4_ACCELERATION_MAX; ++ assert(ctx != NULL); ++ ++ if (dstCapacity >= LZ4_compressBound(srcSize)) { ++ if (srcSize < LZ4_64Klimit) { ++ const tableType_t tableType = byU16; ++ LZ4_prepareTable(ctx, srcSize, tableType); ++ if (ctx->currentOffset) { ++ return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, 0, notLimited, tableType, noDict, dictSmall, acceleration); ++ } else { ++ return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration); ++ } ++ } else { ++ const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)src > LZ4_DISTANCE_MAX)) ? byPtr : byU32; ++ LZ4_prepareTable(ctx, srcSize, tableType); ++ return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration); ++ } ++ } else { ++ if (srcSize < LZ4_64Klimit) { ++ const tableType_t tableType = byU16; ++ LZ4_prepareTable(ctx, srcSize, tableType); ++ if (ctx->currentOffset) { ++ return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, dstCapacity, limitedOutput, tableType, noDict, dictSmall, acceleration); ++ } else { ++ return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, dstCapacity, limitedOutput, tableType, noDict, noDictIssue, acceleration); ++ } ++ } else { ++ const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)src > LZ4_DISTANCE_MAX)) ? byPtr : byU32; ++ LZ4_prepareTable(ctx, srcSize, tableType); ++ return LZ4_compress_generic(ctx, src, dst, srcSize, NULL, dstCapacity, limitedOutput, tableType, noDict, noDictIssue, acceleration); ++ } ++ } ++} ++ ++ ++int LZ4_compress_fast(const char* src, char* dest, int srcSize, int dstCapacity, int acceleration) ++{ ++ int result; ++#if (LZ4_HEAPMODE) ++ LZ4_stream_t* const ctxPtr = (LZ4_stream_t*)ALLOC(sizeof(LZ4_stream_t)); /* malloc-calloc always properly aligned */ ++ if (ctxPtr == NULL) return 0; ++#else ++ LZ4_stream_t ctx; ++ LZ4_stream_t* const ctxPtr = &ctx; ++#endif ++ result = LZ4_compress_fast_extState(ctxPtr, src, dest, srcSize, dstCapacity, acceleration); ++ ++#if (LZ4_HEAPMODE) ++ FREEMEM(ctxPtr); ++#endif ++ return result; ++} ++ ++ ++int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity) ++{ ++ return LZ4_compress_fast(src, dst, srcSize, dstCapacity, 1); ++} ++ ++ ++/* Note!: This function leaves the stream in an unclean/broken state! ++ * It is not safe to subsequently use the same state with a _fastReset() or ++ * _continue() call without resetting it. */ ++static int LZ4_compress_destSize_extState_internal(LZ4_stream_t* state, const char* src, char* dst, int* srcSizePtr, int targetDstSize, int acceleration) ++{ ++ void* const s = LZ4_initStream(state, sizeof (*state)); ++ assert(s != NULL); (void)s; ++ ++ if (targetDstSize >= LZ4_compressBound(*srcSizePtr)) { /* compression success is guaranteed */ ++ return LZ4_compress_fast_extState(state, src, dst, *srcSizePtr, targetDstSize, acceleration); ++ } else { ++ if (*srcSizePtr < LZ4_64Klimit) { ++ return LZ4_compress_generic(&state->internal_donotuse, src, dst, *srcSizePtr, srcSizePtr, targetDstSize, fillOutput, byU16, noDict, noDictIssue, acceleration); ++ } else { ++ tableType_t const addrMode = ((sizeof(void*)==4) && ((uptrval)src > LZ4_DISTANCE_MAX)) ? byPtr : byU32; ++ return LZ4_compress_generic(&state->internal_donotuse, src, dst, *srcSizePtr, srcSizePtr, targetDstSize, fillOutput, addrMode, noDict, noDictIssue, acceleration); ++ } } ++} ++ ++int LZ4_compress_destSize_extState(void* state, const char* src, char* dst, int* srcSizePtr, int targetDstSize, int acceleration) ++{ ++ int const r = LZ4_compress_destSize_extState_internal((LZ4_stream_t*)state, src, dst, srcSizePtr, targetDstSize, acceleration); ++ /* clean the state on exit */ ++ LZ4_initStream(state, sizeof (LZ4_stream_t)); ++ return r; ++} ++ ++ ++int LZ4_compress_destSize(const char* src, char* dst, int* srcSizePtr, int targetDstSize) ++{ ++#if (LZ4_HEAPMODE) ++ LZ4_stream_t* const ctx = (LZ4_stream_t*)ALLOC(sizeof(LZ4_stream_t)); /* malloc-calloc always properly aligned */ ++ if (ctx == NULL) return 0; ++#else ++ LZ4_stream_t ctxBody; ++ LZ4_stream_t* const ctx = &ctxBody; ++#endif ++ ++ int result = LZ4_compress_destSize_extState_internal(ctx, src, dst, srcSizePtr, targetDstSize, 1); ++ ++#if (LZ4_HEAPMODE) ++ FREEMEM(ctx); ++#endif ++ return result; ++} ++ ++ ++ ++/*-****************************** ++* Streaming functions ++********************************/ ++ ++#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++LZ4_stream_t* LZ4_createStream(void) ++{ ++ LZ4_stream_t* const lz4s = (LZ4_stream_t*)ALLOC(sizeof(LZ4_stream_t)); ++ LZ4_STATIC_ASSERT(sizeof(LZ4_stream_t) >= sizeof(LZ4_stream_t_internal)); ++ DEBUGLOG(4, "LZ4_createStream %p", (void*)lz4s); ++ if (lz4s == NULL) return NULL; ++ LZ4_initStream(lz4s, sizeof(*lz4s)); ++ return lz4s; ++} ++#endif ++ ++static size_t LZ4_stream_t_alignment(void) ++{ ++#if LZ4_ALIGN_TEST ++ typedef struct { char c; LZ4_stream_t t; } t_a; ++ return sizeof(t_a) - sizeof(LZ4_stream_t); ++#else ++ return 1; /* effectively disabled */ ++#endif ++} ++ ++LZ4_stream_t* LZ4_initStream (void* buffer, size_t size) ++{ ++ DEBUGLOG(5, "LZ4_initStream"); ++ if (buffer == NULL) { return NULL; } ++ if (size < sizeof(LZ4_stream_t)) { return NULL; } ++ if (!LZ4_isAligned(buffer, LZ4_stream_t_alignment())) return NULL; ++ MEM_INIT(buffer, 0, sizeof(LZ4_stream_t_internal)); ++ return (LZ4_stream_t*)buffer; ++} ++ ++/* resetStream is now deprecated, ++ * prefer initStream() which is more general */ ++void LZ4_resetStream (LZ4_stream_t* LZ4_stream) ++{ ++ DEBUGLOG(5, "LZ4_resetStream (ctx:%p)", (void*)LZ4_stream); ++ MEM_INIT(LZ4_stream, 0, sizeof(LZ4_stream_t_internal)); ++} ++ ++void LZ4_resetStream_fast(LZ4_stream_t* ctx) { ++ LZ4_prepareTable(&(ctx->internal_donotuse), 0, byU32); ++} ++ ++#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++int LZ4_freeStream (LZ4_stream_t* LZ4_stream) ++{ ++ if (!LZ4_stream) return 0; /* support free on NULL */ ++ DEBUGLOG(5, "LZ4_freeStream %p", (void*)LZ4_stream); ++ FREEMEM(LZ4_stream); ++ return (0); ++} ++#endif ++ ++ ++typedef enum { _ld_fast, _ld_slow } LoadDict_mode_e; ++#define HASH_UNIT sizeof(reg_t) ++static int LZ4_loadDict_internal(LZ4_stream_t* LZ4_dict, ++ const char* dictionary, int dictSize, ++ LoadDict_mode_e _ld) ++{ ++ LZ4_stream_t_internal* const dict = &LZ4_dict->internal_donotuse; ++ const tableType_t tableType = byU32; ++ const BYTE* p = (const BYTE*)dictionary; ++ const BYTE* const dictEnd = p + dictSize; ++ U32 idx32; ++ ++ DEBUGLOG(4, "LZ4_loadDict (%i bytes from %p into %p)", dictSize, (void*)dictionary, (void*)LZ4_dict); ++ ++ /* It's necessary to reset the context, ++ * and not just continue it with prepareTable() ++ * to avoid any risk of generating overflowing matchIndex ++ * when compressing using this dictionary */ ++ LZ4_resetStream(LZ4_dict); ++ ++ /* We always increment the offset by 64 KB, since, if the dict is longer, ++ * we truncate it to the last 64k, and if it's shorter, we still want to ++ * advance by a whole window length so we can provide the guarantee that ++ * there are only valid offsets in the window, which allows an optimization ++ * in LZ4_compress_fast_continue() where it uses noDictIssue even when the ++ * dictionary isn't a full 64k. */ ++ dict->currentOffset += 64 KB; ++ ++ if (dictSize < (int)HASH_UNIT) { ++ return 0; ++ } ++ ++ if ((dictEnd - p) > 64 KB) p = dictEnd - 64 KB; ++ dict->dictionary = p; ++ dict->dictSize = (U32)(dictEnd - p); ++ dict->tableType = (U32)tableType; ++ idx32 = dict->currentOffset - dict->dictSize; ++ ++ while (p <= dictEnd-HASH_UNIT) { ++ U32 const h = LZ4_hashPosition(p, tableType); ++ /* Note: overwriting => favors positions end of dictionary */ ++ LZ4_putIndexOnHash(idx32, h, dict->hashTable, tableType); ++ p+=3; idx32+=3; ++ } ++ ++ if (_ld == _ld_slow) { ++ /* Fill hash table with additional references, to improve compression capability */ ++ p = dict->dictionary; ++ idx32 = dict->currentOffset - dict->dictSize; ++ while (p <= dictEnd-HASH_UNIT) { ++ U32 const h = LZ4_hashPosition(p, tableType); ++ U32 const limit = dict->currentOffset - 64 KB; ++ if (LZ4_getIndexOnHash(h, dict->hashTable, tableType) <= limit) { ++ /* Note: not overwriting => favors positions beginning of dictionary */ ++ LZ4_putIndexOnHash(idx32, h, dict->hashTable, tableType); ++ } ++ p++; idx32++; ++ } ++ } ++ ++ return (int)dict->dictSize; ++} ++ ++int LZ4_loadDict(LZ4_stream_t* LZ4_dict, const char* dictionary, int dictSize) ++{ ++ return LZ4_loadDict_internal(LZ4_dict, dictionary, dictSize, _ld_fast); ++} ++ ++int LZ4_loadDictSlow(LZ4_stream_t* LZ4_dict, const char* dictionary, int dictSize) ++{ ++ return LZ4_loadDict_internal(LZ4_dict, dictionary, dictSize, _ld_slow); ++} ++ ++void LZ4_attach_dictionary(LZ4_stream_t* workingStream, const LZ4_stream_t* dictionaryStream) ++{ ++ const LZ4_stream_t_internal* dictCtx = (dictionaryStream == NULL) ? NULL : ++ &(dictionaryStream->internal_donotuse); ++ ++ DEBUGLOG(4, "LZ4_attach_dictionary (%p, %p, size %u)", ++ (void*)workingStream, (void*)dictionaryStream, ++ dictCtx != NULL ? dictCtx->dictSize : 0); ++ ++ if (dictCtx != NULL) { ++ /* If the current offset is zero, we will never look in the ++ * external dictionary context, since there is no value a table ++ * entry can take that indicate a miss. In that case, we need ++ * to bump the offset to something non-zero. ++ */ ++ if (workingStream->internal_donotuse.currentOffset == 0) { ++ workingStream->internal_donotuse.currentOffset = 64 KB; ++ } ++ ++ /* Don't actually attach an empty dictionary. ++ */ ++ if (dictCtx->dictSize == 0) { ++ dictCtx = NULL; ++ } ++ } ++ workingStream->internal_donotuse.dictCtx = dictCtx; ++} ++ ++ ++static void LZ4_renormDictT(LZ4_stream_t_internal* LZ4_dict, int nextSize) ++{ ++ assert(nextSize >= 0); ++ if (LZ4_dict->currentOffset + (unsigned)nextSize > 0x80000000) { /* potential ptrdiff_t overflow (32-bits mode) */ ++ /* rescale hash table */ ++ U32 const delta = LZ4_dict->currentOffset - 64 KB; ++ const BYTE* dictEnd = LZ4_dict->dictionary + LZ4_dict->dictSize; ++ int i; ++ DEBUGLOG(4, "LZ4_renormDictT"); ++ for (i=0; ihashTable[i] < delta) LZ4_dict->hashTable[i]=0; ++ else LZ4_dict->hashTable[i] -= delta; ++ } ++ LZ4_dict->currentOffset = 64 KB; ++ if (LZ4_dict->dictSize > 64 KB) LZ4_dict->dictSize = 64 KB; ++ LZ4_dict->dictionary = dictEnd - LZ4_dict->dictSize; ++ } ++} ++ ++ ++int LZ4_compress_fast_continue (LZ4_stream_t* LZ4_stream, ++ const char* source, char* dest, ++ int inputSize, int maxOutputSize, ++ int acceleration) ++{ ++ const tableType_t tableType = byU32; ++ LZ4_stream_t_internal* const streamPtr = &LZ4_stream->internal_donotuse; ++ const char* dictEnd = streamPtr->dictSize ? (const char*)streamPtr->dictionary + streamPtr->dictSize : NULL; ++ ++ DEBUGLOG(5, "LZ4_compress_fast_continue (inputSize=%i, dictSize=%u)", inputSize, streamPtr->dictSize); ++ ++ LZ4_renormDictT(streamPtr, inputSize); /* fix index overflow */ ++ if (acceleration < 1) acceleration = LZ4_ACCELERATION_DEFAULT; ++ if (acceleration > LZ4_ACCELERATION_MAX) acceleration = LZ4_ACCELERATION_MAX; ++ ++ /* invalidate tiny dictionaries */ ++ if ( (streamPtr->dictSize < 4) /* tiny dictionary : not enough for a hash */ ++ && (dictEnd != source) /* prefix mode */ ++ && (inputSize > 0) /* tolerance : don't lose history, in case next invocation would use prefix mode */ ++ && (streamPtr->dictCtx == NULL) /* usingDictCtx */ ++ ) { ++ DEBUGLOG(5, "LZ4_compress_fast_continue: dictSize(%u) at addr:%p is too small", streamPtr->dictSize, (void*)streamPtr->dictionary); ++ /* remove dictionary existence from history, to employ faster prefix mode */ ++ streamPtr->dictSize = 0; ++ streamPtr->dictionary = (const BYTE*)source; ++ dictEnd = source; ++ } ++ ++ /* Check overlapping input/dictionary space */ ++ { const char* const sourceEnd = source + inputSize; ++ if ((sourceEnd > (const char*)streamPtr->dictionary) && (sourceEnd < dictEnd)) { ++ streamPtr->dictSize = (U32)(dictEnd - sourceEnd); ++ if (streamPtr->dictSize > 64 KB) streamPtr->dictSize = 64 KB; ++ if (streamPtr->dictSize < 4) streamPtr->dictSize = 0; ++ streamPtr->dictionary = (const BYTE*)dictEnd - streamPtr->dictSize; ++ } ++ } ++ ++ /* prefix mode : source data follows dictionary */ ++ if (dictEnd == source) { ++ if ((streamPtr->dictSize < 64 KB) && (streamPtr->dictSize < streamPtr->currentOffset)) ++ return LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, withPrefix64k, dictSmall, acceleration); ++ else ++ return LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, withPrefix64k, noDictIssue, acceleration); ++ } ++ ++ /* external dictionary mode */ ++ { int result; ++ if (streamPtr->dictCtx) { ++ /* We depend here on the fact that dictCtx'es (produced by ++ * LZ4_loadDict) guarantee that their tables contain no references ++ * to offsets between dictCtx->currentOffset - 64 KB and ++ * dictCtx->currentOffset - dictCtx->dictSize. This makes it safe ++ * to use noDictIssue even when the dict isn't a full 64 KB. ++ */ ++ if (inputSize > 4 KB) { ++ /* For compressing large blobs, it is faster to pay the setup ++ * cost to copy the dictionary's tables into the active context, ++ * so that the compression loop is only looking into one table. ++ */ ++ LZ4_memcpy(streamPtr, streamPtr->dictCtx, sizeof(*streamPtr)); ++ result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingExtDict, noDictIssue, acceleration); ++ } else { ++ result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingDictCtx, noDictIssue, acceleration); ++ } ++ } else { /* small data <= 4 KB */ ++ if ((streamPtr->dictSize < 64 KB) && (streamPtr->dictSize < streamPtr->currentOffset)) { ++ result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingExtDict, dictSmall, acceleration); ++ } else { ++ result = LZ4_compress_generic(streamPtr, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, usingExtDict, noDictIssue, acceleration); ++ } ++ } ++ streamPtr->dictionary = (const BYTE*)source; ++ streamPtr->dictSize = (U32)inputSize; ++ return result; ++ } ++} ++ ++ ++/* Hidden debug function, to force-test external dictionary mode */ ++int LZ4_compress_forceExtDict (LZ4_stream_t* LZ4_dict, const char* source, char* dest, int srcSize) ++{ ++ LZ4_stream_t_internal* const streamPtr = &LZ4_dict->internal_donotuse; ++ int result; ++ ++ LZ4_renormDictT(streamPtr, srcSize); ++ ++ if ((streamPtr->dictSize < 64 KB) && (streamPtr->dictSize < streamPtr->currentOffset)) { ++ result = LZ4_compress_generic(streamPtr, source, dest, srcSize, NULL, 0, notLimited, byU32, usingExtDict, dictSmall, 1); ++ } else { ++ result = LZ4_compress_generic(streamPtr, source, dest, srcSize, NULL, 0, notLimited, byU32, usingExtDict, noDictIssue, 1); ++ } ++ ++ streamPtr->dictionary = (const BYTE*)source; ++ streamPtr->dictSize = (U32)srcSize; ++ ++ return result; ++} ++ ++ ++/*! LZ4_saveDict() : ++ * If previously compressed data block is not guaranteed to remain available at its memory location, ++ * save it into a safer place (char* safeBuffer). ++ * Note : no need to call LZ4_loadDict() afterwards, dictionary is immediately usable, ++ * one can therefore call LZ4_compress_fast_continue() right after. ++ * @return : saved dictionary size in bytes (necessarily <= dictSize), or 0 if error. ++ */ ++int LZ4_saveDict (LZ4_stream_t* LZ4_dict, char* safeBuffer, int dictSize) ++{ ++ LZ4_stream_t_internal* const dict = &LZ4_dict->internal_donotuse; ++ ++ DEBUGLOG(5, "LZ4_saveDict : dictSize=%i, safeBuffer=%p", dictSize, (void*)safeBuffer); ++ ++ if ((U32)dictSize > 64 KB) { dictSize = 64 KB; } /* useless to define a dictionary > 64 KB */ ++ if ((U32)dictSize > dict->dictSize) { dictSize = (int)dict->dictSize; } ++ ++ if (safeBuffer == NULL) assert(dictSize == 0); ++ if (dictSize > 0) { ++ const BYTE* const previousDictEnd = dict->dictionary + dict->dictSize; ++ assert(dict->dictionary); ++ LZ4_memmove(safeBuffer, previousDictEnd - dictSize, (size_t)dictSize); ++ } ++ ++ dict->dictionary = (const BYTE*)safeBuffer; ++ dict->dictSize = (U32)dictSize; ++ ++ return dictSize; ++} ++ ++ ++ ++/*-******************************* ++ * Decompression functions ++ ********************************/ ++ ++typedef enum { decode_full_block = 0, partial_decode = 1 } earlyEnd_directive; ++ ++#undef MIN ++#define MIN(a,b) ( (a) < (b) ? (a) : (b) ) ++ ++ ++/* variant for decompress_unsafe() ++ * does not know end of input ++ * presumes input is well formed ++ * note : will consume at least one byte */ ++static size_t read_long_length_no_check(const BYTE** pp) ++{ ++ size_t b, l = 0; ++ do { b = **pp; (*pp)++; l += b; } while (b==255); ++ DEBUGLOG(6, "read_long_length_no_check: +length=%zu using %zu input bytes", l, l/255 + 1) ++ return l; ++} ++ ++/* core decoder variant for LZ4_decompress_fast*() ++ * for legacy support only : these entry points are deprecated. ++ * - Presumes input is correctly formed (no defense vs malformed inputs) ++ * - Does not know input size (presume input buffer is "large enough") ++ * - Decompress a full block (only) ++ * @return : nb of bytes read from input. ++ * Note : this variant is not optimized for speed, just for maintenance. ++ * the goal is to remove support of decompress_fast*() variants by v2.0 ++**/ ++LZ4_FORCE_INLINE int ++LZ4_decompress_unsafe_generic( ++ const BYTE* const istart, ++ BYTE* const ostart, ++ int decompressedSize, ++ ++ size_t prefixSize, ++ const BYTE* const dictStart, /* only if dict==usingExtDict */ ++ const size_t dictSize /* note: =0 if dictStart==NULL */ ++ ) ++{ ++ const BYTE* ip = istart; ++ BYTE* op = (BYTE*)ostart; ++ BYTE* const oend = ostart + decompressedSize; ++ const BYTE* const prefixStart = ostart - prefixSize; ++ ++ DEBUGLOG(5, "LZ4_decompress_unsafe_generic"); ++ if (dictStart == NULL) assert(dictSize == 0); ++ ++ while (1) { ++ /* start new sequence */ ++ unsigned token = *ip++; ++ ++ /* literals */ ++ { size_t ll = token >> ML_BITS; ++ if (ll==15) { ++ /* long literal length */ ++ ll += read_long_length_no_check(&ip); ++ } ++ if ((size_t)(oend-op) < ll) return -1; /* output buffer overflow */ ++ LZ4_memmove(op, ip, ll); /* support in-place decompression */ ++ op += ll; ++ ip += ll; ++ if ((size_t)(oend-op) < MFLIMIT) { ++ if (op==oend) break; /* end of block */ ++ DEBUGLOG(5, "invalid: literals end at distance %zi from end of block", oend-op); ++ /* incorrect end of block : ++ * last match must start at least MFLIMIT==12 bytes before end of output block */ ++ return -1; ++ } } ++ ++ /* match */ ++ { size_t ml = token & 15; ++ size_t const offset = LZ4_readLE16(ip); ++ ip+=2; ++ ++ if (ml==15) { ++ /* long literal length */ ++ ml += read_long_length_no_check(&ip); ++ } ++ ml += MINMATCH; ++ ++ if ((size_t)(oend-op) < ml) return -1; /* output buffer overflow */ ++ ++ { const BYTE* match = op - offset; ++ ++ /* out of range */ ++ if (offset > (size_t)(op - prefixStart) + dictSize) { ++ DEBUGLOG(6, "offset out of range"); ++ return -1; ++ } ++ ++ /* check special case : extDict */ ++ if (offset > (size_t)(op - prefixStart)) { ++ /* extDict scenario */ ++ const BYTE* const dictEnd = dictStart + dictSize; ++ const BYTE* extMatch = dictEnd - (offset - (size_t)(op-prefixStart)); ++ size_t const extml = (size_t)(dictEnd - extMatch); ++ if (extml > ml) { ++ /* match entirely within extDict */ ++ LZ4_memmove(op, extMatch, ml); ++ op += ml; ++ ml = 0; ++ } else { ++ /* match split between extDict & prefix */ ++ LZ4_memmove(op, extMatch, extml); ++ op += extml; ++ ml -= extml; ++ } ++ match = prefixStart; ++ } ++ ++ /* match copy - slow variant, supporting overlap copy */ ++ { size_t u; ++ for (u=0; u= ipmax before start of loop. Returns initial_error if so. ++ * @error (output) - error code. Must be set to 0 before call. ++**/ ++typedef size_t Rvl_t; ++static const Rvl_t rvl_error = (Rvl_t)(-1); ++LZ4_FORCE_INLINE Rvl_t ++read_variable_length(const BYTE** ip, const BYTE* ilimit, ++ int initial_check) ++{ ++ Rvl_t s, length = 0; ++ assert(ip != NULL); ++ assert(*ip != NULL); ++ assert(ilimit != NULL); ++ if (initial_check && unlikely((*ip) >= ilimit)) { /* read limit reached */ ++ return rvl_error; ++ } ++ s = **ip; ++ (*ip)++; ++ length += s; ++ if (unlikely((*ip) > ilimit)) { /* read limit reached */ ++ return rvl_error; ++ } ++ /* accumulator overflow detection (32-bit mode only) */ ++ if ((sizeof(length) < 8) && unlikely(length > ((Rvl_t)(-1)/2)) ) { ++ return rvl_error; ++ } ++ if (likely(s != 255)) return length; ++ do { ++ s = **ip; ++ (*ip)++; ++ length += s; ++ if (unlikely((*ip) > ilimit)) { /* read limit reached */ ++ return rvl_error; ++ } ++ /* accumulator overflow detection (32-bit mode only) */ ++ if ((sizeof(length) < 8) && unlikely(length > ((Rvl_t)(-1)/2)) ) { ++ return rvl_error; ++ } ++ } while (s == 255); ++ ++ return length; ++} ++ ++/*! LZ4_decompress_generic() : ++ * This generic decompression function covers all use cases. ++ * It shall be instantiated several times, using different sets of directives. ++ * Note that it is important for performance that this function really get inlined, ++ * in order to remove useless branches during compilation optimization. ++ */ ++LZ4_FORCE_INLINE int ++LZ4_decompress_generic( ++ const char* const src, ++ char* const dst, ++ int srcSize, ++ int outputSize, /* If endOnInput==endOnInputSize, this value is `dstCapacity` */ ++ ++ earlyEnd_directive partialDecoding, /* full, partial */ ++ dict_directive dict, /* noDict, withPrefix64k, usingExtDict */ ++ const BYTE* const lowPrefix, /* always <= dst, == dst when no prefix */ ++ const BYTE* const dictStart, /* only if dict==usingExtDict */ ++ const size_t dictSize /* note : = 0 if noDict */ ++ ) ++{ ++ if ((src == NULL) || (outputSize < 0)) { return -1; } ++ ++ { const BYTE* ip = (const BYTE*) src; ++ const BYTE* const iend = ip + srcSize; ++ ++ BYTE* op = (BYTE*) dst; ++ BYTE* const oend = op + outputSize; ++ BYTE* cpy; ++ ++ const BYTE* const dictEnd = (dictStart == NULL) ? NULL : dictStart + dictSize; ++ ++ const int checkOffset = (dictSize < (int)(64 KB)); ++ ++ ++ /* Set up the "end" pointers for the shortcut. */ ++ const BYTE* const shortiend = iend - 14 /*maxLL*/ - 2 /*offset*/; ++ const BYTE* const shortoend = oend - 14 /*maxLL*/ - 18 /*maxML*/; ++ ++ const BYTE* match; ++ size_t offset; ++ unsigned token; ++ size_t length; ++ ++ ++ DEBUGLOG(5, "LZ4_decompress_generic (srcSize:%i, dstSize:%i)", srcSize, outputSize); ++ ++ /* Special cases */ ++ assert(lowPrefix <= op); ++ if (unlikely(outputSize==0)) { ++ /* Empty output buffer */ ++ if (partialDecoding) return 0; ++ return ((srcSize==1) && (*ip==0)) ? 0 : -1; ++ } ++ if (unlikely(srcSize==0)) { return -1; } ++ ++ /* LZ4_FAST_DEC_LOOP: ++ * designed for modern OoO performance cpus, ++ * where copying reliably 32-bytes is preferable to an unpredictable branch. ++ * note : fast loop may show a regression for some client arm chips. */ ++#if LZ4_FAST_DEC_LOOP ++ if ((oend - op) < FASTLOOP_SAFE_DISTANCE) { ++ DEBUGLOG(6, "move to safe decode loop"); ++ goto safe_decode; ++ } ++ ++ /* Fast loop : decode sequences as long as output < oend-FASTLOOP_SAFE_DISTANCE */ ++ DEBUGLOG(6, "using fast decode loop"); ++ while (1) { ++ /* Main fastloop assertion: We can always wildcopy FASTLOOP_SAFE_DISTANCE */ ++ assert(oend - op >= FASTLOOP_SAFE_DISTANCE); ++ assert(ip < iend); ++ token = *ip++; ++ length = token >> ML_BITS; /* literal length */ ++ DEBUGLOG(7, "blockPos%6u: litLength token = %u", (unsigned)(op-(BYTE*)dst), (unsigned)length); ++ ++ /* decode literal length */ ++ if (length == RUN_MASK) { ++ size_t const addl = read_variable_length(&ip, iend-RUN_MASK, 1); ++ if (addl == rvl_error) { ++ DEBUGLOG(6, "error reading long literal length"); ++ goto _output_error; ++ } ++ length += addl; ++ if (unlikely((uptrval)(op)+length<(uptrval)(op))) { goto _output_error; } /* overflow detection */ ++ if (unlikely((uptrval)(ip)+length<(uptrval)(ip))) { goto _output_error; } /* overflow detection */ ++ ++ /* copy literals */ ++ LZ4_STATIC_ASSERT(MFLIMIT >= WILDCOPYLENGTH); ++ if ((op+length>oend-32) || (ip+length>iend-32)) { goto safe_literal_copy; } ++ LZ4_wildCopy32(op, ip, op+length); ++ ip += length; op += length; ++ } else if (ip <= iend-(16 + 1/*max lit + offset + nextToken*/)) { ++ /* We don't need to check oend, since we check it once for each loop below */ ++ DEBUGLOG(7, "copy %u bytes in a 16-bytes stripe", (unsigned)length); ++ /* Literals can only be <= 14, but hope compilers optimize better when copy by a register size */ ++ LZ4_memcpy(op, ip, 16); ++ ip += length; op += length; ++ } else { ++ goto safe_literal_copy; ++ } ++ ++ /* get offset */ ++ offset = LZ4_readLE16(ip); ip+=2; ++ DEBUGLOG(6, "blockPos%6u: offset = %u", (unsigned)(op-(BYTE*)dst), (unsigned)offset); ++ match = op - offset; ++ assert(match <= op); /* overflow check */ ++ ++ /* get matchlength */ ++ length = token & ML_MASK; ++ DEBUGLOG(7, " match length token = %u (len==%u)", (unsigned)length, (unsigned)length+MINMATCH); ++ ++ if (length == ML_MASK) { ++ size_t const addl = read_variable_length(&ip, iend - LASTLITERALS + 1, 0); ++ if (addl == rvl_error) { ++ DEBUGLOG(5, "error reading long match length"); ++ goto _output_error; ++ } ++ length += addl; ++ length += MINMATCH; ++ DEBUGLOG(7, " long match length == %u", (unsigned)length); ++ if (unlikely((uptrval)(op)+length<(uptrval)op)) { goto _output_error; } /* overflow detection */ ++ if (op + length >= oend - FASTLOOP_SAFE_DISTANCE) { ++ goto safe_match_copy; ++ } ++ } else { ++ length += MINMATCH; ++ if (op + length >= oend - FASTLOOP_SAFE_DISTANCE) { ++ DEBUGLOG(7, "moving to safe_match_copy (ml==%u)", (unsigned)length); ++ goto safe_match_copy; ++ } ++ ++ /* Fastpath check: skip LZ4_wildCopy32 when true */ ++ if ((dict == withPrefix64k) || (match >= lowPrefix)) { ++ if (offset >= 8) { ++ assert(match >= lowPrefix); ++ assert(match <= op); ++ assert(op + 18 <= oend); ++ ++ LZ4_memcpy(op, match, 8); ++ LZ4_memcpy(op+8, match+8, 8); ++ LZ4_memcpy(op+16, match+16, 2); ++ op += length; ++ continue; ++ } } } ++ ++ if ( checkOffset && (unlikely(match + dictSize < lowPrefix)) ) { ++ DEBUGLOG(5, "Error : pos=%zi, offset=%zi => outside buffers", op-lowPrefix, op-match); ++ goto _output_error; ++ } ++ /* match starting within external dictionary */ ++ if ((dict==usingExtDict) && (match < lowPrefix)) { ++ assert(dictEnd != NULL); ++ if (unlikely(op+length > oend-LASTLITERALS)) { ++ if (partialDecoding) { ++ DEBUGLOG(7, "partialDecoding: dictionary match, close to dstEnd"); ++ length = MIN(length, (size_t)(oend-op)); ++ } else { ++ DEBUGLOG(6, "end-of-block condition violated") ++ goto _output_error; ++ } } ++ ++ if (length <= (size_t)(lowPrefix-match)) { ++ /* match fits entirely within external dictionary : just copy */ ++ LZ4_memmove(op, dictEnd - (lowPrefix-match), length); ++ op += length; ++ } else { ++ /* match stretches into both external dictionary and current block */ ++ size_t const copySize = (size_t)(lowPrefix - match); ++ size_t const restSize = length - copySize; ++ LZ4_memcpy(op, dictEnd - copySize, copySize); ++ op += copySize; ++ if (restSize > (size_t)(op - lowPrefix)) { /* overlap copy */ ++ BYTE* const endOfMatch = op + restSize; ++ const BYTE* copyFrom = lowPrefix; ++ while (op < endOfMatch) { *op++ = *copyFrom++; } ++ } else { ++ LZ4_memcpy(op, lowPrefix, restSize); ++ op += restSize; ++ } } ++ continue; ++ } ++ ++ /* copy match within block */ ++ cpy = op + length; ++ ++ assert((op <= oend) && (oend-op >= 32)); ++ if (unlikely(offset<16)) { ++ LZ4_memcpy_using_offset(op, match, cpy, offset); ++ } else { ++ LZ4_wildCopy32(op, match, cpy); ++ } ++ ++ op = cpy; /* wildcopy correction */ ++ } ++ safe_decode: ++#endif ++ ++ /* Main Loop : decode remaining sequences where output < FASTLOOP_SAFE_DISTANCE */ ++ DEBUGLOG(6, "using safe decode loop"); ++ while (1) { ++ assert(ip < iend); ++ token = *ip++; ++ length = token >> ML_BITS; /* literal length */ ++ DEBUGLOG(7, "blockPos%6u: litLength token = %u", (unsigned)(op-(BYTE*)dst), (unsigned)length); ++ ++ /* A two-stage shortcut for the most common case: ++ * 1) If the literal length is 0..14, and there is enough space, ++ * enter the shortcut and copy 16 bytes on behalf of the literals ++ * (in the fast mode, only 8 bytes can be safely copied this way). ++ * 2) Further if the match length is 4..18, copy 18 bytes in a similar ++ * manner; but we ensure that there's enough space in the output for ++ * those 18 bytes earlier, upon entering the shortcut (in other words, ++ * there is a combined check for both stages). ++ */ ++ if ( (length != RUN_MASK) ++ /* strictly "less than" on input, to re-enter the loop with at least one byte */ ++ && likely((ip < shortiend) & (op <= shortoend)) ) { ++ /* Copy the literals */ ++ LZ4_memcpy(op, ip, 16); ++ op += length; ip += length; ++ ++ /* The second stage: prepare for match copying, decode full info. ++ * If it doesn't work out, the info won't be wasted. */ ++ length = token & ML_MASK; /* match length */ ++ DEBUGLOG(7, "blockPos%6u: matchLength token = %u (len=%u)", (unsigned)(op-(BYTE*)dst), (unsigned)length, (unsigned)length + 4); ++ offset = LZ4_readLE16(ip); ip += 2; ++ match = op - offset; ++ assert(match <= op); /* check overflow */ ++ ++ /* Do not deal with overlapping matches. */ ++ if ( (length != ML_MASK) ++ && (offset >= 8) ++ && (dict==withPrefix64k || match >= lowPrefix) ) { ++ /* Copy the match. */ ++ LZ4_memcpy(op + 0, match + 0, 8); ++ LZ4_memcpy(op + 8, match + 8, 8); ++ LZ4_memcpy(op +16, match +16, 2); ++ op += length + MINMATCH; ++ /* Both stages worked, load the next token. */ ++ continue; ++ } ++ ++ /* The second stage didn't work out, but the info is ready. ++ * Propel it right to the point of match copying. */ ++ goto _copy_match; ++ } ++ ++ /* decode literal length */ ++ if (length == RUN_MASK) { ++ size_t const addl = read_variable_length(&ip, iend-RUN_MASK, 1); ++ if (addl == rvl_error) { goto _output_error; } ++ length += addl; ++ if (unlikely((uptrval)(op)+length<(uptrval)(op))) { goto _output_error; } /* overflow detection */ ++ if (unlikely((uptrval)(ip)+length<(uptrval)(ip))) { goto _output_error; } /* overflow detection */ ++ } ++ ++#if LZ4_FAST_DEC_LOOP ++ safe_literal_copy: ++#endif ++ /* copy literals */ ++ cpy = op+length; ++ ++ LZ4_STATIC_ASSERT(MFLIMIT >= WILDCOPYLENGTH); ++ if ((cpy>oend-MFLIMIT) || (ip+length>iend-(2+1+LASTLITERALS))) { ++ /* We've either hit the input parsing restriction or the output parsing restriction. ++ * In the normal scenario, decoding a full block, it must be the last sequence, ++ * otherwise it's an error (invalid input or dimensions). ++ * In partialDecoding scenario, it's necessary to ensure there is no buffer overflow. ++ */ ++ if (partialDecoding) { ++ /* Since we are partial decoding we may be in this block because of the output parsing ++ * restriction, which is not valid since the output buffer is allowed to be undersized. ++ */ ++ DEBUGLOG(7, "partialDecoding: copying literals, close to input or output end") ++ DEBUGLOG(7, "partialDecoding: literal length = %u", (unsigned)length); ++ DEBUGLOG(7, "partialDecoding: remaining space in dstBuffer : %i", (int)(oend - op)); ++ DEBUGLOG(7, "partialDecoding: remaining space in srcBuffer : %i", (int)(iend - ip)); ++ /* Finishing in the middle of a literals segment, ++ * due to lack of input. ++ */ ++ if (ip+length > iend) { ++ length = (size_t)(iend-ip); ++ cpy = op + length; ++ } ++ /* Finishing in the middle of a literals segment, ++ * due to lack of output space. ++ */ ++ if (cpy > oend) { ++ cpy = oend; ++ assert(op<=oend); ++ length = (size_t)(oend-op); ++ } ++ } else { ++ /* We must be on the last sequence (or invalid) because of the parsing limitations ++ * so check that we exactly consume the input and don't overrun the output buffer. ++ */ ++ if ((ip+length != iend) || (cpy > oend)) { ++ DEBUGLOG(5, "should have been last run of literals") ++ DEBUGLOG(5, "ip(%p) + length(%i) = %p != iend (%p)", (void*)ip, (int)length, (void*)(ip+length), (void*)iend); ++ DEBUGLOG(5, "or cpy(%p) > (oend-MFLIMIT)(%p)", (void*)cpy, (void*)(oend-MFLIMIT)); ++ DEBUGLOG(5, "after writing %u bytes / %i bytes available", (unsigned)(op-(BYTE*)dst), outputSize); ++ goto _output_error; ++ } ++ } ++ LZ4_memmove(op, ip, length); /* supports overlapping memory regions, for in-place decompression scenarios */ ++ ip += length; ++ op += length; ++ /* Necessarily EOF when !partialDecoding. ++ * When partialDecoding, it is EOF if we've either ++ * filled the output buffer or ++ * can't proceed with reading an offset for following match. ++ */ ++ if (!partialDecoding || (cpy == oend) || (ip >= (iend-2))) { ++ break; ++ } ++ } else { ++ LZ4_wildCopy8(op, ip, cpy); /* can overwrite up to 8 bytes beyond cpy */ ++ ip += length; op = cpy; ++ } ++ ++ /* get offset */ ++ offset = LZ4_readLE16(ip); ip+=2; ++ match = op - offset; ++ ++ /* get matchlength */ ++ length = token & ML_MASK; ++ DEBUGLOG(7, "blockPos%6u: matchLength token = %u", (unsigned)(op-(BYTE*)dst), (unsigned)length); ++ ++ _copy_match: ++ if (length == ML_MASK) { ++ size_t const addl = read_variable_length(&ip, iend - LASTLITERALS + 1, 0); ++ if (addl == rvl_error) { goto _output_error; } ++ length += addl; ++ if (unlikely((uptrval)(op)+length<(uptrval)op)) goto _output_error; /* overflow detection */ ++ } ++ length += MINMATCH; ++ ++#if LZ4_FAST_DEC_LOOP ++ safe_match_copy: ++#endif ++ if ((checkOffset) && (unlikely(match + dictSize < lowPrefix))) goto _output_error; /* Error : offset outside buffers */ ++ /* match starting within external dictionary */ ++ if ((dict==usingExtDict) && (match < lowPrefix)) { ++ assert(dictEnd != NULL); ++ if (unlikely(op+length > oend-LASTLITERALS)) { ++ if (partialDecoding) length = MIN(length, (size_t)(oend-op)); ++ else goto _output_error; /* doesn't respect parsing restriction */ ++ } ++ ++ if (length <= (size_t)(lowPrefix-match)) { ++ /* match fits entirely within external dictionary : just copy */ ++ LZ4_memmove(op, dictEnd - (lowPrefix-match), length); ++ op += length; ++ } else { ++ /* match stretches into both external dictionary and current block */ ++ size_t const copySize = (size_t)(lowPrefix - match); ++ size_t const restSize = length - copySize; ++ LZ4_memcpy(op, dictEnd - copySize, copySize); ++ op += copySize; ++ if (restSize > (size_t)(op - lowPrefix)) { /* overlap copy */ ++ BYTE* const endOfMatch = op + restSize; ++ const BYTE* copyFrom = lowPrefix; ++ while (op < endOfMatch) *op++ = *copyFrom++; ++ } else { ++ LZ4_memcpy(op, lowPrefix, restSize); ++ op += restSize; ++ } } ++ continue; ++ } ++ assert(match >= lowPrefix); ++ ++ /* copy match within block */ ++ cpy = op + length; ++ ++ /* partialDecoding : may end anywhere within the block */ ++ assert(op<=oend); ++ if (partialDecoding && (cpy > oend-MATCH_SAFEGUARD_DISTANCE)) { ++ size_t const mlen = MIN(length, (size_t)(oend-op)); ++ const BYTE* const matchEnd = match + mlen; ++ BYTE* const copyEnd = op + mlen; ++ if (matchEnd > op) { /* overlap copy */ ++ while (op < copyEnd) { *op++ = *match++; } ++ } else { ++ LZ4_memcpy(op, match, mlen); ++ } ++ op = copyEnd; ++ if (op == oend) { break; } ++ continue; ++ } ++ ++ if (unlikely(offset<8)) { ++ LZ4_write32(op, 0); /* silence msan warning when offset==0 */ ++ op[0] = match[0]; ++ op[1] = match[1]; ++ op[2] = match[2]; ++ op[3] = match[3]; ++ match += inc32table[offset]; ++ LZ4_memcpy(op+4, match, 4); ++ match -= dec64table[offset]; ++ } else { ++ LZ4_memcpy(op, match, 8); ++ match += 8; ++ } ++ op += 8; ++ ++ if (unlikely(cpy > oend-MATCH_SAFEGUARD_DISTANCE)) { ++ BYTE* const oCopyLimit = oend - (WILDCOPYLENGTH-1); ++ if (cpy > oend-LASTLITERALS) { goto _output_error; } /* Error : last LASTLITERALS bytes must be literals (uncompressed) */ ++ if (op < oCopyLimit) { ++ LZ4_wildCopy8(op, match, oCopyLimit); ++ match += oCopyLimit - op; ++ op = oCopyLimit; ++ } ++ while (op < cpy) { *op++ = *match++; } ++ } else { ++ LZ4_memcpy(op, match, 8); ++ if (length > 16) { LZ4_wildCopy8(op+8, match+8, cpy); } ++ } ++ op = cpy; /* wildcopy correction */ ++ } ++ ++ /* end of decoding */ ++ DEBUGLOG(5, "decoded %i bytes", (int) (((char*)op)-dst)); ++ return (int) (((char*)op)-dst); /* Nb of output bytes decoded */ ++ ++ /* Overflow error detected */ ++ _output_error: ++ return (int) (-(((const char*)ip)-src))-1; ++ } ++} ++ ++ ++/*===== Instantiate the API decoding functions. =====*/ ++ ++LZ4_FORCE_O2 ++int LZ4_decompress_safe(const char* source, char* dest, int compressedSize, int maxDecompressedSize) ++{ ++ return LZ4_decompress_generic(source, dest, compressedSize, maxDecompressedSize, ++ decode_full_block, noDict, ++ (BYTE*)dest, NULL, 0); ++} ++ ++LZ4_FORCE_O2 ++int LZ4_decompress_safe_partial(const char* src, char* dst, int compressedSize, int targetOutputSize, int dstCapacity) ++{ ++ dstCapacity = MIN(targetOutputSize, dstCapacity); ++ return LZ4_decompress_generic(src, dst, compressedSize, dstCapacity, ++ partial_decode, ++ noDict, (BYTE*)dst, NULL, 0); ++} ++ ++LZ4_FORCE_O2 ++int LZ4_decompress_fast(const char* source, char* dest, int originalSize) ++{ ++ DEBUGLOG(5, "LZ4_decompress_fast"); ++ return LZ4_decompress_unsafe_generic( ++ (const BYTE*)source, (BYTE*)dest, originalSize, ++ 0, NULL, 0); ++} ++ ++/*===== Instantiate a few more decoding cases, used more than once. =====*/ ++ ++LZ4_FORCE_O2 /* Exported, an obsolete API function. */ ++int LZ4_decompress_safe_withPrefix64k(const char* source, char* dest, int compressedSize, int maxOutputSize) ++{ ++ return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, ++ decode_full_block, withPrefix64k, ++ (BYTE*)dest - 64 KB, NULL, 0); ++} ++ ++LZ4_FORCE_O2 ++static int LZ4_decompress_safe_partial_withPrefix64k(const char* source, char* dest, int compressedSize, int targetOutputSize, int dstCapacity) ++{ ++ dstCapacity = MIN(targetOutputSize, dstCapacity); ++ return LZ4_decompress_generic(source, dest, compressedSize, dstCapacity, ++ partial_decode, withPrefix64k, ++ (BYTE*)dest - 64 KB, NULL, 0); ++} ++ ++/* Another obsolete API function, paired with the previous one. */ ++int LZ4_decompress_fast_withPrefix64k(const char* source, char* dest, int originalSize) ++{ ++ return LZ4_decompress_unsafe_generic( ++ (const BYTE*)source, (BYTE*)dest, originalSize, ++ 64 KB, NULL, 0); ++} ++ ++LZ4_FORCE_O2 ++static int LZ4_decompress_safe_withSmallPrefix(const char* source, char* dest, int compressedSize, int maxOutputSize, ++ size_t prefixSize) ++{ ++ return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, ++ decode_full_block, noDict, ++ (BYTE*)dest-prefixSize, NULL, 0); ++} ++ ++LZ4_FORCE_O2 ++static int LZ4_decompress_safe_partial_withSmallPrefix(const char* source, char* dest, int compressedSize, int targetOutputSize, int dstCapacity, ++ size_t prefixSize) ++{ ++ dstCapacity = MIN(targetOutputSize, dstCapacity); ++ return LZ4_decompress_generic(source, dest, compressedSize, dstCapacity, ++ partial_decode, noDict, ++ (BYTE*)dest-prefixSize, NULL, 0); ++} ++ ++LZ4_FORCE_O2 ++int LZ4_decompress_safe_forceExtDict(const char* source, char* dest, ++ int compressedSize, int maxOutputSize, ++ const void* dictStart, size_t dictSize) ++{ ++ DEBUGLOG(5, "LZ4_decompress_safe_forceExtDict"); ++ return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, ++ decode_full_block, usingExtDict, ++ (BYTE*)dest, (const BYTE*)dictStart, dictSize); ++} ++ ++LZ4_FORCE_O2 ++int LZ4_decompress_safe_partial_forceExtDict(const char* source, char* dest, ++ int compressedSize, int targetOutputSize, int dstCapacity, ++ const void* dictStart, size_t dictSize) ++{ ++ dstCapacity = MIN(targetOutputSize, dstCapacity); ++ return LZ4_decompress_generic(source, dest, compressedSize, dstCapacity, ++ partial_decode, usingExtDict, ++ (BYTE*)dest, (const BYTE*)dictStart, dictSize); ++} ++ ++LZ4_FORCE_O2 ++static int LZ4_decompress_fast_extDict(const char* source, char* dest, int originalSize, ++ const void* dictStart, size_t dictSize) ++{ ++ return LZ4_decompress_unsafe_generic( ++ (const BYTE*)source, (BYTE*)dest, originalSize, ++ 0, (const BYTE*)dictStart, dictSize); ++} ++ ++/* The "double dictionary" mode, for use with e.g. ring buffers: the first part ++ * of the dictionary is passed as prefix, and the second via dictStart + dictSize. ++ * These routines are used only once, in LZ4_decompress_*_continue(). ++ */ ++LZ4_FORCE_INLINE ++int LZ4_decompress_safe_doubleDict(const char* source, char* dest, int compressedSize, int maxOutputSize, ++ size_t prefixSize, const void* dictStart, size_t dictSize) ++{ ++ return LZ4_decompress_generic(source, dest, compressedSize, maxOutputSize, ++ decode_full_block, usingExtDict, ++ (BYTE*)dest-prefixSize, (const BYTE*)dictStart, dictSize); ++} ++ ++/*===== streaming decompression functions =====*/ ++ ++#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++LZ4_streamDecode_t* LZ4_createStreamDecode(void) ++{ ++ LZ4_STATIC_ASSERT(sizeof(LZ4_streamDecode_t) >= sizeof(LZ4_streamDecode_t_internal)); ++ return (LZ4_streamDecode_t*) ALLOC_AND_ZERO(sizeof(LZ4_streamDecode_t)); ++} ++ ++int LZ4_freeStreamDecode (LZ4_streamDecode_t* LZ4_stream) ++{ ++ if (LZ4_stream == NULL) { return 0; } /* support free on NULL */ ++ FREEMEM(LZ4_stream); ++ return 0; ++} ++#endif ++ ++/*! LZ4_setStreamDecode() : ++ * Use this function to instruct where to find the dictionary. ++ * This function is not necessary if previous data is still available where it was decoded. ++ * Loading a size of 0 is allowed (same effect as no dictionary). ++ * @return : 1 if OK, 0 if error ++ */ ++int LZ4_setStreamDecode (LZ4_streamDecode_t* LZ4_streamDecode, const char* dictionary, int dictSize) ++{ ++ LZ4_streamDecode_t_internal* lz4sd = &LZ4_streamDecode->internal_donotuse; ++ lz4sd->prefixSize = (size_t)dictSize; ++ if (dictSize) { ++ assert(dictionary != NULL); ++ lz4sd->prefixEnd = (const BYTE*) dictionary + dictSize; ++ } else { ++ lz4sd->prefixEnd = (const BYTE*) dictionary; ++ } ++ lz4sd->externalDict = NULL; ++ lz4sd->extDictSize = 0; ++ return 1; ++} ++ ++/*! LZ4_decoderRingBufferSize() : ++ * when setting a ring buffer for streaming decompression (optional scenario), ++ * provides the minimum size of this ring buffer ++ * to be compatible with any source respecting maxBlockSize condition. ++ * Note : in a ring buffer scenario, ++ * blocks are presumed decompressed next to each other. ++ * When not enough space remains for next block (remainingSize < maxBlockSize), ++ * decoding resumes from beginning of ring buffer. ++ * @return : minimum ring buffer size, ++ * or 0 if there is an error (invalid maxBlockSize). ++ */ ++int LZ4_decoderRingBufferSize(int maxBlockSize) ++{ ++ if (maxBlockSize < 0) return 0; ++ if (maxBlockSize > LZ4_MAX_INPUT_SIZE) return 0; ++ if (maxBlockSize < 16) maxBlockSize = 16; ++ return LZ4_DECODER_RING_BUFFER_SIZE(maxBlockSize); ++} ++ ++/* ++*_continue() : ++ These decoding functions allow decompression of multiple blocks in "streaming" mode. ++ Previously decoded blocks must still be available at the memory position where they were decoded. ++ If it's not possible, save the relevant part of decoded data into a safe buffer, ++ and indicate where it stands using LZ4_setStreamDecode() ++*/ ++LZ4_FORCE_O2 ++int LZ4_decompress_safe_continue (LZ4_streamDecode_t* LZ4_streamDecode, const char* source, char* dest, int compressedSize, int maxOutputSize) ++{ ++ LZ4_streamDecode_t_internal* lz4sd = &LZ4_streamDecode->internal_donotuse; ++ int result; ++ ++ if (lz4sd->prefixSize == 0) { ++ /* The first call, no dictionary yet. */ ++ assert(lz4sd->extDictSize == 0); ++ result = LZ4_decompress_safe(source, dest, compressedSize, maxOutputSize); ++ if (result <= 0) return result; ++ lz4sd->prefixSize = (size_t)result; ++ lz4sd->prefixEnd = (BYTE*)dest + result; ++ } else if (lz4sd->prefixEnd == (BYTE*)dest) { ++ /* They're rolling the current segment. */ ++ if (lz4sd->prefixSize >= 64 KB - 1) ++ result = LZ4_decompress_safe_withPrefix64k(source, dest, compressedSize, maxOutputSize); ++ else if (lz4sd->extDictSize == 0) ++ result = LZ4_decompress_safe_withSmallPrefix(source, dest, compressedSize, maxOutputSize, ++ lz4sd->prefixSize); ++ else ++ result = LZ4_decompress_safe_doubleDict(source, dest, compressedSize, maxOutputSize, ++ lz4sd->prefixSize, lz4sd->externalDict, lz4sd->extDictSize); ++ if (result <= 0) return result; ++ lz4sd->prefixSize += (size_t)result; ++ lz4sd->prefixEnd += result; ++ } else { ++ /* The buffer wraps around, or they're switching to another buffer. */ ++ lz4sd->extDictSize = lz4sd->prefixSize; ++ lz4sd->externalDict = lz4sd->prefixEnd - lz4sd->extDictSize; ++ result = LZ4_decompress_safe_forceExtDict(source, dest, compressedSize, maxOutputSize, ++ lz4sd->externalDict, lz4sd->extDictSize); ++ if (result <= 0) return result; ++ lz4sd->prefixSize = (size_t)result; ++ lz4sd->prefixEnd = (BYTE*)dest + result; ++ } ++ ++ return result; ++} ++ ++LZ4_FORCE_O2 int ++LZ4_decompress_fast_continue (LZ4_streamDecode_t* LZ4_streamDecode, ++ const char* source, char* dest, int originalSize) ++{ ++ LZ4_streamDecode_t_internal* const lz4sd = ++ (assert(LZ4_streamDecode!=NULL), &LZ4_streamDecode->internal_donotuse); ++ int result; ++ ++ DEBUGLOG(5, "LZ4_decompress_fast_continue (toDecodeSize=%i)", originalSize); ++ assert(originalSize >= 0); ++ ++ if (lz4sd->prefixSize == 0) { ++ DEBUGLOG(5, "first invocation : no prefix nor extDict"); ++ assert(lz4sd->extDictSize == 0); ++ result = LZ4_decompress_fast(source, dest, originalSize); ++ if (result <= 0) return result; ++ lz4sd->prefixSize = (size_t)originalSize; ++ lz4sd->prefixEnd = (BYTE*)dest + originalSize; ++ } else if (lz4sd->prefixEnd == (BYTE*)dest) { ++ DEBUGLOG(5, "continue using existing prefix"); ++ result = LZ4_decompress_unsafe_generic( ++ (const BYTE*)source, (BYTE*)dest, originalSize, ++ lz4sd->prefixSize, ++ lz4sd->externalDict, lz4sd->extDictSize); ++ if (result <= 0) return result; ++ lz4sd->prefixSize += (size_t)originalSize; ++ lz4sd->prefixEnd += originalSize; ++ } else { ++ DEBUGLOG(5, "prefix becomes extDict"); ++ lz4sd->extDictSize = lz4sd->prefixSize; ++ lz4sd->externalDict = lz4sd->prefixEnd - lz4sd->extDictSize; ++ result = LZ4_decompress_fast_extDict(source, dest, originalSize, ++ lz4sd->externalDict, lz4sd->extDictSize); ++ if (result <= 0) return result; ++ lz4sd->prefixSize = (size_t)originalSize; ++ lz4sd->prefixEnd = (BYTE*)dest + originalSize; ++ } ++ ++ return result; ++} ++ ++ ++/* ++Advanced decoding functions : ++*_usingDict() : ++ These decoding functions work the same as "_continue" ones, ++ the dictionary must be explicitly provided within parameters ++*/ ++ ++int LZ4_decompress_safe_usingDict(const char* source, char* dest, int compressedSize, int maxOutputSize, const char* dictStart, int dictSize) ++{ ++ if (dictSize==0) ++ return LZ4_decompress_safe(source, dest, compressedSize, maxOutputSize); ++ if (dictStart+dictSize == dest) { ++ if (dictSize >= 64 KB - 1) { ++ return LZ4_decompress_safe_withPrefix64k(source, dest, compressedSize, maxOutputSize); ++ } ++ assert(dictSize >= 0); ++ return LZ4_decompress_safe_withSmallPrefix(source, dest, compressedSize, maxOutputSize, (size_t)dictSize); ++ } ++ assert(dictSize >= 0); ++ return LZ4_decompress_safe_forceExtDict(source, dest, compressedSize, maxOutputSize, dictStart, (size_t)dictSize); ++} ++ ++int LZ4_decompress_safe_partial_usingDict(const char* source, char* dest, int compressedSize, int targetOutputSize, int dstCapacity, const char* dictStart, int dictSize) ++{ ++ if (dictSize==0) ++ return LZ4_decompress_safe_partial(source, dest, compressedSize, targetOutputSize, dstCapacity); ++ if (dictStart+dictSize == dest) { ++ if (dictSize >= 64 KB - 1) { ++ return LZ4_decompress_safe_partial_withPrefix64k(source, dest, compressedSize, targetOutputSize, dstCapacity); ++ } ++ assert(dictSize >= 0); ++ return LZ4_decompress_safe_partial_withSmallPrefix(source, dest, compressedSize, targetOutputSize, dstCapacity, (size_t)dictSize); ++ } ++ assert(dictSize >= 0); ++ return LZ4_decompress_safe_partial_forceExtDict(source, dest, compressedSize, targetOutputSize, dstCapacity, dictStart, (size_t)dictSize); ++} ++ ++int LZ4_decompress_fast_usingDict(const char* source, char* dest, int originalSize, const char* dictStart, int dictSize) ++{ ++ if (dictSize==0 || dictStart+dictSize == dest) ++ return LZ4_decompress_unsafe_generic( ++ (const BYTE*)source, (BYTE*)dest, originalSize, ++ (size_t)dictSize, NULL, 0); ++ assert(dictSize >= 0); ++ return LZ4_decompress_fast_extDict(source, dest, originalSize, dictStart, (size_t)dictSize); ++} ++ ++ ++/*=************************************************* ++* Obsolete Functions ++***************************************************/ ++/* obsolete compression functions */ ++int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) ++{ ++ return LZ4_compress_default(source, dest, inputSize, maxOutputSize); ++} ++int LZ4_compress(const char* src, char* dest, int srcSize) ++{ ++ return LZ4_compress_default(src, dest, srcSize, LZ4_compressBound(srcSize)); ++} ++int LZ4_compress_limitedOutput_withState (void* state, const char* src, char* dst, int srcSize, int dstSize) ++{ ++ return LZ4_compress_fast_extState(state, src, dst, srcSize, dstSize, 1); ++} ++int LZ4_compress_withState (void* state, const char* src, char* dst, int srcSize) ++{ ++ return LZ4_compress_fast_extState(state, src, dst, srcSize, LZ4_compressBound(srcSize), 1); ++} ++int LZ4_compress_limitedOutput_continue (LZ4_stream_t* LZ4_stream, const char* src, char* dst, int srcSize, int dstCapacity) ++{ ++ return LZ4_compress_fast_continue(LZ4_stream, src, dst, srcSize, dstCapacity, 1); ++} ++int LZ4_compress_continue (LZ4_stream_t* LZ4_stream, const char* source, char* dest, int inputSize) ++{ ++ return LZ4_compress_fast_continue(LZ4_stream, source, dest, inputSize, LZ4_compressBound(inputSize), 1); ++} ++ ++/* ++These decompression functions are deprecated and should no longer be used. ++They are only provided here for compatibility with older user programs. ++- LZ4_uncompress is totally equivalent to LZ4_decompress_fast ++- LZ4_uncompress_unknownOutputSize is totally equivalent to LZ4_decompress_safe ++*/ ++int LZ4_uncompress (const char* source, char* dest, int outputSize) ++{ ++ return LZ4_decompress_fast(source, dest, outputSize); ++} ++int LZ4_uncompress_unknownOutputSize (const char* source, char* dest, int isize, int maxOutputSize) ++{ ++ return LZ4_decompress_safe(source, dest, isize, maxOutputSize); ++} ++ ++/* Obsolete Streaming functions */ ++ ++int LZ4_sizeofStreamState(void) { return sizeof(LZ4_stream_t); } ++ ++int LZ4_resetStreamState(void* state, char* inputBuffer) ++{ ++ (void)inputBuffer; ++ LZ4_resetStream((LZ4_stream_t*)state); ++ return 0; ++} ++ ++#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++void* LZ4_create (char* inputBuffer) ++{ ++ (void)inputBuffer; ++ return LZ4_createStream(); ++} ++#endif ++ ++char* LZ4_slideInputBuffer (void* state) ++{ ++ /* avoid const char * -> char * conversion warning */ ++ return (char *)(uptrval)((LZ4_stream_t*)state)->internal_donotuse.dictionary; ++} ++ ++#endif /* LZ4_COMMONDEFS_ONLY */ +diff --git a/code/server/lz4.h b/code/server/lz4.h +new file mode 100644 +index 0000000..5b147b2 +--- /dev/null ++++ b/code/server/lz4.h +@@ -0,0 +1,886 @@ ++/* ++ * LZ4 - Fast LZ compression algorithm ++ * Header File ++ * Copyright (c) Yann Collet. All rights reserved. ++ ++ BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) ++ ++ Redistribution and use in source and binary forms, with or without ++ modification, are permitted provided that the following conditions are ++ met: ++ ++ * Redistributions of source code must retain the above copyright ++ notice, this list of conditions and the following disclaimer. ++ * Redistributions in binary form must reproduce the above ++ copyright notice, this list of conditions and the following disclaimer ++ in the documentation and/or other materials provided with the ++ distribution. ++ ++ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ ++ You can contact the author at : ++ - LZ4 homepage : http://www.lz4.org ++ - LZ4 source repository : https://github.com/lz4/lz4 ++*/ ++#if defined (__cplusplus) ++extern "C" { ++#endif ++ ++#ifndef LZ4_H_2983827168210 ++#define LZ4_H_2983827168210 ++ ++/* --- Dependency --- */ ++#include /* size_t */ ++ ++ ++/** ++ Introduction ++ ++ LZ4 is lossless compression algorithm, providing compression speed >500 MB/s per core, ++ scalable with multi-cores CPU. It features an extremely fast decoder, with speed in ++ multiple GB/s per core, typically reaching RAM speed limits on multi-core systems. ++ ++ The LZ4 compression library provides in-memory compression and decompression functions. ++ It gives full buffer control to user. ++ Compression can be done in: ++ - a single step (described as Simple Functions) ++ - a single step, reusing a context (described in Advanced Functions) ++ - unbounded multiple steps (described as Streaming compression) ++ ++ lz4.h generates and decodes LZ4-compressed blocks (doc/lz4_Block_format.md). ++ Decompressing such a compressed block requires additional metadata. ++ Exact metadata depends on exact decompression function. ++ For the typical case of LZ4_decompress_safe(), ++ metadata includes block's compressed size, and maximum bound of decompressed size. ++ Each application is free to encode and pass such metadata in whichever way it wants. ++ ++ lz4.h only handle blocks, it can not generate Frames. ++ ++ Blocks are different from Frames (doc/lz4_Frame_format.md). ++ Frames bundle both blocks and metadata in a specified manner. ++ Embedding metadata is required for compressed data to be self-contained and portable. ++ Frame format is delivered through a companion API, declared in lz4frame.h. ++ The `lz4` CLI can only manage frames. ++*/ ++ ++/*^*************************************************************** ++* Export parameters ++*****************************************************************/ ++/* ++* LZ4_DLL_EXPORT : ++* Enable exporting of functions when building a Windows DLL ++* LZ4LIB_VISIBILITY : ++* Control library symbols visibility. ++*/ ++#ifndef LZ4LIB_VISIBILITY ++# if defined(__GNUC__) && (__GNUC__ >= 4) ++# define LZ4LIB_VISIBILITY __attribute__ ((visibility ("default"))) ++# else ++# define LZ4LIB_VISIBILITY ++# endif ++#endif ++#if defined(LZ4_DLL_EXPORT) && (LZ4_DLL_EXPORT==1) ++# define LZ4LIB_API __declspec(dllexport) LZ4LIB_VISIBILITY ++#elif defined(LZ4_DLL_IMPORT) && (LZ4_DLL_IMPORT==1) ++# define LZ4LIB_API __declspec(dllimport) LZ4LIB_VISIBILITY /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/ ++#else ++# define LZ4LIB_API LZ4LIB_VISIBILITY ++#endif ++ ++/*! LZ4_FREESTANDING : ++ * When this macro is set to 1, it enables "freestanding mode" that is ++ * suitable for typical freestanding environment which doesn't support ++ * standard C library. ++ * ++ * - LZ4_FREESTANDING is a compile-time switch. ++ * - It requires the following macros to be defined: ++ * LZ4_memcpy, LZ4_memmove, LZ4_memset. ++ * - It only enables LZ4/HC functions which don't use heap. ++ * All LZ4F_* functions are not supported. ++ * - See tests/freestanding.c to check its basic setup. ++ */ ++#if defined(LZ4_FREESTANDING) && (LZ4_FREESTANDING == 1) ++# define LZ4_HEAPMODE 0 ++# define LZ4HC_HEAPMODE 0 ++# define LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION 1 ++# if !defined(LZ4_memcpy) ++# error "LZ4_FREESTANDING requires macro 'LZ4_memcpy'." ++# endif ++# if !defined(LZ4_memset) ++# error "LZ4_FREESTANDING requires macro 'LZ4_memset'." ++# endif ++# if !defined(LZ4_memmove) ++# error "LZ4_FREESTANDING requires macro 'LZ4_memmove'." ++# endif ++#elif ! defined(LZ4_FREESTANDING) ++# define LZ4_FREESTANDING 0 ++#endif ++ ++ ++/*------ Version ------*/ ++#define LZ4_VERSION_MAJOR 1 /* for breaking interface changes */ ++#define LZ4_VERSION_MINOR 10 /* for new (non-breaking) interface capabilities */ ++#define LZ4_VERSION_RELEASE 0 /* for tweaks, bug-fixes, or development */ ++ ++#define LZ4_VERSION_NUMBER (LZ4_VERSION_MAJOR *100*100 + LZ4_VERSION_MINOR *100 + LZ4_VERSION_RELEASE) ++ ++#define LZ4_LIB_VERSION LZ4_VERSION_MAJOR.LZ4_VERSION_MINOR.LZ4_VERSION_RELEASE ++#define LZ4_QUOTE(str) #str ++#define LZ4_EXPAND_AND_QUOTE(str) LZ4_QUOTE(str) ++#define LZ4_VERSION_STRING LZ4_EXPAND_AND_QUOTE(LZ4_LIB_VERSION) /* requires v1.7.3+ */ ++ ++LZ4LIB_API int LZ4_versionNumber (void); /**< library version number; useful to check dll version; requires v1.3.0+ */ ++LZ4LIB_API const char* LZ4_versionString (void); /**< library version string; useful to check dll version; requires v1.7.5+ */ ++ ++ ++/*-************************************ ++* Tuning memory usage ++**************************************/ ++/*! ++ * LZ4_MEMORY_USAGE : ++ * Can be selected at compile time, by setting LZ4_MEMORY_USAGE. ++ * Memory usage formula : N->2^N Bytes (examples : 10 -> 1KB; 12 -> 4KB ; 16 -> 64KB; 20 -> 1MB) ++ * Increasing memory usage improves compression ratio, generally at the cost of speed. ++ * Reduced memory usage may improve speed at the cost of ratio, thanks to better cache locality. ++ * Default value is 14, for 16KB, which nicely fits into most L1 caches. ++ */ ++#ifndef LZ4_MEMORY_USAGE ++# define LZ4_MEMORY_USAGE LZ4_MEMORY_USAGE_DEFAULT ++#endif ++ ++/* These are absolute limits, they should not be changed by users */ ++#define LZ4_MEMORY_USAGE_MIN 10 ++#define LZ4_MEMORY_USAGE_DEFAULT 14 ++#define LZ4_MEMORY_USAGE_MAX 20 ++ ++#if (LZ4_MEMORY_USAGE < LZ4_MEMORY_USAGE_MIN) ++# error "LZ4_MEMORY_USAGE is too small !" ++#endif ++ ++#if (LZ4_MEMORY_USAGE > LZ4_MEMORY_USAGE_MAX) ++# error "LZ4_MEMORY_USAGE is too large !" ++#endif ++ ++/*-************************************ ++* Simple Functions ++**************************************/ ++/*! LZ4_compress_default() : ++ * Compresses 'srcSize' bytes from buffer 'src' ++ * into already allocated 'dst' buffer of size 'dstCapacity'. ++ * Compression is guaranteed to succeed if 'dstCapacity' >= LZ4_compressBound(srcSize). ++ * It also runs faster, so it's a recommended setting. ++ * If the function cannot compress 'src' into a more limited 'dst' budget, ++ * compression stops *immediately*, and the function result is zero. ++ * In which case, 'dst' content is undefined (invalid). ++ * srcSize : max supported value is LZ4_MAX_INPUT_SIZE. ++ * dstCapacity : size of buffer 'dst' (which must be already allocated) ++ * @return : the number of bytes written into buffer 'dst' (necessarily <= dstCapacity) ++ * or 0 if compression fails ++ * Note : This function is protected against buffer overflow scenarios (never writes outside 'dst' buffer, nor read outside 'source' buffer). ++ */ ++LZ4LIB_API int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity); ++ ++/*! LZ4_decompress_safe() : ++ * @compressedSize : is the exact complete size of the compressed block. ++ * @dstCapacity : is the size of destination buffer (which must be already allocated), ++ * presumed an upper bound of decompressed size. ++ * @return : the number of bytes decompressed into destination buffer (necessarily <= dstCapacity) ++ * If destination buffer is not large enough, decoding will stop and output an error code (negative value). ++ * If the source stream is detected malformed, the function will stop decoding and return a negative result. ++ * Note 1 : This function is protected against malicious data packets : ++ * it will never writes outside 'dst' buffer, nor read outside 'source' buffer, ++ * even if the compressed block is maliciously modified to order the decoder to do these actions. ++ * In such case, the decoder stops immediately, and considers the compressed block malformed. ++ * Note 2 : compressedSize and dstCapacity must be provided to the function, the compressed block does not contain them. ++ * The implementation is free to send / store / derive this information in whichever way is most beneficial. ++ * If there is a need for a different format which bundles together both compressed data and its metadata, consider looking at lz4frame.h instead. ++ */ ++LZ4LIB_API int LZ4_decompress_safe (const char* src, char* dst, int compressedSize, int dstCapacity); ++ ++ ++/*-************************************ ++* Advanced Functions ++**************************************/ ++#define LZ4_MAX_INPUT_SIZE 0x7E000000 /* 2 113 929 216 bytes */ ++#define LZ4_COMPRESSBOUND(isize) ((unsigned)(isize) > (unsigned)LZ4_MAX_INPUT_SIZE ? 0 : (isize) + ((isize)/255) + 16) ++ ++/*! LZ4_compressBound() : ++ Provides the maximum size that LZ4 compression may output in a "worst case" scenario (input data not compressible) ++ This function is primarily useful for memory allocation purposes (destination buffer size). ++ Macro LZ4_COMPRESSBOUND() is also provided for compilation-time evaluation (stack memory allocation for example). ++ Note that LZ4_compress_default() compresses faster when dstCapacity is >= LZ4_compressBound(srcSize) ++ inputSize : max supported value is LZ4_MAX_INPUT_SIZE ++ return : maximum output size in a "worst case" scenario ++ or 0, if input size is incorrect (too large or negative) ++*/ ++LZ4LIB_API int LZ4_compressBound(int inputSize); ++ ++/*! LZ4_compress_fast() : ++ Same as LZ4_compress_default(), but allows selection of "acceleration" factor. ++ The larger the acceleration value, the faster the algorithm, but also the lesser the compression. ++ It's a trade-off. It can be fine tuned, with each successive value providing roughly +~3% to speed. ++ An acceleration value of "1" is the same as regular LZ4_compress_default() ++ Values <= 0 will be replaced by LZ4_ACCELERATION_DEFAULT (currently == 1, see lz4.c). ++ Values > LZ4_ACCELERATION_MAX will be replaced by LZ4_ACCELERATION_MAX (currently == 65537, see lz4.c). ++*/ ++LZ4LIB_API int LZ4_compress_fast (const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); ++ ++ ++/*! LZ4_compress_fast_extState() : ++ * Same as LZ4_compress_fast(), using an externally allocated memory space for its state. ++ * Use LZ4_sizeofState() to know how much memory must be allocated, ++ * and allocate it on 8-bytes boundaries (using `malloc()` typically). ++ * Then, provide this buffer as `void* state` to compression function. ++ */ ++LZ4LIB_API int LZ4_sizeofState(void); ++LZ4LIB_API int LZ4_compress_fast_extState (void* state, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); ++ ++/*! LZ4_compress_destSize() : ++ * Reverse the logic : compresses as much data as possible from 'src' buffer ++ * into already allocated buffer 'dst', of size >= 'dstCapacity'. ++ * This function either compresses the entire 'src' content into 'dst' if it's large enough, ++ * or fill 'dst' buffer completely with as much data as possible from 'src'. ++ * note: acceleration parameter is fixed to "default". ++ * ++ * *srcSizePtr : in+out parameter. Initially contains size of input. ++ * Will be modified to indicate how many bytes where read from 'src' to fill 'dst'. ++ * New value is necessarily <= input value. ++ * @return : Nb bytes written into 'dst' (necessarily <= dstCapacity) ++ * or 0 if compression fails. ++ * ++ * Note : 'targetDstSize' must be >= 1, because it's the smallest valid lz4 payload. ++ * ++ * Note 2:from v1.8.2 to v1.9.1, this function had a bug (fixed in v1.9.2+): ++ * the produced compressed content could, in rare circumstances, ++ * require to be decompressed into a destination buffer ++ * larger by at least 1 byte than decompressesSize. ++ * If an application uses `LZ4_compress_destSize()`, ++ * it's highly recommended to update liblz4 to v1.9.2 or better. ++ * If this can't be done or ensured, ++ * the receiving decompression function should provide ++ * a dstCapacity which is > decompressedSize, by at least 1 byte. ++ * See https://github.com/lz4/lz4/issues/859 for details ++ */ ++LZ4LIB_API int LZ4_compress_destSize(const char* src, char* dst, int* srcSizePtr, int targetDstSize); ++ ++/*! LZ4_decompress_safe_partial() : ++ * Decompress an LZ4 compressed block, of size 'srcSize' at position 'src', ++ * into destination buffer 'dst' of size 'dstCapacity'. ++ * Up to 'targetOutputSize' bytes will be decoded. ++ * The function stops decoding on reaching this objective. ++ * This can be useful to boost performance ++ * whenever only the beginning of a block is required. ++ * ++ * @return : the number of bytes decoded in `dst` (necessarily <= targetOutputSize) ++ * If source stream is detected malformed, function returns a negative result. ++ * ++ * Note 1 : @return can be < targetOutputSize, if compressed block contains less data. ++ * ++ * Note 2 : targetOutputSize must be <= dstCapacity ++ * ++ * Note 3 : this function effectively stops decoding on reaching targetOutputSize, ++ * so dstCapacity is kind of redundant. ++ * This is because in older versions of this function, ++ * decoding operation would still write complete sequences. ++ * Therefore, there was no guarantee that it would stop writing at exactly targetOutputSize, ++ * it could write more bytes, though only up to dstCapacity. ++ * Some "margin" used to be required for this operation to work properly. ++ * Thankfully, this is no longer necessary. ++ * The function nonetheless keeps the same signature, in an effort to preserve API compatibility. ++ * ++ * Note 4 : If srcSize is the exact size of the block, ++ * then targetOutputSize can be any value, ++ * including larger than the block's decompressed size. ++ * The function will, at most, generate block's decompressed size. ++ * ++ * Note 5 : If srcSize is _larger_ than block's compressed size, ++ * then targetOutputSize **MUST** be <= block's decompressed size. ++ * Otherwise, *silent corruption will occur*. ++ */ ++LZ4LIB_API int LZ4_decompress_safe_partial (const char* src, char* dst, int srcSize, int targetOutputSize, int dstCapacity); ++ ++ ++/*-********************************************* ++* Streaming Compression Functions ++***********************************************/ ++typedef union LZ4_stream_u LZ4_stream_t; /* incomplete type (defined later) */ ++ ++/*! ++ Note about RC_INVOKED ++ ++ - RC_INVOKED is predefined symbol of rc.exe (the resource compiler which is part of MSVC/Visual Studio). ++ https://docs.microsoft.com/en-us/windows/win32/menurc/predefined-macros ++ ++ - Since rc.exe is a legacy compiler, it truncates long symbol (> 30 chars) ++ and reports warning "RC4011: identifier truncated". ++ ++ - To eliminate the warning, we surround long preprocessor symbol with ++ "#if !defined(RC_INVOKED) ... #endif" block that means ++ "skip this block when rc.exe is trying to read it". ++*/ ++#if !defined(RC_INVOKED) /* https://docs.microsoft.com/en-us/windows/win32/menurc/predefined-macros */ ++#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++LZ4LIB_API LZ4_stream_t* LZ4_createStream(void); ++LZ4LIB_API int LZ4_freeStream (LZ4_stream_t* streamPtr); ++#endif /* !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) */ ++#endif ++ ++/*! LZ4_resetStream_fast() : v1.9.0+ ++ * Use this to prepare an LZ4_stream_t for a new chain of dependent blocks ++ * (e.g., LZ4_compress_fast_continue()). ++ * ++ * An LZ4_stream_t must be initialized once before usage. ++ * This is automatically done when created by LZ4_createStream(). ++ * However, should the LZ4_stream_t be simply declared on stack (for example), ++ * it's necessary to initialize it first, using LZ4_initStream(). ++ * ++ * After init, start any new stream with LZ4_resetStream_fast(). ++ * A same LZ4_stream_t can be re-used multiple times consecutively ++ * and compress multiple streams, ++ * provided that it starts each new stream with LZ4_resetStream_fast(). ++ * ++ * LZ4_resetStream_fast() is much faster than LZ4_initStream(), ++ * but is not compatible with memory regions containing garbage data. ++ * ++ * Note: it's only useful to call LZ4_resetStream_fast() ++ * in the context of streaming compression. ++ * The *extState* functions perform their own resets. ++ * Invoking LZ4_resetStream_fast() before is redundant, and even counterproductive. ++ */ ++LZ4LIB_API void LZ4_resetStream_fast (LZ4_stream_t* streamPtr); ++ ++/*! LZ4_loadDict() : ++ * Use this function to reference a static dictionary into LZ4_stream_t. ++ * The dictionary must remain available during compression. ++ * LZ4_loadDict() triggers a reset, so any previous data will be forgotten. ++ * The same dictionary will have to be loaded on decompression side for successful decoding. ++ * Dictionary are useful for better compression of small data (KB range). ++ * While LZ4 itself accepts any input as dictionary, dictionary efficiency is also a topic. ++ * When in doubt, employ the Zstandard's Dictionary Builder. ++ * Loading a size of 0 is allowed, and is the same as reset. ++ * @return : loaded dictionary size, in bytes (note: only the last 64 KB are loaded) ++ */ ++LZ4LIB_API int LZ4_loadDict (LZ4_stream_t* streamPtr, const char* dictionary, int dictSize); ++ ++/*! LZ4_loadDictSlow() : v1.10.0+ ++ * Same as LZ4_loadDict(), ++ * but uses a bit more cpu to reference the dictionary content more thoroughly. ++ * This is expected to slightly improve compression ratio. ++ * The extra-cpu cost is likely worth it if the dictionary is re-used across multiple sessions. ++ * @return : loaded dictionary size, in bytes (note: only the last 64 KB are loaded) ++ */ ++LZ4LIB_API int LZ4_loadDictSlow(LZ4_stream_t* streamPtr, const char* dictionary, int dictSize); ++ ++/*! LZ4_attach_dictionary() : stable since v1.10.0 ++ * ++ * This allows efficient re-use of a static dictionary multiple times. ++ * ++ * Rather than re-loading the dictionary buffer into a working context before ++ * each compression, or copying a pre-loaded dictionary's LZ4_stream_t into a ++ * working LZ4_stream_t, this function introduces a no-copy setup mechanism, ++ * in which the working stream references @dictionaryStream in-place. ++ * ++ * Several assumptions are made about the state of @dictionaryStream. ++ * Currently, only states which have been prepared by LZ4_loadDict() or ++ * LZ4_loadDictSlow() should be expected to work. ++ * ++ * Alternatively, the provided @dictionaryStream may be NULL, ++ * in which case any existing dictionary stream is unset. ++ * ++ * If a dictionary is provided, it replaces any pre-existing stream history. ++ * The dictionary contents are the only history that can be referenced and ++ * logically immediately precede the data compressed in the first subsequent ++ * compression call. ++ * ++ * The dictionary will only remain attached to the working stream through the ++ * first compression call, at the end of which it is cleared. ++ * @dictionaryStream stream (and source buffer) must remain in-place / accessible / unchanged ++ * through the completion of the compression session. ++ * ++ * Note: there is no equivalent LZ4_attach_*() method on the decompression side ++ * because there is no initialization cost, hence no need to share the cost across multiple sessions. ++ * To decompress LZ4 blocks using dictionary, attached or not, ++ * just employ the regular LZ4_setStreamDecode() for streaming, ++ * or the stateless LZ4_decompress_safe_usingDict() for one-shot decompression. ++ */ ++LZ4LIB_API void ++LZ4_attach_dictionary(LZ4_stream_t* workingStream, ++ const LZ4_stream_t* dictionaryStream); ++ ++/*! LZ4_compress_fast_continue() : ++ * Compress 'src' content using data from previously compressed blocks, for better compression ratio. ++ * 'dst' buffer must be already allocated. ++ * If dstCapacity >= LZ4_compressBound(srcSize), compression is guaranteed to succeed, and runs faster. ++ * ++ * @return : size of compressed block ++ * or 0 if there is an error (typically, cannot fit into 'dst'). ++ * ++ * Note 1 : Each invocation to LZ4_compress_fast_continue() generates a new block. ++ * Each block has precise boundaries. ++ * Each block must be decompressed separately, calling LZ4_decompress_*() with relevant metadata. ++ * It's not possible to append blocks together and expect a single invocation of LZ4_decompress_*() to decompress them together. ++ * ++ * Note 2 : The previous 64KB of source data is __assumed__ to remain present, unmodified, at same address in memory ! ++ * ++ * Note 3 : When input is structured as a double-buffer, each buffer can have any size, including < 64 KB. ++ * Make sure that buffers are separated, by at least one byte. ++ * This construction ensures that each block only depends on previous block. ++ * ++ * Note 4 : If input buffer is a ring-buffer, it can have any size, including < 64 KB. ++ * ++ * Note 5 : After an error, the stream status is undefined (invalid), it can only be reset or freed. ++ */ ++LZ4LIB_API int LZ4_compress_fast_continue (LZ4_stream_t* streamPtr, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); ++ ++/*! LZ4_saveDict() : ++ * If last 64KB data cannot be guaranteed to remain available at its current memory location, ++ * save it into a safer place (char* safeBuffer). ++ * This is schematically equivalent to a memcpy() followed by LZ4_loadDict(), ++ * but is much faster, because LZ4_saveDict() doesn't need to rebuild tables. ++ * @return : saved dictionary size in bytes (necessarily <= maxDictSize), or 0 if error. ++ */ ++LZ4LIB_API int LZ4_saveDict (LZ4_stream_t* streamPtr, char* safeBuffer, int maxDictSize); ++ ++ ++/*-********************************************** ++* Streaming Decompression Functions ++* Bufferless synchronous API ++************************************************/ ++typedef union LZ4_streamDecode_u LZ4_streamDecode_t; /* tracking context */ ++ ++/*! LZ4_createStreamDecode() and LZ4_freeStreamDecode() : ++ * creation / destruction of streaming decompression tracking context. ++ * A tracking context can be re-used multiple times. ++ */ ++#if !defined(RC_INVOKED) /* https://docs.microsoft.com/en-us/windows/win32/menurc/predefined-macros */ ++#if !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) ++LZ4LIB_API LZ4_streamDecode_t* LZ4_createStreamDecode(void); ++LZ4LIB_API int LZ4_freeStreamDecode (LZ4_streamDecode_t* LZ4_stream); ++#endif /* !defined(LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION) */ ++#endif ++ ++/*! LZ4_setStreamDecode() : ++ * An LZ4_streamDecode_t context can be allocated once and re-used multiple times. ++ * Use this function to start decompression of a new stream of blocks. ++ * A dictionary can optionally be set. Use NULL or size 0 for a reset order. ++ * Dictionary is presumed stable : it must remain accessible and unmodified during next decompression. ++ * @return : 1 if OK, 0 if error ++ */ ++LZ4LIB_API int LZ4_setStreamDecode (LZ4_streamDecode_t* LZ4_streamDecode, const char* dictionary, int dictSize); ++ ++/*! LZ4_decoderRingBufferSize() : v1.8.2+ ++ * Note : in a ring buffer scenario (optional), ++ * blocks are presumed decompressed next to each other ++ * up to the moment there is not enough remaining space for next block (remainingSize < maxBlockSize), ++ * at which stage it resumes from beginning of ring buffer. ++ * When setting such a ring buffer for streaming decompression, ++ * provides the minimum size of this ring buffer ++ * to be compatible with any source respecting maxBlockSize condition. ++ * @return : minimum ring buffer size, ++ * or 0 if there is an error (invalid maxBlockSize). ++ */ ++LZ4LIB_API int LZ4_decoderRingBufferSize(int maxBlockSize); ++#define LZ4_DECODER_RING_BUFFER_SIZE(maxBlockSize) (65536 + 14 + (maxBlockSize)) /* for static allocation; maxBlockSize presumed valid */ ++ ++/*! LZ4_decompress_safe_continue() : ++ * This decoding function allows decompression of consecutive blocks in "streaming" mode. ++ * The difference with the usual independent blocks is that ++ * new blocks are allowed to find references into former blocks. ++ * A block is an unsplittable entity, and must be presented entirely to the decompression function. ++ * LZ4_decompress_safe_continue() only accepts one block at a time. ++ * It's modeled after `LZ4_decompress_safe()` and behaves similarly. ++ * ++ * @LZ4_streamDecode : decompression state, tracking the position in memory of past data ++ * @compressedSize : exact complete size of one compressed block. ++ * @dstCapacity : size of destination buffer (which must be already allocated), ++ * must be an upper bound of decompressed size. ++ * @return : number of bytes decompressed into destination buffer (necessarily <= dstCapacity) ++ * If destination buffer is not large enough, decoding will stop and output an error code (negative value). ++ * If the source stream is detected malformed, the function will stop decoding and return a negative result. ++ * ++ * The last 64KB of previously decoded data *must* remain available and unmodified ++ * at the memory position where they were previously decoded. ++ * If less than 64KB of data has been decoded, all the data must be present. ++ * ++ * Special : if decompression side sets a ring buffer, it must respect one of the following conditions : ++ * - Decompression buffer size is _at least_ LZ4_decoderRingBufferSize(maxBlockSize). ++ * maxBlockSize is the maximum size of any single block. It can have any value > 16 bytes. ++ * In which case, encoding and decoding buffers do not need to be synchronized. ++ * Actually, data can be produced by any source compliant with LZ4 format specification, and respecting maxBlockSize. ++ * - Synchronized mode : ++ * Decompression buffer size is _exactly_ the same as compression buffer size, ++ * and follows exactly same update rule (block boundaries at same positions), ++ * and decoding function is provided with exact decompressed size of each block (exception for last block of the stream), ++ * _then_ decoding & encoding ring buffer can have any size, including small ones ( < 64 KB). ++ * - Decompression buffer is larger than encoding buffer, by a minimum of maxBlockSize more bytes. ++ * In which case, encoding and decoding buffers do not need to be synchronized, ++ * and encoding ring buffer can have any size, including small ones ( < 64 KB). ++ * ++ * Whenever these conditions are not possible, ++ * save the last 64KB of decoded data into a safe buffer where it can't be modified during decompression, ++ * then indicate where this data is saved using LZ4_setStreamDecode(), before decompressing next block. ++*/ ++LZ4LIB_API int ++LZ4_decompress_safe_continue (LZ4_streamDecode_t* LZ4_streamDecode, ++ const char* src, char* dst, ++ int srcSize, int dstCapacity); ++ ++ ++/*! LZ4_decompress_safe_usingDict() : ++ * Works the same as ++ * a combination of LZ4_setStreamDecode() followed by LZ4_decompress_safe_continue() ++ * However, it's stateless: it doesn't need any LZ4_streamDecode_t state. ++ * Dictionary is presumed stable : it must remain accessible and unmodified during decompression. ++ * Performance tip : Decompression speed can be substantially increased ++ * when dst == dictStart + dictSize. ++ */ ++LZ4LIB_API int ++LZ4_decompress_safe_usingDict(const char* src, char* dst, ++ int srcSize, int dstCapacity, ++ const char* dictStart, int dictSize); ++ ++/*! LZ4_decompress_safe_partial_usingDict() : ++ * Behaves the same as LZ4_decompress_safe_partial() ++ * with the added ability to specify a memory segment for past data. ++ * Performance tip : Decompression speed can be substantially increased ++ * when dst == dictStart + dictSize. ++ */ ++LZ4LIB_API int ++LZ4_decompress_safe_partial_usingDict(const char* src, char* dst, ++ int compressedSize, ++ int targetOutputSize, int maxOutputSize, ++ const char* dictStart, int dictSize); ++ ++#endif /* LZ4_H_2983827168210 */ ++ ++ ++/*^************************************* ++ * !!!!!! STATIC LINKING ONLY !!!!!! ++ ***************************************/ ++ ++/*-**************************************************************************** ++ * Experimental section ++ * ++ * Symbols declared in this section must be considered unstable. Their ++ * signatures or semantics may change, or they may be removed altogether in the ++ * future. They are therefore only safe to depend on when the caller is ++ * statically linked against the library. ++ * ++ * To protect against unsafe usage, not only are the declarations guarded, ++ * the definitions are hidden by default ++ * when building LZ4 as a shared/dynamic library. ++ * ++ * In order to access these declarations, ++ * define LZ4_STATIC_LINKING_ONLY in your application ++ * before including LZ4's headers. ++ * ++ * In order to make their implementations accessible dynamically, you must ++ * define LZ4_PUBLISH_STATIC_FUNCTIONS when building the LZ4 library. ++ ******************************************************************************/ ++ ++#ifdef LZ4_STATIC_LINKING_ONLY ++ ++#ifndef LZ4_STATIC_3504398509 ++#define LZ4_STATIC_3504398509 ++ ++#ifdef LZ4_PUBLISH_STATIC_FUNCTIONS ++# define LZ4LIB_STATIC_API LZ4LIB_API ++#else ++# define LZ4LIB_STATIC_API ++#endif ++ ++ ++/*! LZ4_compress_fast_extState_fastReset() : ++ * A variant of LZ4_compress_fast_extState(). ++ * ++ * Using this variant avoids an expensive initialization step. ++ * It is only safe to call if the state buffer is known to be correctly initialized already ++ * (see above comment on LZ4_resetStream_fast() for a definition of "correctly initialized"). ++ * From a high level, the difference is that ++ * this function initializes the provided state with a call to something like LZ4_resetStream_fast() ++ * while LZ4_compress_fast_extState() starts with a call to LZ4_resetStream(). ++ */ ++LZ4LIB_STATIC_API int LZ4_compress_fast_extState_fastReset (void* state, const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); ++ ++/*! LZ4_compress_destSize_extState() : introduced in v1.10.0 ++ * Same as LZ4_compress_destSize(), but using an externally allocated state. ++ * Also: exposes @acceleration ++ */ ++int LZ4_compress_destSize_extState(void* state, const char* src, char* dst, int* srcSizePtr, int targetDstSize, int acceleration); ++ ++/*! In-place compression and decompression ++ * ++ * It's possible to have input and output sharing the same buffer, ++ * for highly constrained memory environments. ++ * In both cases, it requires input to lay at the end of the buffer, ++ * and decompression to start at beginning of the buffer. ++ * Buffer size must feature some margin, hence be larger than final size. ++ * ++ * |<------------------------buffer--------------------------------->| ++ * |<-----------compressed data--------->| ++ * |<-----------decompressed size------------------>| ++ * |<----margin---->| ++ * ++ * This technique is more useful for decompression, ++ * since decompressed size is typically larger, ++ * and margin is short. ++ * ++ * In-place decompression will work inside any buffer ++ * which size is >= LZ4_DECOMPRESS_INPLACE_BUFFER_SIZE(decompressedSize). ++ * This presumes that decompressedSize > compressedSize. ++ * Otherwise, it means compression actually expanded data, ++ * and it would be more efficient to store such data with a flag indicating it's not compressed. ++ * This can happen when data is not compressible (already compressed, or encrypted). ++ * ++ * For in-place compression, margin is larger, as it must be able to cope with both ++ * history preservation, requiring input data to remain unmodified up to LZ4_DISTANCE_MAX, ++ * and data expansion, which can happen when input is not compressible. ++ * As a consequence, buffer size requirements are much higher, ++ * and memory savings offered by in-place compression are more limited. ++ * ++ * There are ways to limit this cost for compression : ++ * - Reduce history size, by modifying LZ4_DISTANCE_MAX. ++ * Note that it is a compile-time constant, so all compressions will apply this limit. ++ * Lower values will reduce compression ratio, except when input_size < LZ4_DISTANCE_MAX, ++ * so it's a reasonable trick when inputs are known to be small. ++ * - Require the compressor to deliver a "maximum compressed size". ++ * This is the `dstCapacity` parameter in `LZ4_compress*()`. ++ * When this size is < LZ4_COMPRESSBOUND(inputSize), then compression can fail, ++ * in which case, the return code will be 0 (zero). ++ * The caller must be ready for these cases to happen, ++ * and typically design a backup scheme to send data uncompressed. ++ * The combination of both techniques can significantly reduce ++ * the amount of margin required for in-place compression. ++ * ++ * In-place compression can work in any buffer ++ * which size is >= (maxCompressedSize) ++ * with maxCompressedSize == LZ4_COMPRESSBOUND(srcSize) for guaranteed compression success. ++ * LZ4_COMPRESS_INPLACE_BUFFER_SIZE() depends on both maxCompressedSize and LZ4_DISTANCE_MAX, ++ * so it's possible to reduce memory requirements by playing with them. ++ */ ++ ++#define LZ4_DECOMPRESS_INPLACE_MARGIN(compressedSize) (((compressedSize) >> 8) + 32) ++#define LZ4_DECOMPRESS_INPLACE_BUFFER_SIZE(decompressedSize) ((decompressedSize) + LZ4_DECOMPRESS_INPLACE_MARGIN(decompressedSize)) /**< note: presumes that compressedSize < decompressedSize. note2: margin is overestimated a bit, since it could use compressedSize instead */ ++ ++#ifndef LZ4_DISTANCE_MAX /* history window size; can be user-defined at compile time */ ++# define LZ4_DISTANCE_MAX 65535 /* set to maximum value by default */ ++#endif ++ ++#define LZ4_COMPRESS_INPLACE_MARGIN (LZ4_DISTANCE_MAX + 32) /* LZ4_DISTANCE_MAX can be safely replaced by srcSize when it's smaller */ ++#define LZ4_COMPRESS_INPLACE_BUFFER_SIZE(maxCompressedSize) ((maxCompressedSize) + LZ4_COMPRESS_INPLACE_MARGIN) /**< maxCompressedSize is generally LZ4_COMPRESSBOUND(inputSize), but can be set to any lower value, with the risk that compression can fail (return code 0(zero)) */ ++ ++#endif /* LZ4_STATIC_3504398509 */ ++#endif /* LZ4_STATIC_LINKING_ONLY */ ++ ++ ++ ++#ifndef LZ4_H_98237428734687 ++#define LZ4_H_98237428734687 ++ ++/*-************************************************************ ++ * Private Definitions ++ ************************************************************** ++ * Do not use these definitions directly. ++ * They are only exposed to allow static allocation of `LZ4_stream_t` and `LZ4_streamDecode_t`. ++ * Accessing members will expose user code to API and/or ABI break in future versions of the library. ++ **************************************************************/ ++#define LZ4_HASHLOG (LZ4_MEMORY_USAGE-2) ++#define LZ4_HASHTABLESIZE (1 << LZ4_MEMORY_USAGE) ++#define LZ4_HASH_SIZE_U32 (1 << LZ4_HASHLOG) /* required as macro for static allocation */ ++ ++#if defined(__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ++# include ++ typedef int8_t LZ4_i8; ++ typedef unsigned char LZ4_byte; ++ typedef uint16_t LZ4_u16; ++ typedef uint32_t LZ4_u32; ++#else ++ typedef signed char LZ4_i8; ++ typedef unsigned char LZ4_byte; ++ typedef unsigned short LZ4_u16; ++ typedef unsigned int LZ4_u32; ++#endif ++ ++/*! LZ4_stream_t : ++ * Never ever use below internal definitions directly ! ++ * These definitions are not API/ABI safe, and may change in future versions. ++ * If you need static allocation, declare or allocate an LZ4_stream_t object. ++**/ ++ ++typedef struct LZ4_stream_t_internal LZ4_stream_t_internal; ++struct LZ4_stream_t_internal { ++ LZ4_u32 hashTable[LZ4_HASH_SIZE_U32]; ++ const LZ4_byte* dictionary; ++ const LZ4_stream_t_internal* dictCtx; ++ LZ4_u32 currentOffset; ++ LZ4_u32 tableType; ++ LZ4_u32 dictSize; ++ /* Implicit padding to ensure structure is aligned */ ++}; ++ ++#define LZ4_STREAM_MINSIZE ((1UL << (LZ4_MEMORY_USAGE)) + 32) /* static size, for inter-version compatibility */ ++union LZ4_stream_u { ++ char minStateSize[LZ4_STREAM_MINSIZE]; ++ LZ4_stream_t_internal internal_donotuse; ++}; /* previously typedef'd to LZ4_stream_t */ ++ ++ ++/*! LZ4_initStream() : v1.9.0+ ++ * An LZ4_stream_t structure must be initialized at least once. ++ * This is automatically done when invoking LZ4_createStream(), ++ * but it's not when the structure is simply declared on stack (for example). ++ * ++ * Use LZ4_initStream() to properly initialize a newly declared LZ4_stream_t. ++ * It can also initialize any arbitrary buffer of sufficient size, ++ * and will @return a pointer of proper type upon initialization. ++ * ++ * Note : initialization fails if size and alignment conditions are not respected. ++ * In which case, the function will @return NULL. ++ * Note2: An LZ4_stream_t structure guarantees correct alignment and size. ++ * Note3: Before v1.9.0, use LZ4_resetStream() instead ++**/ ++LZ4LIB_API LZ4_stream_t* LZ4_initStream (void* stateBuffer, size_t size); ++ ++ ++/*! LZ4_streamDecode_t : ++ * Never ever use below internal definitions directly ! ++ * These definitions are not API/ABI safe, and may change in future versions. ++ * If you need static allocation, declare or allocate an LZ4_streamDecode_t object. ++**/ ++typedef struct { ++ const LZ4_byte* externalDict; ++ const LZ4_byte* prefixEnd; ++ size_t extDictSize; ++ size_t prefixSize; ++} LZ4_streamDecode_t_internal; ++ ++#define LZ4_STREAMDECODE_MINSIZE 32 ++union LZ4_streamDecode_u { ++ char minStateSize[LZ4_STREAMDECODE_MINSIZE]; ++ LZ4_streamDecode_t_internal internal_donotuse; ++} ; /* previously typedef'd to LZ4_streamDecode_t */ ++ ++ ++ ++/*-************************************ ++* Obsolete Functions ++**************************************/ ++ ++/*! Deprecation warnings ++ * ++ * Deprecated functions make the compiler generate a warning when invoked. ++ * This is meant to invite users to update their source code. ++ * Should deprecation warnings be a problem, it is generally possible to disable them, ++ * typically with -Wno-deprecated-declarations for gcc ++ * or _CRT_SECURE_NO_WARNINGS in Visual. ++ * ++ * Another method is to define LZ4_DISABLE_DEPRECATE_WARNINGS ++ * before including the header file. ++ */ ++#ifdef LZ4_DISABLE_DEPRECATE_WARNINGS ++# define LZ4_DEPRECATED(message) /* disable deprecation warnings */ ++#else ++# if defined (__cplusplus) && (__cplusplus >= 201402) /* C++14 or greater */ ++# define LZ4_DEPRECATED(message) [[deprecated(message)]] ++# elif defined(_MSC_VER) ++# define LZ4_DEPRECATED(message) __declspec(deprecated(message)) ++# elif defined(__clang__) || (defined(__GNUC__) && (__GNUC__ * 10 + __GNUC_MINOR__ >= 45)) ++# define LZ4_DEPRECATED(message) __attribute__((deprecated(message))) ++# elif defined(__GNUC__) && (__GNUC__ * 10 + __GNUC_MINOR__ >= 31) ++# define LZ4_DEPRECATED(message) __attribute__((deprecated)) ++# else ++# pragma message("WARNING: LZ4_DEPRECATED needs custom implementation for this compiler") ++# define LZ4_DEPRECATED(message) /* disabled */ ++# endif ++#endif /* LZ4_DISABLE_DEPRECATE_WARNINGS */ ++ ++/*! Obsolete compression functions (since v1.7.3) */ ++LZ4_DEPRECATED("use LZ4_compress_default() instead") LZ4LIB_API int LZ4_compress (const char* src, char* dest, int srcSize); ++LZ4_DEPRECATED("use LZ4_compress_default() instead") LZ4LIB_API int LZ4_compress_limitedOutput (const char* src, char* dest, int srcSize, int maxOutputSize); ++LZ4_DEPRECATED("use LZ4_compress_fast_extState() instead") LZ4LIB_API int LZ4_compress_withState (void* state, const char* source, char* dest, int inputSize); ++LZ4_DEPRECATED("use LZ4_compress_fast_extState() instead") LZ4LIB_API int LZ4_compress_limitedOutput_withState (void* state, const char* source, char* dest, int inputSize, int maxOutputSize); ++LZ4_DEPRECATED("use LZ4_compress_fast_continue() instead") LZ4LIB_API int LZ4_compress_continue (LZ4_stream_t* LZ4_streamPtr, const char* source, char* dest, int inputSize); ++LZ4_DEPRECATED("use LZ4_compress_fast_continue() instead") LZ4LIB_API int LZ4_compress_limitedOutput_continue (LZ4_stream_t* LZ4_streamPtr, const char* source, char* dest, int inputSize, int maxOutputSize); ++ ++/*! Obsolete decompression functions (since v1.8.0) */ ++LZ4_DEPRECATED("use LZ4_decompress_fast() instead") LZ4LIB_API int LZ4_uncompress (const char* source, char* dest, int outputSize); ++LZ4_DEPRECATED("use LZ4_decompress_safe() instead") LZ4LIB_API int LZ4_uncompress_unknownOutputSize (const char* source, char* dest, int isize, int maxOutputSize); ++ ++/* Obsolete streaming functions (since v1.7.0) ++ * degraded functionality; do not use! ++ * ++ * In order to perform streaming compression, these functions depended on data ++ * that is no longer tracked in the state. They have been preserved as well as ++ * possible: using them will still produce a correct output. However, they don't ++ * actually retain any history between compression calls. The compression ratio ++ * achieved will therefore be no better than compressing each chunk ++ * independently. ++ */ ++LZ4_DEPRECATED("Use LZ4_createStream() instead") LZ4LIB_API void* LZ4_create (char* inputBuffer); ++LZ4_DEPRECATED("Use LZ4_createStream() instead") LZ4LIB_API int LZ4_sizeofStreamState(void); ++LZ4_DEPRECATED("Use LZ4_resetStream() instead") LZ4LIB_API int LZ4_resetStreamState(void* state, char* inputBuffer); ++LZ4_DEPRECATED("Use LZ4_saveDict() instead") LZ4LIB_API char* LZ4_slideInputBuffer (void* state); ++ ++/*! Obsolete streaming decoding functions (since v1.7.0) */ ++LZ4_DEPRECATED("use LZ4_decompress_safe_usingDict() instead") LZ4LIB_API int LZ4_decompress_safe_withPrefix64k (const char* src, char* dst, int compressedSize, int maxDstSize); ++LZ4_DEPRECATED("use LZ4_decompress_fast_usingDict() instead") LZ4LIB_API int LZ4_decompress_fast_withPrefix64k (const char* src, char* dst, int originalSize); ++ ++/*! Obsolete LZ4_decompress_fast variants (since v1.9.0) : ++ * These functions used to be faster than LZ4_decompress_safe(), ++ * but this is no longer the case. They are now slower. ++ * This is because LZ4_decompress_fast() doesn't know the input size, ++ * and therefore must progress more cautiously into the input buffer to not read beyond the end of block. ++ * On top of that `LZ4_decompress_fast()` is not protected vs malformed or malicious inputs, making it a security liability. ++ * As a consequence, LZ4_decompress_fast() is strongly discouraged, and deprecated. ++ * ++ * The last remaining LZ4_decompress_fast() specificity is that ++ * it can decompress a block without knowing its compressed size. ++ * Such functionality can be achieved in a more secure manner ++ * by employing LZ4_decompress_safe_partial(). ++ * ++ * Parameters: ++ * originalSize : is the uncompressed size to regenerate. ++ * `dst` must be already allocated, its size must be >= 'originalSize' bytes. ++ * @return : number of bytes read from source buffer (== compressed size). ++ * The function expects to finish at block's end exactly. ++ * If the source stream is detected malformed, the function stops decoding and returns a negative result. ++ * note : LZ4_decompress_fast*() requires originalSize. Thanks to this information, it never writes past the output buffer. ++ * However, since it doesn't know its 'src' size, it may read an unknown amount of input, past input buffer bounds. ++ * Also, since match offsets are not validated, match reads from 'src' may underflow too. ++ * These issues never happen if input (compressed) data is correct. ++ * But they may happen if input data is invalid (error or intentional tampering). ++ * As a consequence, use these functions in trusted environments with trusted data **only**. ++ */ ++LZ4_DEPRECATED("This function is deprecated and unsafe. Consider using LZ4_decompress_safe_partial() instead") ++LZ4LIB_API int LZ4_decompress_fast (const char* src, char* dst, int originalSize); ++LZ4_DEPRECATED("This function is deprecated and unsafe. Consider migrating towards LZ4_decompress_safe_continue() instead. " ++ "Note that the contract will change (requires block's compressed size, instead of decompressed size)") ++LZ4LIB_API int LZ4_decompress_fast_continue (LZ4_streamDecode_t* LZ4_streamDecode, const char* src, char* dst, int originalSize); ++LZ4_DEPRECATED("This function is deprecated and unsafe. Consider using LZ4_decompress_safe_partial_usingDict() instead") ++LZ4LIB_API int LZ4_decompress_fast_usingDict (const char* src, char* dst, int originalSize, const char* dictStart, int dictSize); ++ ++/*! LZ4_resetStream() : ++ * An LZ4_stream_t structure must be initialized at least once. ++ * This is done with LZ4_initStream(), or LZ4_resetStream(). ++ * Consider switching to LZ4_initStream(), ++ * invoking LZ4_resetStream() will trigger deprecation warnings in the future. ++ */ ++LZ4LIB_API void LZ4_resetStream (LZ4_stream_t* streamPtr); ++ ++ ++#endif /* LZ4_H_98237428734687 */ ++ ++ ++#if defined (__cplusplus) ++} ++#endif +diff --git a/code/server/server.h b/code/server/server.h +index c32b2dc..6d59686 100644 +--- a/code/server/server.h ++++ b/code/server/server.h +@@ -341,6 +341,29 @@ int SV_BotGetConsoleMessage( int client, char *buf, int size ); + int BotImport_DebugPolygonCreate(int color, int numPoints, vec3_t *points); + void BotImport_DebugPolygonDelete(int id); + ++// ++// sv_netdemo.c ++// ++void SVD_Record_f( void ); ++void SVD_StopRecord_f( void ); ++void SVD_RecordFrame( void ); ++void SVD_ResetDeltaState( void ); ++void SVD_AutoRecord( void ); ++void SVD_CaptureServerCommand( const char *cmd ); ++void SVD_Play_f( void ); ++void SVD_StopPlay_f( void ); ++void SVD_CleanupPlayback( void ); ++void SVD_Stop_f( void ); ++void SVD_Pause_f( void ); ++void SVD_Seek_f( void ); ++void SVD_SeekExact_f( void ); ++qboolean SVD_PlaybackFrame( void ); ++qboolean SVD_IsRecording( void ); ++qboolean SVD_IsPlaying( void ); ++qboolean SVD_IsPaused( void ); ++qboolean SVD_IsStarting( void ); ++qboolean SVD_ShouldPause( void ); ++ + //============================================================ + // + // high level object sorting to reduce interaction tests +diff --git a/code/server/sv_ccmds.c b/code/server/sv_ccmds.c +index 62ae179..7f1370f 100644 +--- a/code/server/sv_ccmds.c ++++ b/code/server/sv_ccmds.c +@@ -280,6 +280,9 @@ static void SV_MapRestart_f( void ) { + sv.state = SS_GAME; + sv.restarting = qfalse; + ++ // reset demo delta state so next frame writes full entities ++ SVD_ResetDeltaState(); ++ + // connect and begin all the clients + for (i=0 ; iinteger ; i++) { + client = &svs.clients[i]; +@@ -736,6 +739,14 @@ void SV_AddOperatorCommands( void ) { + if( com_dedicated->integer ) { + Cmd_AddCommand ("say", SV_ConSay_f); + } ++ ++ // server-side demo recording/playback ++ Cmd_AddCommand ("svdemo_record", SVD_Record_f); ++ Cmd_AddCommand ("svdemo_stop", SVD_Stop_f); ++ Cmd_AddCommand ("svdemo_play", SVD_Play_f); ++ Cmd_AddCommand ("svdemo_pause", SVD_Pause_f); ++ Cmd_AddCommand ("svdemo_seek", SVD_Seek_f); ++ Cmd_AddCommand ("svdemo_seekexact", SVD_SeekExact_f); + } + + /* +diff --git a/code/server/sv_client.c b/code/server/sv_client.c +index c3fdc92..84c2e3b 100644 +--- a/code/server/sv_client.c ++++ b/code/server/sv_client.c +@@ -1419,7 +1419,12 @@ static void SV_UserMove( client_t *cl, msg_t *msg, qboolean delta ) { + // don't execute if this is an old cmd which is already executed + // these old cmds are included when cl_packetdup > 0 + if ( cmds[i].serverTime <= cl->lastUsercmd.serverTime ) { +- continue; ++ // demo playback: still process the LAST cmd even with duplicate ++ // serverTime (paused = frozen time, all cmds have same time). ++ // Need buttons and origin from fresh usercmds. ++ if ( !SVD_IsPlaying() || i != cmdCount - 1 ) { ++ continue; ++ } + } + SV_ClientThink (cl, &cmds[ i ]); + } +diff --git a/code/server/sv_game.c b/code/server/sv_game.c +index 202994e..b227f3c 100644 +--- a/code/server/sv_game.c ++++ b/code/server/sv_game.c +@@ -83,13 +83,20 @@ Sends a command string to a client + =============== + */ + void SV_GameSendServerCommand( int clientNum, const char *text ) { ++ // capture for demo recording: broadcasts and per-client chat/tchat ++ if ( clientNum == -1 ) { ++ SVD_CaptureServerCommand( text ); ++ } else if ( !strncmp( text, "chat", 4 ) || !strncmp( text, "tchat", 5 ) ) { ++ SVD_CaptureServerCommand( text ); ++ } ++ + if ( clientNum == -1 ) { + SV_SendServerCommand( NULL, "%s", text ); + } else { + if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) { + return; + } +- SV_SendServerCommand( svs.clients + clientNum, "%s", text ); ++ SV_SendServerCommand( svs.clients + clientNum, "%s", text ); + } + } + +diff --git a/code/server/sv_init.c b/code/server/sv_init.c +index b3d0c4d..c59568c 100644 +--- a/code/server/sv_init.c ++++ b/code/server/sv_init.c +@@ -1,695 +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; +- +- // 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"); +-} +- +-/* +-=============== +-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 ); +- +- // 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" ); +- +- 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 98c74ce..b521dd0 100644 +--- a/code/server/sv_main.c ++++ b/code/server/sv_main.c +@@ -1,855 +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; +- svs.time += frameMsec; +- +- // let everything in the world think and move +- VM_Call( gvm, GAME_RUN_FRAME, svs.time ); +- } +- +- if ( com_speeds->integer ) { +- time_game = Sys_Milliseconds () - startTime; +- } +- +- // check timeouts +- 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 +new file mode 100644 +index 0000000..cf31610 +--- /dev/null ++++ b/code/server/sv_netdemo.c +@@ -0,0 +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 ++} ++ +diff --git a/svdemo.txt b/svdemo.txt +new file mode 100644 +index 0000000..10e2480 +--- /dev/null ++++ b/svdemo.txt +@@ -0,0 +1,196 @@ ++=========================================================================== ++ Серверные демо-записи (SVDEMO) ++ Руководство пользователя ++=========================================================================== ++ ++ОПИСАНИЕ ++-------- ++ ++Система серверных демо-записей позволяет записывать полное состояние ++игрового сервера (все сущности, все игроки) и воспроизводить запись ++с возможностью свободного перемещения камеры или просмотра от первого ++лица любого игрока. ++ ++В отличие от обычных клиентских демо (запись только того, что видит ++один игрок), серверная запись содержит полную картину матча. ++ ++ ++КОНСОЛЬНЫЕ КОМАНДЫ ++------------------ ++ ++Запись: ++ ++ svdemo_record <имя> ++ Начать запись серверного демо. Файл сохраняется в ++ svdemos/<имя>.svdm внутри игровой директории. ++ ++ svdemo_stop ++ Остановить текущую запись или воспроизведение. ++ При остановке воспроизведения происходит отключение от сервера. ++ ++Воспроизведение: ++ ++ svdemo_play <имя> ++ Воспроизвести серверное демо. Загружает карту из записи, ++ подключает зрителя автоматически. Можно вызывать из меню, ++ из игры или во время просмотра другого демо. ++ ++ svdemo_pause ++ Пауза / продолжение воспроизведения. Во время паузы камера ++ свободно перемещается, сущности заморожены. ++ На экране отображается "Playback Paused". ++ ++ svdemo_seek <секунды> ++ Перемотка относительно текущей позиции. Положительное значение -- ++ вперёд, отрицательное -- назад. ++ Перемещает к ближайшему ключевому кадру (точность ±5 сек). ++ Работает в паузе. ++ Пример: svdemo_seek -10 (назад на 10 секунд) ++ ++ svdemo_seekexact <секунды> ++ Точная перемотка. Находит ближайший ключевой кадр, затем ++ прочитывает кадры до целевого времени. Точность до одного ++ серверного кадра (50 мс при sv_fps 20). Может занять долю ++ секунды на длинных перемотках. Работает в паузе. ++ Пример: svdemo_seekexact -10 (назад ровно на 10 секунд) ++ ++ ++НАСТРОЙКИ (CVARS) ++----------------- ++ ++ svdemo_autorecord <0|1> (по умолчанию: 0) ++ Автоматическая запись демо при каждой загрузке карты. ++ Файлы именуются автоматически: <карта>_ГГГГММДД_ЧЧММСС.svdm ++ Пример: q3dm6_20260323_141530.svdm ++ ++ svdemo_pauseEmpty <0|1> (по умолчанию: 1) ++ Пауза воспроизведения, когда нет подключённых зрителей. ++ Демо начинается с первого кадра при подключении зрителя. ++ ++ svdemo_keyframeInterval <секунды> (по умолчанию: 5, 0 = выкл.) ++ Интервал ключевых кадров. Ключевые кадры позволяют перематывать ++ запись. Чем меньше интервал, тем точнее перемотка командой ++ svdemo_seek, но немного больше размер файла. Первый кадр ++ записи всегда является ключевым. ++ ++ Все настройки сохраняются в конфигурации (CVAR_ARCHIVE). ++ ++ ++ЗАПИСЬ ++------ ++ ++1. Запустите сервер и начните игру как обычно: ++ devmap q3dm6 ++ ++2. Начните запись: ++ svdemo_record mymatch ++ ++3. Играйте. Все действия всех игроков записываются. ++ ++4. Остановите запись: ++ svdemo_stop ++ ++ Запись также автоматически останавливается при: ++ - Смене карты (map, devmap, nextmap по таймлимиту/фраглимиту) ++ - Выключении сервера ++ Перезапуск карты (map_restart) НЕ прерывает запись. ++ ++ ++ВОСПРОИЗВЕДЕНИЕ ++--------------- ++ ++1. Запустите воспроизведение: ++ svdemo_play mymatch ++ ++2. Карта загрузится автоматически. Вы подключитесь как зритель ++ со свободной камерой (полёт по карте). ++ ++3. Управление зрителем: ++ - Свободная камера: перемещайтесь как обычный спектатор ++ - Следование за игроком: нажмите MOUSE1 (ATTACK) для входа ++ в режим следования и переключения между игроками ++ - Выход из следования: team spectator (можно забиндить) ++ ++4. В режиме следования вы видите игру от первого лица выбранного ++ игрока с полным HUD: здоровье, броня, боеприпасы, оружие. ++ ++5. Табло (TAB) показывает счёт записанных игроков. ++ ++6. Управление воспроизведением: ++ - svdemo_pause -- пауза/продолжение ++ - svdemo_seek -10 -- быстрая перемотка назад на 10 сек ++ - svdemo_seek 30 -- быстрая перемотка вперёд на 30 сек ++ - svdemo_seekexact -10 -- точная перемотка назад на 10 сек ++ - svdemo_stop -- остановка и выход ++ ++7. Воспроизведение останавливается автоматически при ++ достижении конца записи. ++ ++ ++ФОРМАТ ФАЙЛА ++------------- ++ ++Расширение: .svdm ++Директория: svdemos/ ++ ++Файл содержит: ++- Заголовок: название карты, настройки сервера, конфигстроки ++ (имена игроков, модели, настройки игры) ++- Покадровые данные: дельта-сжатые состояния сущностей и игроков, ++ серверные команды (чат, принты), изменения конфигстрок ++- Ключевые кадры с индексом в конце файла ++ ++Дельта-кодирование обеспечивает компактный размер файла. ++Типичный 10-минутный матч с 10 ботами занимает ~8 МБ. ++ ++Одна запись = одна карта. При смене карты запись останавливается. ++ ++ ++ОГРАНИЧЕНИЯ ++----------- ++ ++- Если в записанной игре было 64 игрока (MAX_CLIENTS), один ++ из них не будет виден при воспроизведении (его слот занят зрителем). ++- Воспроизведение требует наличия тех же pk3-файлов (карты, модели), ++ что использовались при записи. ++- Демо несовместимы между разными версиями движка, если изменился ++ формат сетевых структур. ++- При перемотке могут кратковременно отображаться визуальные артефакты ++ (частицы, вспышки) от локальных эффектов. ++ ++ ++ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ ++--------------------- ++ ++Автоматическая запись всех матчей: ++ ++ set svdemo_autorecord 1 ++ devmap q3dm17 ++ ++ (все матчи на этом сервере будут записываться автоматически) ++ ++ ++Запись конкретного матча: ++ ++ devmap q3tourney2 ++ svdemo_record duel_finals ++ (играть...) ++ svdemo_stop ++ ++ ++Просмотр записи: ++ ++ svdemo_play duel_finals ++ ++ ++Просмотр с перемоткой: ++ ++ svdemo_play duel_finals ++ (подождать 30 секунд) ++ svdemo_seekexact -20 (вернуться на 20 секунд назад) ++ svdemo_pause (поставить на паузу) ++ svdemo_seek -5 (ещё назад на 5 секунд, в паузе) ++ svdemo_pause (продолжить) ++ ++ ++=========================================================================== diff --git a/svdemo.txt b/svdemo.txt new file mode 100644 index 0000000..10e2480 --- /dev/null +++ b/svdemo.txt @@ -0,0 +1,196 @@ +=========================================================================== + Серверные демо-записи (SVDEMO) + Руководство пользователя +=========================================================================== + +ОПИСАНИЕ +-------- + +Система серверных демо-записей позволяет записывать полное состояние +игрового сервера (все сущности, все игроки) и воспроизводить запись +с возможностью свободного перемещения камеры или просмотра от первого +лица любого игрока. + +В отличие от обычных клиентских демо (запись только того, что видит +один игрок), серверная запись содержит полную картину матча. + + +КОНСОЛЬНЫЕ КОМАНДЫ +------------------ + +Запись: + + svdemo_record <имя> + Начать запись серверного демо. Файл сохраняется в + svdemos/<имя>.svdm внутри игровой директории. + + svdemo_stop + Остановить текущую запись или воспроизведение. + При остановке воспроизведения происходит отключение от сервера. + +Воспроизведение: + + svdemo_play <имя> + Воспроизвести серверное демо. Загружает карту из записи, + подключает зрителя автоматически. Можно вызывать из меню, + из игры или во время просмотра другого демо. + + svdemo_pause + Пауза / продолжение воспроизведения. Во время паузы камера + свободно перемещается, сущности заморожены. + На экране отображается "Playback Paused". + + svdemo_seek <секунды> + Перемотка относительно текущей позиции. Положительное значение -- + вперёд, отрицательное -- назад. + Перемещает к ближайшему ключевому кадру (точность ±5 сек). + Работает в паузе. + Пример: svdemo_seek -10 (назад на 10 секунд) + + svdemo_seekexact <секунды> + Точная перемотка. Находит ближайший ключевой кадр, затем + прочитывает кадры до целевого времени. Точность до одного + серверного кадра (50 мс при sv_fps 20). Может занять долю + секунды на длинных перемотках. Работает в паузе. + Пример: svdemo_seekexact -10 (назад ровно на 10 секунд) + + +НАСТРОЙКИ (CVARS) +----------------- + + svdemo_autorecord <0|1> (по умолчанию: 0) + Автоматическая запись демо при каждой загрузке карты. + Файлы именуются автоматически: <карта>_ГГГГММДД_ЧЧММСС.svdm + Пример: q3dm6_20260323_141530.svdm + + svdemo_pauseEmpty <0|1> (по умолчанию: 1) + Пауза воспроизведения, когда нет подключённых зрителей. + Демо начинается с первого кадра при подключении зрителя. + + svdemo_keyframeInterval <секунды> (по умолчанию: 5, 0 = выкл.) + Интервал ключевых кадров. Ключевые кадры позволяют перематывать + запись. Чем меньше интервал, тем точнее перемотка командой + svdemo_seek, но немного больше размер файла. Первый кадр + записи всегда является ключевым. + + Все настройки сохраняются в конфигурации (CVAR_ARCHIVE). + + +ЗАПИСЬ +------ + +1. Запустите сервер и начните игру как обычно: + devmap q3dm6 + +2. Начните запись: + svdemo_record mymatch + +3. Играйте. Все действия всех игроков записываются. + +4. Остановите запись: + svdemo_stop + + Запись также автоматически останавливается при: + - Смене карты (map, devmap, nextmap по таймлимиту/фраглимиту) + - Выключении сервера + Перезапуск карты (map_restart) НЕ прерывает запись. + + +ВОСПРОИЗВЕДЕНИЕ +--------------- + +1. Запустите воспроизведение: + svdemo_play mymatch + +2. Карта загрузится автоматически. Вы подключитесь как зритель + со свободной камерой (полёт по карте). + +3. Управление зрителем: + - Свободная камера: перемещайтесь как обычный спектатор + - Следование за игроком: нажмите MOUSE1 (ATTACK) для входа + в режим следования и переключения между игроками + - Выход из следования: team spectator (можно забиндить) + +4. В режиме следования вы видите игру от первого лица выбранного + игрока с полным HUD: здоровье, броня, боеприпасы, оружие. + +5. Табло (TAB) показывает счёт записанных игроков. + +6. Управление воспроизведением: + - svdemo_pause -- пауза/продолжение + - svdemo_seek -10 -- быстрая перемотка назад на 10 сек + - svdemo_seek 30 -- быстрая перемотка вперёд на 30 сек + - svdemo_seekexact -10 -- точная перемотка назад на 10 сек + - svdemo_stop -- остановка и выход + +7. Воспроизведение останавливается автоматически при + достижении конца записи. + + +ФОРМАТ ФАЙЛА +------------- + +Расширение: .svdm +Директория: svdemos/ + +Файл содержит: +- Заголовок: название карты, настройки сервера, конфигстроки + (имена игроков, модели, настройки игры) +- Покадровые данные: дельта-сжатые состояния сущностей и игроков, + серверные команды (чат, принты), изменения конфигстрок +- Ключевые кадры с индексом в конце файла + +Дельта-кодирование обеспечивает компактный размер файла. +Типичный 10-минутный матч с 10 ботами занимает ~8 МБ. + +Одна запись = одна карта. При смене карты запись останавливается. + + +ОГРАНИЧЕНИЯ +----------- + +- Если в записанной игре было 64 игрока (MAX_CLIENTS), один + из них не будет виден при воспроизведении (его слот занят зрителем). +- Воспроизведение требует наличия тех же pk3-файлов (карты, модели), + что использовались при записи. +- Демо несовместимы между разными версиями движка, если изменился + формат сетевых структур. +- При перемотке могут кратковременно отображаться визуальные артефакты + (частицы, вспышки) от локальных эффектов. + + +ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ +--------------------- + +Автоматическая запись всех матчей: + + set svdemo_autorecord 1 + devmap q3dm17 + + (все матчи на этом сервере будут записываться автоматически) + + +Запись конкретного матча: + + devmap q3tourney2 + svdemo_record duel_finals + (играть...) + svdemo_stop + + +Просмотр записи: + + svdemo_play duel_finals + + +Просмотр с перемоткой: + + svdemo_play duel_finals + (подождать 30 секунд) + svdemo_seekexact -20 (вернуться на 20 секунд назад) + svdemo_pause (поставить на паузу) + svdemo_seek -5 (ещё назад на 5 секунд, в паузе) + svdemo_pause (продолжить) + + +===========================================================================