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 (продолжить) + + +===========================================================================