Complete netdemo feature as a single commit on implant2. See svdemo.txt for full documentation. Recording: svdemo_record, delta compression, keyframes, auto-record, chat capture, configstring tracking, map_restart handling. Playback: svdemo_play, free camera (client-owned), follow mode, pause, seek/seekexact with keyframes, scoreboard, PVS via usercmd origin, SNAPFLAG_RESET_ENTITIES for clean transitions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16297 lines
517 KiB
Diff
16297 lines
517 KiB
Diff
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 ; i<pm->numtouch ; i++) {
|
||
- for (j=0 ; j<i ; j++) {
|
||
- if (pm->touchents[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 ; i<num ; i++ ) {
|
||
- hit = &g_entities[touch[i]];
|
||
-
|
||
- if ( !hit->touch && !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 ; i<pm->numtouch ; i++) {
|
||
+ for (j=0 ; j<i ; j++) {
|
||
+ if (pm->touchents[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 ; i<num ; i++ ) {
|
||
+ hit = &g_entities[touch[i]];
|
||
+
|
||
+ if ( !hit->touch && !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<level.maxclients ; i++ ) {
|
||
- g_entities[i].client = level.clients + i;
|
||
- }
|
||
-
|
||
- // always leave room for the max number of clients,
|
||
- // even if they aren't all used, so numbers inside that
|
||
- // range are NEVER anything but clients
|
||
- level.num_entities = MAX_CLIENTS;
|
||
-
|
||
- // let the server system know where the entites are
|
||
- trap_LocateGameData( level.gentities, level.num_entities, sizeof( gentity_t ),
|
||
- &level.clients[0].ps, sizeof( level.clients[0] ) );
|
||
-
|
||
- // reserve some spots for dead player bodies
|
||
- InitBodyQue();
|
||
-
|
||
- ClearRegisteredItems();
|
||
-
|
||
- // parse the key/value pairs and spawn gentities
|
||
- G_SpawnEntitiesFromString();
|
||
-
|
||
- // general initialization
|
||
- G_FindTeams();
|
||
-
|
||
- // make sure we have flags for CTF, etc
|
||
- if( g_gametype.integer >= 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 ; i<level.num_entities ; i++, ent++) {
|
||
- if ( !ent->inuse ) {
|
||
- 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<level.maxclients ; i++ ) {
|
||
+ g_entities[i].client = level.clients + i;
|
||
+ }
|
||
+
|
||
+ // always leave room for the max number of clients,
|
||
+ // even if they aren't all used, so numbers inside that
|
||
+ // range are NEVER anything but clients
|
||
+ level.num_entities = MAX_CLIENTS;
|
||
+
|
||
+ // let the server system know where the entites are
|
||
+ trap_LocateGameData( level.gentities, level.num_entities, sizeof( gentity_t ),
|
||
+ &level.clients[0].ps, sizeof( level.clients[0] ) );
|
||
+
|
||
+ // reserve some spots for dead player bodies
|
||
+ InitBodyQue();
|
||
+
|
||
+ ClearRegisteredItems();
|
||
+
|
||
+ // parse the key/value pairs and spawn gentities
|
||
+ G_SpawnEntitiesFromString();
|
||
+
|
||
+ // general initialization
|
||
+ G_FindTeams();
|
||
+
|
||
+ // make sure we have flags for CTF, etc
|
||
+ if( g_gametype.integer >= 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 ; i<level.num_entities ; i++, ent++) {
|
||
+ if ( !ent->inuse ) {
|
||
+ 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 @@
|
||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
|
||
<Optimization Condition="'$(Configuration)|$(Platform)'=='vector|Win32'">MaxSpeed</Optimization>
|
||
</ClCompile>
|
||
+ <ClCompile Include="server\sv_netdemo.c">
|
||
+ </ClCompile>
|
||
<ClCompile Include="server\sv_world.c">
|
||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">Disabled</Optimization>
|
||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">true</BrowseInformation>
|
||
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 <intrin.h> /* 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: (<non-zero constant> && <expression>) always evaluates to the result of <expression> */
|
||
+# pragma warning(disable : 6240) /* disable: C6240: (<expression> && <non-zero constant>) always evaluates to the result of <expression> */
|
||
+# 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 <stdlib.h> /* 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 <string.h> /* 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<<ML_BITS)-1)
|
||
+#define RUN_BITS (8-ML_BITS)
|
||
+#define RUN_MASK ((1U<<RUN_BITS)-1)
|
||
+
|
||
+
|
||
+/*-************************************
|
||
+* Error detection
|
||
+**************************************/
|
||
+#if defined(LZ4_DEBUG) && (LZ4_DEBUG>=1)
|
||
+# include <assert.h>
|
||
+#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 <stdio.h>
|
||
+ 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 <limits.h>
|
||
+#if defined(__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */)
|
||
+# include <stdint.h>
|
||
+ 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<e);
|
||
+}
|
||
+
|
||
+static const unsigned inc32table[8] = {0, 1, 2, 1, 0, 4, 4, 4};
|
||
+static const int dec64table[8] = {0, 0, 0, -1, -4, 1, 2, 3};
|
||
+
|
||
+
|
||
+#ifndef LZ4_FAST_DEC_LOOP
|
||
+# if defined __i386__ || defined _M_IX86 || defined __x86_64__ || defined _M_X64
|
||
+# define LZ4_FAST_DEC_LOOP 1
|
||
+# elif defined(__aarch64__)
|
||
+# define LZ4_FAST_DEC_LOOP 1
|
||
+# else
|
||
+# define LZ4_FAST_DEC_LOOP 0
|
||
+# endif
|
||
+#endif
|
||
+
|
||
+#if LZ4_FAST_DEC_LOOP
|
||
+
|
||
+LZ4_FORCE_INLINE void
|
||
+LZ4_memcpy_using_offset_base(BYTE* dstPtr, const BYTE* srcPtr, BYTE* dstEnd, const size_t offset)
|
||
+{
|
||
+ assert(srcPtr + offset == dstPtr);
|
||
+ if (offset < 8) {
|
||
+ LZ4_write32(dstPtr, 0); /* silence an msan warning when offset==0 */
|
||
+ dstPtr[0] = srcPtr[0];
|
||
+ dstPtr[1] = srcPtr[1];
|
||
+ dstPtr[2] = srcPtr[2];
|
||
+ dstPtr[3] = srcPtr[3];
|
||
+ srcPtr += inc32table[offset];
|
||
+ LZ4_memcpy(dstPtr+4, srcPtr, 4);
|
||
+ srcPtr -= dec64table[offset];
|
||
+ dstPtr += 8;
|
||
+ } else {
|
||
+ LZ4_memcpy(dstPtr, srcPtr, 8);
|
||
+ dstPtr += 8;
|
||
+ srcPtr += 8;
|
||
+ }
|
||
+
|
||
+ LZ4_wildCopy8(dstPtr, srcPtr, dstEnd);
|
||
+}
|
||
+
|
||
+/* customized variant of memcpy, which can overwrite up to 32 bytes beyond dstEnd
|
||
+ * this version copies two times 16 bytes (instead of one time 32 bytes)
|
||
+ * because it must be compatible with offsets >= 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<e);
|
||
+}
|
||
+
|
||
+/* LZ4_memcpy_using_offset() presumes :
|
||
+ * - dstEnd >= 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<pInLimit) && (*pMatch == *pIn)) pIn++;
|
||
+ return (unsigned)(pIn - pStart);
|
||
+}
|
||
+
|
||
+
|
||
+#ifndef LZ4_COMMONDEFS_ONLY
|
||
+/*-************************************
|
||
+* Local Constants
|
||
+**************************************/
|
||
+static const int LZ4_64Klimit = ((64 KB) + (MFLIMIT-1));
|
||
+static const U32 LZ4_skipTrigger = 6; /* Increase this value ==> 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<LZ4_64Klimit); /* Size too large (not within 64K limit) */
|
||
+ if (tableType == byPtr) assert(dictDirective==noDict); /* only supported use case with byPtr */
|
||
+ /* If init conditions are not met, we don't have to mark stream
|
||
+ * as having dirty context, since no action was taken yet */
|
||
+ if (outputDirective == fillOutput && maxOutputSize < 1) { return 0; } /* Impossible to store anything */
|
||
+ assert(acceleration >= 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 (inputSize<LZ4_minLength) goto _last_literals; /* Input too small, no compression (all literals) */
|
||
+
|
||
+ /* First Byte */
|
||
+ { U32 const h = LZ4_hashPosition(ip, tableType);
|
||
+ if (tableType == byPtr) {
|
||
+ LZ4_putPositionOnHash(ip, h, cctx->hashTable, 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<<ML_BITS);
|
||
+ for(; len >= 255 ; len-=255) *op++ = 255;
|
||
+ *op++ = (BYTE)len;
|
||
+ }
|
||
+ else *token = (BYTE)(litLength<<ML_BITS);
|
||
+
|
||
+ /* Copy Literals */
|
||
+ LZ4_wildCopy8(op, anchor, op+litLength);
|
||
+ op+=litLength;
|
||
+ DEBUGLOG(6, "seq.start:%i, literals=%u, match.start:%i",
|
||
+ (int)(anchor-(const BYTE*)source), litLength, (int)(ip-(const BYTE*)source));
|
||
+ }
|
||
+
|
||
+_next_match:
|
||
+ /* at this stage, the following variables must be correctly set :
|
||
+ * - ip : at start of LZ operation
|
||
+ * - match : at start of previous pattern occurrence; can be within current prefix, or within extDict
|
||
+ * - offset : if maybe_ext_memSegment==1 (constant)
|
||
+ * - lowLimit : must be == dictionary to mean "match is within extDict"; must be == source otherwise
|
||
+ * - token and *token : position to write 4-bits for match length; higher 4-bits for literal length supposed already written
|
||
+ */
|
||
+
|
||
+ if ((outputDirective == fillOutput) &&
|
||
+ (op + 2 /* offset */ + 1 /* token */ + MFLIMIT - MINMATCH /* min last literals so last match is <= end - MFLIMIT */ > 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<<ML_BITS);
|
||
+ }
|
||
+ LZ4_memcpy(op, anchor, lastRun);
|
||
+ ip = anchor + lastRun;
|
||
+ op += lastRun;
|
||
+ }
|
||
+
|
||
+ if (outputDirective == fillOutput) {
|
||
+ *inputConsumed = (int) (((const char*)ip)-source);
|
||
+ }
|
||
+ result = (int)(((char*)op) - dest);
|
||
+ assert(result > 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; i<LZ4_HASH_SIZE_U32; i++) {
|
||
+ if (LZ4_dict->hashTable[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<ml; u++) {
|
||
+ op[u] = match[u];
|
||
+ } } }
|
||
+ op += ml;
|
||
+ if ((size_t)(oend-op) < LASTLITERALS) {
|
||
+ DEBUGLOG(5, "invalid: match ends at distance %zi from end of block", oend-op);
|
||
+ /* incorrect end of block :
|
||
+ * last match must stop at least LASTLITERALS==5 bytes before end of output block */
|
||
+ return -1;
|
||
+ }
|
||
+ } /* match */
|
||
+ } /* main loop */
|
||
+ return (int)(ip - istart);
|
||
+}
|
||
+
|
||
+
|
||
+/* Read the variable-length literal or match length.
|
||
+ *
|
||
+ * @ip : input pointer
|
||
+ * @ilimit : position after which if length is not decoded, the input is necessarily corrupted.
|
||
+ * @initial_check - check ip >= 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 <stddef.h> /* 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 <stdint.h>
|
||
+ 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 ; i<sv_maxclients->integer ; 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 ; i<sv_maxclients->integer ; 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 ; i<sv_maxclients->integer ; 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 <pass> <command>" 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 <pass> <command>" 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 <demoname>\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 <demoname>\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 <seconds>\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 <seconds>\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 (продолжить)
|
||
+
|
||
+
|
||
+===========================================================================
|