aboutsummaryrefslogtreecommitdiff
path: root/code/server
diff options
context:
space:
mode:
Diffstat (limited to 'code/server')
-rw-r--r--code/server/server.h461
-rw-r--r--code/server/sv_bot.c631
-rw-r--r--code/server/sv_ccmds.c1319
-rw-r--r--code/server/sv_client.c1914
-rw-r--r--code/server/sv_game.c965
-rw-r--r--code/server/sv_init.c771
-rw-r--r--code/server/sv_main.c1131
-rw-r--r--code/server/sv_net_chan.c209
-rw-r--r--code/server/sv_rankings.c1537
-rw-r--r--code/server/sv_snapshot.c699
-rw-r--r--code/server/sv_world.c691
11 files changed, 10328 insertions, 0 deletions
diff --git a/code/server/server.h b/code/server/server.h
new file mode 100644
index 0000000..bf85594
--- /dev/null
+++ b/code/server/server.h
@@ -0,0 +1,461 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+// server.h
+
+#include "../qcommon/q_shared.h"
+#include "../qcommon/qcommon.h"
+#include "../game/g_public.h"
+#include "../game/bg_public.h"
+
+//=============================================================================
+
+#define PERS_SCORE 0 // !!! MUST NOT CHANGE, SERVER AND
+ // GAME BOTH REFERENCE !!!
+
+#define MAX_ENT_CLUSTERS 16
+
+#ifdef USE_VOIP
+typedef struct voipServerPacket_s
+{
+ int generation;
+ int sequence;
+ int frames;
+ int len;
+ int sender;
+ byte data[1024];
+} voipServerPacket_t;
+#endif
+
+typedef struct svEntity_s {
+ struct worldSector_s *worldSector;
+ struct svEntity_s *nextEntityInWorldSector;
+
+ entityState_t baseline; // for delta compression of initial sighting
+ int numClusters; // if -1, use headnode instead
+ int clusternums[MAX_ENT_CLUSTERS];
+ int lastCluster; // if all the clusters don't fit in clusternums
+ int areanum, areanum2;
+ int snapshotCounter; // used to prevent double adding from portal views
+} svEntity_t;
+
+typedef enum {
+ SS_DEAD, // no map loaded
+ SS_LOADING, // spawning level entities
+ SS_GAME // actively running
+} serverState_t;
+
+typedef struct {
+ serverState_t state;
+ qboolean restarting; // if true, send configstring changes during SS_LOADING
+ int serverId; // changes each server start
+ int restartedServerId; // serverId before a map_restart
+ int checksumFeed; // the feed key that we use to compute the pure checksum strings
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=475
+ // the serverId associated with the current checksumFeed (always <= serverId)
+ int checksumFeedServerId;
+ int snapshotCounter; // incremented for each snapshot built
+ int timeResidual; // <= 1000 / sv_frame->value
+ int nextFrameTime; // when time > nextFrameTime, process world
+ struct cmodel_s *models[MAX_MODELS];
+ char *configstrings[MAX_CONFIGSTRINGS];
+ svEntity_t svEntities[MAX_GENTITIES];
+
+ char *entityParsePoint; // used during game VM init
+
+ // the game virtual machine will update these on init and changes
+ sharedEntity_t *gentities;
+ int gentitySize;
+ int num_entities; // current number, <= MAX_GENTITIES
+
+ playerState_t *gameClients;
+ int gameClientSize; // will be > sizeof(playerState_t) due to game private data
+
+ int restartTime;
+ int time;
+} server_t;
+
+
+
+
+
+typedef struct {
+ int areabytes;
+ byte areabits[MAX_MAP_AREA_BYTES]; // portalarea visibility bits
+ playerState_t ps;
+ int num_entities;
+ int first_entity; // into the circular sv_packet_entities[]
+ // the entities MUST be in increasing state number
+ // order, otherwise the delta compression will fail
+ int messageSent; // time the message was transmitted
+ int messageAcked; // time the message was acked
+ int messageSize; // used to rate drop packets
+} clientSnapshot_t;
+
+typedef enum {
+ CS_FREE, // can be reused for a new connection
+ CS_ZOMBIE, // client has been disconnected, but don't reuse
+ // connection for a couple seconds
+ CS_CONNECTED, // has been assigned to a client_t, but no gamestate yet
+ CS_PRIMED, // gamestate has been sent, but client hasn't sent a usercmd
+ CS_ACTIVE // client is fully in game
+} clientState_t;
+
+typedef struct netchan_buffer_s {
+ msg_t msg;
+ byte msgBuffer[MAX_MSGLEN];
+ struct netchan_buffer_s *next;
+} netchan_buffer_t;
+
+typedef struct client_s {
+ clientState_t state;
+ char userinfo[MAX_INFO_STRING]; // name, etc
+
+ char reliableCommands[MAX_RELIABLE_COMMANDS][MAX_STRING_CHARS];
+ int reliableSequence; // last added reliable message, not necesarily sent or acknowledged yet
+ int reliableAcknowledge; // last acknowledged reliable message
+ int reliableSent; // last sent reliable message, not necesarily acknowledged yet
+ int messageAcknowledge;
+
+ int gamestateMessageNum; // netchan->outgoingSequence of gamestate
+ int challenge;
+
+ usercmd_t lastUsercmd;
+ int lastMessageNum; // for delta compression
+ int lastClientCommand; // reliable client message sequence
+ char lastClientCommandString[MAX_STRING_CHARS];
+ sharedEntity_t *gentity; // SV_GentityNum(clientnum)
+ char name[MAX_NAME_LENGTH]; // extracted from userinfo, high bits masked
+
+ // downloading
+ char downloadName[MAX_QPATH]; // if not empty string, we are downloading
+ fileHandle_t download; // file being downloaded
+ int downloadSize; // total bytes (can't use EOF because of paks)
+ int downloadCount; // bytes sent
+ int downloadClientBlock; // last block we sent to the client, awaiting ack
+ int downloadCurrentBlock; // current block number
+ int downloadXmitBlock; // last block we xmited
+ unsigned char *downloadBlocks[MAX_DOWNLOAD_WINDOW]; // the buffers for the download blocks
+ int downloadBlockSize[MAX_DOWNLOAD_WINDOW];
+ qboolean downloadEOF; // We have sent the EOF block
+ int downloadSendTime; // time we last got an ack from the client
+
+ int deltaMessage; // frame last client usercmd message
+ int nextReliableTime; // svs.time when another reliable command will be allowed
+ int lastPacketTime; // svs.time when packet was last received
+ int lastConnectTime; // svs.time when connection started
+ int nextSnapshotTime; // send another snapshot when svs.time >= nextSnapshotTime
+ qboolean rateDelayed; // true if nextSnapshotTime was set based on rate instead of snapshotMsec
+ int timeoutCount; // must timeout a few frames in a row so debugging doesn't break
+ clientSnapshot_t frames[PACKET_BACKUP]; // updates can be delta'd from here
+ int ping;
+ int rate; // bytes / second
+ int snapshotMsec; // requests a snapshot every snapshotMsec unless rate choked
+ int pureAuthentic;
+ qboolean gotCP; // TTimo - additional flag to distinguish between a bad pure checksum, and no cp command at all
+ netchan_t netchan;
+ // TTimo
+ // queuing outgoing fragmented messages to send them properly, without udp packet bursts
+ // in case large fragmented messages are stacking up
+ // buffer them into this queue, and hand them out to netchan as needed
+ netchan_buffer_t *netchan_start_queue;
+ netchan_buffer_t **netchan_end_queue;
+
+#ifdef USE_VOIP
+ qboolean hasVoip;
+ qboolean muteAllVoip;
+ qboolean ignoreVoipFromClient[MAX_CLIENTS];
+ voipServerPacket_t voipPacket[64]; // !!! FIXME: WAY too much memory!
+ int queuedVoipPackets;
+#endif
+
+ int oldServerTime;
+ qboolean csUpdated[MAX_CONFIGSTRINGS+1];
+} client_t;
+
+//=============================================================================
+
+
+// MAX_CHALLENGES is made large to prevent a denial
+// of service attack that could cycle all of them
+// out before legitimate users connected
+#define MAX_CHALLENGES 1024
+
+#define AUTHORIZE_TIMEOUT 5000
+
+typedef struct {
+ netadr_t adr;
+ int challenge;
+ int clientChallenge; // challenge number coming from the client
+ int time; // time the last packet was sent to the autherize server
+ int pingTime; // time the challenge response was sent to client
+ int firstTime; // time the adr was first used, for authorize timeout checks
+ qboolean wasrefused;
+ qboolean connected;
+} challenge_t;
+
+
+#define MAX_MASTERS 8 // max recipients for heartbeat packets
+
+
+// this structure will be cleared only when the game dll changes
+typedef struct {
+ qboolean initialized; // sv_init has completed
+
+ int time; // will be strictly increasing across level changes
+
+ int snapFlagServerBit; // ^= SNAPFLAG_SERVERCOUNT every SV_SpawnServer()
+
+ client_t *clients; // [sv_maxclients->integer];
+ int numSnapshotEntities; // sv_maxclients->integer*PACKET_BACKUP*MAX_PACKET_ENTITIES
+ int nextSnapshotEntities; // next snapshotEntities to use
+ entityState_t *snapshotEntities; // [numSnapshotEntities]
+ int nextHeartbeatTime;
+ challenge_t challenges[MAX_CHALLENGES]; // to prevent invalid IPs from connecting
+ netadr_t redirectAddress; // for rcon return messages
+
+ netadr_t authorizeAddress; // for rcon return messages
+} serverStatic_t;
+
+#define SERVER_MAXBANS 1024
+// Structure for managing bans
+typedef struct
+{
+ netadr_t ip;
+ // For a CIDR-Notation type suffix
+ int subnet;
+
+ qboolean isexception;
+} serverBan_t;
+
+//=============================================================================
+
+extern serverStatic_t svs; // persistant server info across maps
+extern server_t sv; // cleared each map
+extern vm_t *gvm; // game virtual machine
+
+#define MAX_MASTER_SERVERS 5
+
+extern cvar_t *sv_fps;
+extern cvar_t *sv_timeout;
+extern cvar_t *sv_zombietime;
+extern cvar_t *sv_rconPassword;
+extern cvar_t *sv_privatePassword;
+extern cvar_t *sv_allowDownload;
+extern cvar_t *sv_maxclients;
+
+extern cvar_t *sv_privateClients;
+extern cvar_t *sv_hostname;
+extern cvar_t *sv_master[MAX_MASTER_SERVERS];
+extern cvar_t *sv_reconnectlimit;
+extern cvar_t *sv_showloss;
+extern cvar_t *sv_padPackets;
+extern cvar_t *sv_killserver;
+extern cvar_t *sv_mapname;
+extern cvar_t *sv_mapChecksum;
+extern cvar_t *sv_serverid;
+extern cvar_t *sv_minRate;
+extern cvar_t *sv_maxRate;
+extern cvar_t *sv_minPing;
+extern cvar_t *sv_maxPing;
+extern cvar_t *sv_gametype;
+extern cvar_t *sv_pure;
+extern cvar_t *sv_floodProtect;
+extern cvar_t *sv_lanForceRate;
+extern cvar_t *sv_strictAuth;
+extern cvar_t *sv_banFile;
+
+extern serverBan_t serverBans[SERVER_MAXBANS];
+extern int serverBansCount;
+
+#ifdef USE_VOIP
+extern cvar_t *sv_voip;
+#endif
+
+
+//===========================================================
+
+//
+// sv_main.c
+//
+void SV_FinalMessage (char *message);
+void QDECL SV_SendServerCommand( client_t *cl, const char *fmt, ...);
+
+
+void SV_AddOperatorCommands (void);
+void SV_RemoveOperatorCommands (void);
+
+
+void SV_MasterHeartbeat (void);
+void SV_MasterShutdown (void);
+
+
+
+
+//
+// sv_init.c
+//
+void SV_SetConfigstring( int index, const char *val );
+void SV_GetConfigstring( int index, char *buffer, int bufferSize );
+void SV_UpdateConfigstrings( client_t *client );
+
+void SV_SetUserinfo( int index, const char *val );
+void SV_GetUserinfo( int index, char *buffer, int bufferSize );
+
+void SV_ChangeMaxClients( void );
+void SV_SpawnServer( char *server, qboolean killBots );
+
+
+
+//
+// sv_client.c
+//
+void SV_GetChallenge(netadr_t from);
+
+void SV_DirectConnect( netadr_t from );
+
+#ifndef STANDALONE
+void SV_AuthorizeIpPacket( netadr_t from );
+#endif
+
+void SV_ExecuteClientMessage( client_t *cl, msg_t *msg );
+void SV_UserinfoChanged( client_t *cl );
+
+void SV_ClientEnterWorld( client_t *client, usercmd_t *cmd );
+void SV_DropClient( client_t *drop, const char *reason );
+
+void SV_ExecuteClientCommand( client_t *cl, const char *s, qboolean clientOK );
+void SV_ClientThink (client_t *cl, usercmd_t *cmd);
+
+void SV_WriteDownloadToClient( client_t *cl , msg_t *msg );
+
+#ifdef USE_VOIP
+void SV_WriteVoipToClient( client_t *cl, msg_t *msg );
+#endif
+
+
+//
+// sv_ccmds.c
+//
+void SV_Heartbeat_f( void );
+
+//
+// sv_snapshot.c
+//
+void SV_AddServerCommand( client_t *client, const char *cmd );
+void SV_UpdateServerCommandsToClient( client_t *client, msg_t *msg );
+void SV_WriteFrameToClient (client_t *client, msg_t *msg);
+void SV_SendMessageToClient( msg_t *msg, client_t *client );
+void SV_SendClientMessages( void );
+void SV_SendClientSnapshot( client_t *client );
+
+//
+// sv_game.c
+//
+int SV_NumForGentity( sharedEntity_t *ent );
+sharedEntity_t *SV_GentityNum( int num );
+playerState_t *SV_GameClientNum( int num );
+svEntity_t *SV_SvEntityForGentity( sharedEntity_t *gEnt );
+sharedEntity_t *SV_GEntityForSvEntity( svEntity_t *svEnt );
+void SV_InitGameProgs ( void );
+void SV_ShutdownGameProgs ( void );
+void SV_RestartGameProgs( void );
+qboolean SV_inPVS (const vec3_t p1, const vec3_t p2);
+
+//
+// sv_bot.c
+//
+void SV_BotFrame( int time );
+int SV_BotAllocateClient(void);
+void SV_BotFreeClient( int clientNum );
+
+void SV_BotInitCvars(void);
+int SV_BotLibSetup( void );
+int SV_BotLibShutdown( void );
+int SV_BotGetSnapshotEntity( int client, int ent );
+int SV_BotGetConsoleMessage( int client, char *buf, int size );
+
+int BotImport_DebugPolygonCreate(int color, int numPoints, vec3_t *points);
+void BotImport_DebugPolygonDelete(int id);
+
+void SV_BotInitBotLib(void);
+
+//============================================================
+//
+// high level object sorting to reduce interaction tests
+//
+
+void SV_ClearWorld (void);
+// called after the world model has been loaded, before linking any entities
+
+void SV_UnlinkEntity( sharedEntity_t *ent );
+// call before removing an entity, and before trying to move one,
+// so it doesn't clip against itself
+
+void SV_LinkEntity( sharedEntity_t *ent );
+// Needs to be called any time an entity changes origin, mins, maxs,
+// or solid. Automatically unlinks if needed.
+// sets ent->v.absmin and ent->v.absmax
+// sets ent->leafnums[] for pvs determination even if the entity
+// is not solid
+
+
+clipHandle_t SV_ClipHandleForEntity( const sharedEntity_t *ent );
+
+
+void SV_SectorList_f( void );
+
+
+int SV_AreaEntities( const vec3_t mins, const vec3_t maxs, int *entityList, int maxcount );
+// fills in a table of entity numbers with entities that have bounding boxes
+// that intersect the given area. It is possible for a non-axial bmodel
+// to be returned that doesn't actually intersect the area on an exact
+// test.
+// returns the number of pointers filled in
+// The world entity is never returned in this list.
+
+
+int SV_PointContents( const vec3_t p, int passEntityNum );
+// returns the CONTENTS_* value from the world and all entities at the given point.
+
+
+void SV_Trace( trace_t *results, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask, int capsule );
+// mins and maxs are relative
+
+// if the entire move stays in a solid volume, trace.allsolid will be set,
+// trace.startsolid will be set, and trace.fraction will be 0
+
+// if the starting point is in a solid, it will be allowed to move out
+// to an open area
+
+// passEntityNum is explicitly excluded from clipping checks (normally ENTITYNUM_NONE)
+
+
+void SV_ClipToEntity( trace_t *trace, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int entityNum, int contentmask, int capsule );
+// clip to a specific entity
+
+//
+// sv_net_chan.c
+//
+void SV_Netchan_Transmit( client_t *client, msg_t *msg);
+void SV_Netchan_TransmitNextFragment( client_t *client );
+qboolean SV_Netchan_Process( client_t *client, msg_t *msg );
+
diff --git a/code/server/sv_bot.c b/code/server/sv_bot.c
new file mode 100644
index 0000000..8583f86
--- /dev/null
+++ b/code/server/sv_bot.c
@@ -0,0 +1,631 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+// sv_bot.c
+
+#include "server.h"
+#include "../botlib/botlib.h"
+
+typedef struct bot_debugpoly_s
+{
+ int inuse;
+ int color;
+ int numPoints;
+ vec3_t points[128];
+} bot_debugpoly_t;
+
+static bot_debugpoly_t *debugpolygons;
+int bot_maxdebugpolys;
+
+extern botlib_export_t *botlib_export;
+int bot_enable;
+
+
+/*
+==================
+SV_BotAllocateClient
+==================
+*/
+int SV_BotAllocateClient(void) {
+ int i;
+ client_t *cl;
+
+ // find a client slot
+ for ( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ ) {
+ if ( cl->state == CS_FREE ) {
+ break;
+ }
+ }
+
+ if ( i == sv_maxclients->integer ) {
+ return -1;
+ }
+
+ cl->gentity = SV_GentityNum( i );
+ cl->gentity->s.number = i;
+ cl->state = CS_ACTIVE;
+ cl->lastPacketTime = svs.time;
+ cl->netchan.remoteAddress.type = NA_BOT;
+ cl->rate = 16384;
+
+ return i;
+}
+
+/*
+==================
+SV_BotFreeClient
+==================
+*/
+void SV_BotFreeClient( int clientNum ) {
+ client_t *cl;
+
+ if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) {
+ Com_Error( ERR_DROP, "SV_BotFreeClient: bad clientNum: %i", clientNum );
+ }
+ cl = &svs.clients[clientNum];
+ cl->state = CS_FREE;
+ cl->name[0] = 0;
+ if ( cl->gentity ) {
+ cl->gentity->r.svFlags &= ~SVF_BOT;
+ }
+}
+
+/*
+==================
+BotDrawDebugPolygons
+==================
+*/
+void BotDrawDebugPolygons(void (*drawPoly)(int color, int numPoints, float *points), int value) {
+ static cvar_t *bot_debug, *bot_groundonly, *bot_reachability, *bot_highlightarea;
+ bot_debugpoly_t *poly;
+ int i, parm0;
+
+ if (!debugpolygons)
+ return;
+ //bot debugging
+ if (!bot_debug) bot_debug = Cvar_Get("bot_debug", "0", 0);
+ //
+ if (bot_enable && bot_debug->integer) {
+ //show reachabilities
+ if (!bot_reachability) bot_reachability = Cvar_Get("bot_reachability", "0", 0);
+ //show ground faces only
+ if (!bot_groundonly) bot_groundonly = Cvar_Get("bot_groundonly", "1", 0);
+ //get the hightlight area
+ if (!bot_highlightarea) bot_highlightarea = Cvar_Get("bot_highlightarea", "0", 0);
+ //
+ parm0 = 0;
+ if (svs.clients[0].lastUsercmd.buttons & BUTTON_ATTACK) parm0 |= 1;
+ if (bot_reachability->integer) parm0 |= 2;
+ if (bot_groundonly->integer) parm0 |= 4;
+ botlib_export->BotLibVarSet("bot_highlightarea", bot_highlightarea->string);
+ botlib_export->Test(parm0, NULL, svs.clients[0].gentity->r.currentOrigin,
+ svs.clients[0].gentity->r.currentAngles);
+ } //end if
+ //draw all debug polys
+ for (i = 0; i < bot_maxdebugpolys; i++) {
+ poly = &debugpolygons[i];
+ if (!poly->inuse) continue;
+ drawPoly(poly->color, poly->numPoints, (float *) poly->points);
+ //Com_Printf("poly %i, numpoints = %d\n", i, poly->numPoints);
+ }
+}
+
+/*
+==================
+BotImport_Print
+==================
+*/
+static void QDECL BotImport_Print(int type, char *fmt, ...)
+{
+ char str[2048];
+ va_list ap;
+
+ va_start(ap, fmt);
+ Q_vsnprintf(str, sizeof(str), fmt, ap);
+ va_end(ap);
+
+ switch(type) {
+ case PRT_MESSAGE: {
+ Com_Printf("%s", str);
+ break;
+ }
+ case PRT_WARNING: {
+ Com_Printf(S_COLOR_YELLOW "Warning: %s", str);
+ break;
+ }
+ case PRT_ERROR: {
+ Com_Printf(S_COLOR_RED "Error: %s", str);
+ break;
+ }
+ case PRT_FATAL: {
+ Com_Printf(S_COLOR_RED "Fatal: %s", str);
+ break;
+ }
+ case PRT_EXIT: {
+ Com_Error(ERR_DROP, S_COLOR_RED "Exit: %s", str);
+ break;
+ }
+ default: {
+ Com_Printf("unknown print type\n");
+ break;
+ }
+ }
+}
+
+/*
+==================
+BotImport_Trace
+==================
+*/
+static void BotImport_Trace(bsp_trace_t *bsptrace, vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end, int passent, int contentmask) {
+ trace_t trace;
+
+ SV_Trace(&trace, start, mins, maxs, end, passent, contentmask, qfalse);
+ //copy the trace information
+ bsptrace->allsolid = trace.allsolid;
+ bsptrace->startsolid = trace.startsolid;
+ bsptrace->fraction = trace.fraction;
+ VectorCopy(trace.endpos, bsptrace->endpos);
+ bsptrace->plane.dist = trace.plane.dist;
+ VectorCopy(trace.plane.normal, bsptrace->plane.normal);
+ bsptrace->plane.signbits = trace.plane.signbits;
+ bsptrace->plane.type = trace.plane.type;
+ bsptrace->surface.value = trace.surfaceFlags;
+ bsptrace->ent = trace.entityNum;
+ bsptrace->exp_dist = 0;
+ bsptrace->sidenum = 0;
+ bsptrace->contents = 0;
+}
+
+/*
+==================
+BotImport_EntityTrace
+==================
+*/
+static void BotImport_EntityTrace(bsp_trace_t *bsptrace, vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end, int entnum, int contentmask) {
+ trace_t trace;
+
+ SV_ClipToEntity(&trace, start, mins, maxs, end, entnum, contentmask, qfalse);
+ //copy the trace information
+ bsptrace->allsolid = trace.allsolid;
+ bsptrace->startsolid = trace.startsolid;
+ bsptrace->fraction = trace.fraction;
+ VectorCopy(trace.endpos, bsptrace->endpos);
+ bsptrace->plane.dist = trace.plane.dist;
+ VectorCopy(trace.plane.normal, bsptrace->plane.normal);
+ bsptrace->plane.signbits = trace.plane.signbits;
+ bsptrace->plane.type = trace.plane.type;
+ bsptrace->surface.value = trace.surfaceFlags;
+ bsptrace->ent = trace.entityNum;
+ bsptrace->exp_dist = 0;
+ bsptrace->sidenum = 0;
+ bsptrace->contents = 0;
+}
+
+
+/*
+==================
+BotImport_PointContents
+==================
+*/
+static int BotImport_PointContents(vec3_t point) {
+ return SV_PointContents(point, -1);
+}
+
+/*
+==================
+BotImport_inPVS
+==================
+*/
+static int BotImport_inPVS(vec3_t p1, vec3_t p2) {
+ return SV_inPVS (p1, p2);
+}
+
+/*
+==================
+BotImport_BSPEntityData
+==================
+*/
+static char *BotImport_BSPEntityData(void) {
+ return CM_EntityString();
+}
+
+/*
+==================
+BotImport_BSPModelMinsMaxsOrigin
+==================
+*/
+static void BotImport_BSPModelMinsMaxsOrigin(int modelnum, vec3_t angles, vec3_t outmins, vec3_t outmaxs, vec3_t origin) {
+ clipHandle_t h;
+ vec3_t mins, maxs;
+ float max;
+ int i;
+
+ h = CM_InlineModel(modelnum);
+ CM_ModelBounds(h, mins, maxs);
+ //if the model is rotated
+ if ((angles[0] || angles[1] || angles[2])) {
+ // expand for rotation
+
+ max = RadiusFromBounds(mins, maxs);
+ for (i = 0; i < 3; i++) {
+ mins[i] = -max;
+ maxs[i] = max;
+ }
+ }
+ if (outmins) VectorCopy(mins, outmins);
+ if (outmaxs) VectorCopy(maxs, outmaxs);
+ if (origin) VectorClear(origin);
+}
+
+/*
+==================
+BotImport_GetMemory
+==================
+*/
+static void *BotImport_GetMemory(int size) {
+ void *ptr;
+
+ ptr = Z_TagMalloc( size, TAG_BOTLIB );
+ return ptr;
+}
+
+/*
+==================
+BotImport_FreeMemory
+==================
+*/
+static void BotImport_FreeMemory(void *ptr) {
+ Z_Free(ptr);
+}
+
+/*
+=================
+BotImport_HunkAlloc
+=================
+*/
+static void *BotImport_HunkAlloc( int size ) {
+ if( Hunk_CheckMark() ) {
+ Com_Error( ERR_DROP, "SV_Bot_HunkAlloc: Alloc with marks already set\n" );
+ }
+ return Hunk_Alloc( size, h_high );
+}
+
+/*
+==================
+BotImport_DebugPolygonCreate
+==================
+*/
+int BotImport_DebugPolygonCreate(int color, int numPoints, vec3_t *points) {
+ bot_debugpoly_t *poly;
+ int i;
+
+ if (!debugpolygons)
+ return 0;
+
+ for (i = 1; i < bot_maxdebugpolys; i++) {
+ if (!debugpolygons[i].inuse)
+ break;
+ }
+ if (i >= bot_maxdebugpolys)
+ return 0;
+ poly = &debugpolygons[i];
+ poly->inuse = qtrue;
+ poly->color = color;
+ poly->numPoints = numPoints;
+ Com_Memcpy(poly->points, points, numPoints * sizeof(vec3_t));
+ //
+ return i;
+}
+
+/*
+==================
+BotImport_DebugPolygonShow
+==================
+*/
+static void BotImport_DebugPolygonShow(int id, int color, int numPoints, vec3_t *points) {
+ bot_debugpoly_t *poly;
+
+ if (!debugpolygons) return;
+ poly = &debugpolygons[id];
+ poly->inuse = qtrue;
+ poly->color = color;
+ poly->numPoints = numPoints;
+ Com_Memcpy(poly->points, points, numPoints * sizeof(vec3_t));
+}
+
+/*
+==================
+BotImport_DebugPolygonDelete
+==================
+*/
+void BotImport_DebugPolygonDelete(int id)
+{
+ if (!debugpolygons) return;
+ debugpolygons[id].inuse = qfalse;
+}
+
+/*
+==================
+BotImport_DebugLineCreate
+==================
+*/
+static int BotImport_DebugLineCreate(void) {
+ vec3_t points[1];
+ return BotImport_DebugPolygonCreate(0, 0, points);
+}
+
+/*
+==================
+BotImport_DebugLineDelete
+==================
+*/
+static void BotImport_DebugLineDelete(int line) {
+ BotImport_DebugPolygonDelete(line);
+}
+
+/*
+==================
+BotImport_DebugLineShow
+==================
+*/
+static void BotImport_DebugLineShow(int line, vec3_t start, vec3_t end, int color) {
+ vec3_t points[4], dir, cross, up = {0, 0, 1};
+ float dot;
+
+ VectorCopy(start, points[0]);
+ VectorCopy(start, points[1]);
+ //points[1][2] -= 2;
+ VectorCopy(end, points[2]);
+ //points[2][2] -= 2;
+ VectorCopy(end, points[3]);
+
+
+ VectorSubtract(end, start, dir);
+ VectorNormalize(dir);
+ dot = DotProduct(dir, up);
+ if (dot > 0.99 || dot < -0.99) VectorSet(cross, 1, 0, 0);
+ else CrossProduct(dir, up, cross);
+
+ VectorNormalize(cross);
+
+ VectorMA(points[0], 2, cross, points[0]);
+ VectorMA(points[1], -2, cross, points[1]);
+ VectorMA(points[2], -2, cross, points[2]);
+ VectorMA(points[3], 2, cross, points[3]);
+
+ BotImport_DebugPolygonShow(line, color, 4, points);
+}
+
+/*
+==================
+SV_BotClientCommand
+==================
+*/
+static void BotClientCommand( int client, char *command ) {
+ SV_ExecuteClientCommand( &svs.clients[client], command, qtrue );
+}
+
+/*
+==================
+SV_BotFrame
+==================
+*/
+void SV_BotFrame( int time ) {
+ if (!bot_enable) return;
+ //NOTE: maybe the game is already shutdown
+ if (!gvm) return;
+ VM_Call( gvm, BOTAI_START_FRAME, time );
+}
+
+/*
+===============
+SV_BotLibSetup
+===============
+*/
+int SV_BotLibSetup( void ) {
+ if (!bot_enable) {
+ return 0;
+ }
+
+ if ( !botlib_export ) {
+ Com_Printf( S_COLOR_RED "Error: SV_BotLibSetup without SV_BotInitBotLib\n" );
+ return -1;
+ }
+
+ return botlib_export->BotLibSetup();
+}
+
+/*
+===============
+SV_ShutdownBotLib
+
+Called when either the entire server is being killed, or
+it is changing to a different game directory.
+===============
+*/
+int SV_BotLibShutdown( void ) {
+
+ if ( !botlib_export ) {
+ return -1;
+ }
+
+ return botlib_export->BotLibShutdown();
+}
+
+/*
+==================
+SV_BotInitCvars
+==================
+*/
+void SV_BotInitCvars(void) {
+
+ Cvar_Get("bot_enable", "1", 0); //enable the bot
+ Cvar_Get("bot_developer", "0", CVAR_CHEAT); //bot developer mode
+ Cvar_Get("bot_debug", "0", CVAR_CHEAT); //enable bot debugging
+ Cvar_Get("bot_maxdebugpolys", "2", 0); //maximum number of debug polys
+ Cvar_Get("bot_groundonly", "1", 0); //only show ground faces of areas
+ Cvar_Get("bot_reachability", "0", 0); //show all reachabilities to other areas
+ Cvar_Get("bot_visualizejumppads", "0", CVAR_CHEAT); //show jumppads
+ Cvar_Get("bot_forceclustering", "0", 0); //force cluster calculations
+ Cvar_Get("bot_forcereachability", "0", 0); //force reachability calculations
+ Cvar_Get("bot_forcewrite", "0", 0); //force writing aas file
+ Cvar_Get("bot_aasoptimize", "0", 0); //no aas file optimisation
+ Cvar_Get("bot_saveroutingcache", "0", 0); //save routing cache
+ Cvar_Get("bot_thinktime", "100", CVAR_CHEAT); //msec the bots thinks
+ Cvar_Get("bot_reloadcharacters", "0", 0); //reload the bot characters each time
+ Cvar_Get("bot_testichat", "0", 0); //test ichats
+ Cvar_Get("bot_testrchat", "0", 0); //test rchats
+ Cvar_Get("bot_testsolid", "0", CVAR_CHEAT); //test for solid areas
+ Cvar_Get("bot_testclusters", "0", CVAR_CHEAT); //test the AAS clusters
+ Cvar_Get("bot_fastchat", "0", 0); //fast chatting bots
+ Cvar_Get("bot_nochat", "0", 0); //disable chats
+ Cvar_Get("bot_pause", "0", CVAR_CHEAT); //pause the bots thinking
+ Cvar_Get("bot_report", "0", CVAR_CHEAT); //get a full report in ctf
+ Cvar_Get("bot_grapple", "0", 0); //enable grapple
+ Cvar_Get("bot_rocketjump", "1", 0); //enable rocket jumping
+ Cvar_Get("bot_challenge", "0", 0); //challenging bot
+ Cvar_Get("bot_minplayers", "0", 0); //minimum players in a team or the game
+ Cvar_Get("bot_interbreedchar", "", CVAR_CHEAT); //bot character used for interbreeding
+ Cvar_Get("bot_interbreedbots", "10", CVAR_CHEAT); //number of bots used for interbreeding
+ Cvar_Get("bot_interbreedcycle", "20", CVAR_CHEAT); //bot interbreeding cycle
+ Cvar_Get("bot_interbreedwrite", "", CVAR_CHEAT); //write interbreeded bots to this file
+}
+
+/*
+==================
+SV_BotInitBotLib
+==================
+*/
+void SV_BotInitBotLib(void) {
+ botlib_import_t botlib_import;
+
+ if (debugpolygons) Z_Free(debugpolygons);
+ bot_maxdebugpolys = Cvar_VariableIntegerValue("bot_maxdebugpolys");
+ debugpolygons = Z_Malloc(sizeof(bot_debugpoly_t) * bot_maxdebugpolys);
+
+ botlib_import.Print = BotImport_Print;
+ botlib_import.Trace = BotImport_Trace;
+ botlib_import.EntityTrace = BotImport_EntityTrace;
+ botlib_import.PointContents = BotImport_PointContents;
+ botlib_import.inPVS = BotImport_inPVS;
+ botlib_import.BSPEntityData = BotImport_BSPEntityData;
+ botlib_import.BSPModelMinsMaxsOrigin = BotImport_BSPModelMinsMaxsOrigin;
+ botlib_import.BotClientCommand = BotClientCommand;
+
+ //memory management
+ botlib_import.GetMemory = BotImport_GetMemory;
+ botlib_import.FreeMemory = BotImport_FreeMemory;
+ botlib_import.AvailableMemory = Z_AvailableMemory;
+ botlib_import.HunkAlloc = BotImport_HunkAlloc;
+
+ // file system access
+ botlib_import.FS_FOpenFile = FS_FOpenFileByMode;
+ botlib_import.FS_Read = FS_Read2;
+ botlib_import.FS_Write = FS_Write;
+ botlib_import.FS_FCloseFile = FS_FCloseFile;
+ botlib_import.FS_Seek = FS_Seek;
+
+ //debug lines
+ botlib_import.DebugLineCreate = BotImport_DebugLineCreate;
+ botlib_import.DebugLineDelete = BotImport_DebugLineDelete;
+ botlib_import.DebugLineShow = BotImport_DebugLineShow;
+
+ //debug polygons
+ botlib_import.DebugPolygonCreate = BotImport_DebugPolygonCreate;
+ botlib_import.DebugPolygonDelete = BotImport_DebugPolygonDelete;
+
+ botlib_export = (botlib_export_t *)GetBotLibAPI( BOTLIB_API_VERSION, &botlib_import );
+ assert(botlib_export); // somehow we end up with a zero import.
+}
+
+
+//
+// * * * BOT AI CODE IS BELOW THIS POINT * * *
+//
+
+/*
+==================
+SV_BotGetConsoleMessage
+==================
+*/
+int SV_BotGetConsoleMessage( int client, char *buf, int size )
+{
+ client_t *cl;
+ int index;
+
+ cl = &svs.clients[client];
+ cl->lastPacketTime = svs.time;
+
+ if ( cl->reliableAcknowledge == cl->reliableSequence ) {
+ return qfalse;
+ }
+
+ cl->reliableAcknowledge++;
+ index = cl->reliableAcknowledge & ( MAX_RELIABLE_COMMANDS - 1 );
+
+ if ( !cl->reliableCommands[index][0] ) {
+ return qfalse;
+ }
+
+ Q_strncpyz( buf, cl->reliableCommands[index], size );
+ return qtrue;
+}
+
+#if 0
+/*
+==================
+EntityInPVS
+==================
+*/
+int EntityInPVS( int client, int entityNum ) {
+ client_t *cl;
+ clientSnapshot_t *frame;
+ int i;
+
+ cl = &svs.clients[client];
+ frame = &cl->frames[cl->netchan.outgoingSequence & PACKET_MASK];
+ for ( i = 0; i < frame->num_entities; i++ ) {
+ if ( svs.snapshotEntities[(frame->first_entity + i) % svs.numSnapshotEntities].number == entityNum ) {
+ return qtrue;
+ }
+ }
+ return qfalse;
+}
+#endif
+
+/*
+==================
+SV_BotGetSnapshotEntity
+==================
+*/
+int SV_BotGetSnapshotEntity( int client, int sequence ) {
+ client_t *cl;
+ clientSnapshot_t *frame;
+
+ cl = &svs.clients[client];
+ frame = &cl->frames[cl->netchan.outgoingSequence & PACKET_MASK];
+ if (sequence < 0 || sequence >= frame->num_entities) {
+ return -1;
+ }
+ return svs.snapshotEntities[(frame->first_entity + sequence) % svs.numSnapshotEntities].number;
+}
+
diff --git a/code/server/sv_ccmds.c b/code/server/sv_ccmds.c
new file mode 100644
index 0000000..0b9d578
--- /dev/null
+++ b/code/server/sv_ccmds.c
@@ -0,0 +1,1319 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "server.h"
+
+/*
+===============================================================================
+
+OPERATOR CONSOLE ONLY COMMANDS
+
+These commands can only be entered from stdin or by a remote operator datagram
+===============================================================================
+*/
+
+
+/*
+==================
+SV_GetPlayerByHandle
+
+Returns the player with player id or name from Cmd_Argv(1)
+==================
+*/
+static client_t *SV_GetPlayerByHandle( void ) {
+ client_t *cl;
+ int i;
+ char *s;
+ char cleanName[64];
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ return NULL;
+ }
+
+ if ( Cmd_Argc() < 2 ) {
+ Com_Printf( "No player specified.\n" );
+ return NULL;
+ }
+
+ s = Cmd_Argv(1);
+
+ // Check whether this is a numeric player handle
+ for(i = 0; s[i] >= '0' && s[i] <= '9'; i++);
+
+ if(!s[i])
+ {
+ int plid = atoi(s);
+
+ // Check for numeric playerid match
+ if(plid >= 0 && plid < sv_maxclients->integer)
+ {
+ cl = &svs.clients[plid];
+
+ if(cl->state)
+ return cl;
+ }
+ }
+
+ // check for a name match
+ for ( i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++ ) {
+ if ( !cl->state ) {
+ continue;
+ }
+ if ( !Q_stricmp( cl->name, s ) ) {
+ return cl;
+ }
+
+ Q_strncpyz( cleanName, cl->name, sizeof(cleanName) );
+ Q_CleanStr( cleanName );
+ if ( !Q_stricmp( cleanName, s ) ) {
+ return cl;
+ }
+ }
+
+ Com_Printf( "Player %s is not on the server\n", s );
+
+ return NULL;
+}
+
+/*
+==================
+SV_GetPlayerByNum
+
+Returns the player with idnum from Cmd_Argv(1)
+==================
+*/
+static client_t *SV_GetPlayerByNum( void ) {
+ client_t *cl;
+ int i;
+ int idnum;
+ char *s;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ return NULL;
+ }
+
+ if ( Cmd_Argc() < 2 ) {
+ Com_Printf( "No player specified.\n" );
+ return NULL;
+ }
+
+ s = Cmd_Argv(1);
+
+ for (i = 0; s[i]; i++) {
+ if (s[i] < '0' || s[i] > '9') {
+ Com_Printf( "Bad slot number: %s\n", s);
+ return NULL;
+ }
+ }
+ idnum = atoi( s );
+ if ( idnum < 0 || idnum >= sv_maxclients->integer ) {
+ Com_Printf( "Bad client slot: %i\n", idnum );
+ return NULL;
+ }
+
+ cl = &svs.clients[idnum];
+ if ( !cl->state ) {
+ Com_Printf( "Client %i is not active\n", idnum );
+ return NULL;
+ }
+ return cl;
+}
+
+//=========================================================
+
+
+/*
+==================
+SV_Map_f
+
+Restart the server on a different map
+==================
+*/
+static void SV_Map_f( void ) {
+ char *cmd;
+ char *map;
+ qboolean killBots, cheat;
+ char expanded[MAX_QPATH];
+ char mapname[MAX_QPATH];
+
+ map = Cmd_Argv(1);
+ if ( !map ) {
+ return;
+ }
+
+ // make sure the level exists before trying to change, so that
+ // a typo at the server console won't end the game
+ Com_sprintf (expanded, sizeof(expanded), "maps/%s.bsp", map);
+ if ( FS_ReadFile (expanded, NULL) == -1 ) {
+ Com_Printf ("Can't find map %s\n", expanded);
+ return;
+ }
+
+ // force latched values to get set
+ Cvar_Get ("g_gametype", "0", CVAR_SERVERINFO | CVAR_USERINFO | CVAR_LATCH );
+
+ cmd = Cmd_Argv(0);
+ if( Q_stricmpn( cmd, "sp", 2 ) == 0 ) {
+ Cvar_SetValue( "g_gametype", GT_SINGLE_PLAYER );
+ Cvar_SetValue( "g_doWarmup", 0 );
+ // may not set sv_maxclients directly, always set latched
+ Cvar_SetLatched( "sv_maxclients", "8" );
+ cmd += 2;
+ cheat = qfalse;
+ killBots = qtrue;
+ }
+ else {
+ if ( !Q_stricmp( cmd, "devmap" ) || !Q_stricmp( cmd, "spdevmap" ) ) {
+ cheat = qtrue;
+ killBots = qtrue;
+ } else {
+ cheat = qfalse;
+ killBots = qfalse;
+ }
+ if( sv_gametype->integer == GT_SINGLE_PLAYER ) {
+ Cvar_SetValue( "g_gametype", GT_FFA );
+ }
+ }
+
+ // save the map name here cause on a map restart we reload the q3config.cfg
+ // and thus nuke the arguments of the map command
+ Q_strncpyz(mapname, map, sizeof(mapname));
+
+ // start up the map
+ SV_SpawnServer( mapname, killBots );
+
+ // set the cheat value
+ // if the level was started with "map <levelname>", then
+ // cheats will not be allowed. If started with "devmap <levelname>"
+ // then cheats will be allowed
+ if ( cheat ) {
+ Cvar_Set( "sv_cheats", "1" );
+ } else {
+ Cvar_Set( "sv_cheats", "0" );
+ }
+}
+
+/*
+================
+SV_MapRestart_f
+
+Completely restarts a level, but doesn't send a new gamestate to the clients.
+This allows fair starts with variable load times.
+================
+*/
+static void SV_MapRestart_f( void ) {
+ int i;
+ client_t *client;
+ char *denied;
+ qboolean isBot;
+ int delay;
+
+ // make sure we aren't restarting twice in the same frame
+ if ( com_frameTime == sv.serverId ) {
+ return;
+ }
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( sv.restartTime ) {
+ return;
+ }
+
+ if (Cmd_Argc() > 1 ) {
+ delay = atoi( Cmd_Argv(1) );
+ }
+ else {
+ delay = 5;
+ }
+ if( delay && !Cvar_VariableValue("g_doWarmup") ) {
+ sv.restartTime = sv.time + delay * 1000;
+ SV_SetConfigstring( CS_WARMUP, va("%i", sv.restartTime) );
+ return;
+ }
+
+ // check for changes in variables that can't just be restarted
+ // check for maxclients change
+ if ( sv_maxclients->modified || sv_gametype->modified ) {
+ char mapname[MAX_QPATH];
+
+ Com_Printf( "variable change -- restarting.\n" );
+ // restart the map the slow way
+ Q_strncpyz( mapname, Cvar_VariableString( "mapname" ), sizeof( mapname ) );
+
+ SV_SpawnServer( mapname, qfalse );
+ return;
+ }
+
+ // toggle the server bit so clients can detect that a
+ // map_restart has happened
+ svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;
+
+ // generate a new serverid
+ // TTimo - don't update restartedserverId there, otherwise we won't deal correctly with multiple map_restart
+ sv.serverId = com_frameTime;
+ Cvar_Set( "sv_serverid", va("%i", sv.serverId ) );
+
+ // if a map_restart occurs while a client is changing maps, we need
+ // to give them the correct time so that when they finish loading
+ // they don't violate the backwards time check in cl_cgame.c
+ for (i=0 ; i<sv_maxclients->integer ; i++) {
+ if (svs.clients[i].state == CS_PRIMED) {
+ svs.clients[i].oldServerTime = sv.restartTime;
+ }
+ }
+
+ // reset all the vm data in place without changing memory allocation
+ // note that we do NOT set sv.state = SS_LOADING, so configstrings that
+ // had been changed from their default values will generate broadcast updates
+ sv.state = SS_LOADING;
+ sv.restarting = qtrue;
+
+ SV_RestartGameProgs();
+
+ // run a few frames to allow everything to settle
+ for (i = 0; i < 3; i++)
+ {
+ VM_Call (gvm, GAME_RUN_FRAME, sv.time);
+ sv.time += 100;
+ svs.time += 100;
+ }
+
+ sv.state = SS_GAME;
+ sv.restarting = qfalse;
+
+ // connect and begin all the clients
+ for (i=0 ; i<sv_maxclients->integer ; i++) {
+ client = &svs.clients[i];
+
+ // send the new gamestate to all connected clients
+ if ( client->state < CS_CONNECTED) {
+ continue;
+ }
+
+ if ( client->netchan.remoteAddress.type == NA_BOT ) {
+ isBot = qtrue;
+ } else {
+ isBot = qfalse;
+ }
+
+ // add the map_restart command
+ SV_AddServerCommand( client, "map_restart\n" );
+
+ // connect the client again, without the firstTime flag
+ denied = VM_ExplicitArgPtr( gvm, VM_Call( gvm, GAME_CLIENT_CONNECT, i, qfalse, isBot ) );
+ if ( denied ) {
+ // this generally shouldn't happen, because the client
+ // was connected before the level change
+ SV_DropClient( client, denied );
+ Com_Printf( "SV_MapRestart_f(%d): dropped client %i - denied!\n", delay, i );
+ continue;
+ }
+
+ client->state = CS_ACTIVE;
+
+ SV_ClientEnterWorld( client, &client->lastUsercmd );
+ }
+
+ // run another frame to allow things to look at all the players
+ VM_Call (gvm, GAME_RUN_FRAME, sv.time);
+ sv.time += 100;
+ svs.time += 100;
+}
+
+//===============================================================
+
+/*
+==================
+SV_Kick_f
+
+Kick a user off of the server FIXME: move to game
+==================
+*/
+static void SV_Kick_f( void ) {
+ client_t *cl;
+ int i;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( Cmd_Argc() != 2 ) {
+ Com_Printf ("Usage: kick <player name>\nkick all = kick everyone\nkick allbots = kick all bots\n");
+ return;
+ }
+
+ cl = SV_GetPlayerByHandle();
+ if ( !cl ) {
+ if ( !Q_stricmp(Cmd_Argv(1), "all") ) {
+ for ( i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++ ) {
+ if ( !cl->state ) {
+ continue;
+ }
+ if( cl->netchan.remoteAddress.type == NA_LOOPBACK ) {
+ continue;
+ }
+ SV_DropClient( cl, "was kicked" );
+ cl->lastPacketTime = svs.time; // in case there is a funny zombie
+ }
+ }
+ else if ( !Q_stricmp(Cmd_Argv(1), "allbots") ) {
+ for ( i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++ ) {
+ if ( !cl->state ) {
+ continue;
+ }
+ if( cl->netchan.remoteAddress.type != NA_BOT ) {
+ continue;
+ }
+ SV_DropClient( cl, "was kicked" );
+ cl->lastPacketTime = svs.time; // in case there is a funny zombie
+ }
+ }
+ return;
+ }
+ if( cl->netchan.remoteAddress.type == NA_LOOPBACK ) {
+ SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
+ return;
+ }
+
+ SV_DropClient( cl, "was kicked" );
+ cl->lastPacketTime = svs.time; // in case there is a funny zombie
+}
+
+#ifndef STANDALONE
+// these functions require the auth server which of course is not available anymore for stand-alone games.
+
+/*
+==================
+SV_Ban_f
+
+Ban a user from being able to play on this server through the auth
+server
+==================
+*/
+static void SV_Ban_f( void ) {
+ client_t *cl;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( Cmd_Argc() != 2 ) {
+ Com_Printf ("Usage: banUser <player name>\n");
+ return;
+ }
+
+ cl = SV_GetPlayerByHandle();
+
+ if (!cl) {
+ return;
+ }
+
+ if( cl->netchan.remoteAddress.type == NA_LOOPBACK ) {
+ SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
+ return;
+ }
+
+ // look up the authorize server's IP
+ if ( !svs.authorizeAddress.ip[0] && svs.authorizeAddress.type != NA_BAD ) {
+ Com_Printf( "Resolving %s\n", AUTHORIZE_SERVER_NAME );
+ if ( !NET_StringToAdr( AUTHORIZE_SERVER_NAME, &svs.authorizeAddress, NA_IP ) ) {
+ Com_Printf( "Couldn't resolve address\n" );
+ return;
+ }
+ svs.authorizeAddress.port = BigShort( PORT_AUTHORIZE );
+ Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME,
+ svs.authorizeAddress.ip[0], svs.authorizeAddress.ip[1],
+ svs.authorizeAddress.ip[2], svs.authorizeAddress.ip[3],
+ BigShort( svs.authorizeAddress.port ) );
+ }
+
+ // otherwise send their ip to the authorize server
+ if ( svs.authorizeAddress.type != NA_BAD ) {
+ NET_OutOfBandPrint( NS_SERVER, svs.authorizeAddress,
+ "banUser %i.%i.%i.%i", cl->netchan.remoteAddress.ip[0], cl->netchan.remoteAddress.ip[1],
+ cl->netchan.remoteAddress.ip[2], cl->netchan.remoteAddress.ip[3] );
+ Com_Printf("%s was banned from coming back\n", cl->name);
+ }
+}
+
+/*
+==================
+SV_BanNum_f
+
+Ban a user from being able to play on this server through the auth
+server
+==================
+*/
+static void SV_BanNum_f( void ) {
+ client_t *cl;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( Cmd_Argc() != 2 ) {
+ Com_Printf ("Usage: banClient <client number>\n");
+ return;
+ }
+
+ cl = SV_GetPlayerByNum();
+ if ( !cl ) {
+ return;
+ }
+ if( cl->netchan.remoteAddress.type == NA_LOOPBACK ) {
+ SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
+ return;
+ }
+
+ // look up the authorize server's IP
+ if ( !svs.authorizeAddress.ip[0] && svs.authorizeAddress.type != NA_BAD ) {
+ Com_Printf( "Resolving %s\n", AUTHORIZE_SERVER_NAME );
+ if ( !NET_StringToAdr( AUTHORIZE_SERVER_NAME, &svs.authorizeAddress, NA_IP ) ) {
+ Com_Printf( "Couldn't resolve address\n" );
+ return;
+ }
+ svs.authorizeAddress.port = BigShort( PORT_AUTHORIZE );
+ Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME,
+ svs.authorizeAddress.ip[0], svs.authorizeAddress.ip[1],
+ svs.authorizeAddress.ip[2], svs.authorizeAddress.ip[3],
+ BigShort( svs.authorizeAddress.port ) );
+ }
+
+ // otherwise send their ip to the authorize server
+ if ( svs.authorizeAddress.type != NA_BAD ) {
+ NET_OutOfBandPrint( NS_SERVER, svs.authorizeAddress,
+ "banUser %i.%i.%i.%i", cl->netchan.remoteAddress.ip[0], cl->netchan.remoteAddress.ip[1],
+ cl->netchan.remoteAddress.ip[2], cl->netchan.remoteAddress.ip[3] );
+ Com_Printf("%s was banned from coming back\n", cl->name);
+ }
+}
+#endif
+
+/*
+==================
+SV_RehashBans_f
+
+Load saved bans from file.
+==================
+*/
+static void SV_RehashBans_f(void)
+{
+ int index, filelen;
+ fileHandle_t readfrom;
+ char *textbuf, *curpos, *maskpos, *newlinepos, *endpos;
+ char filepath[MAX_QPATH];
+
+ serverBansCount = 0;
+
+ if(!sv_banFile->string || !*sv_banFile->string)
+ return;
+
+ if(!(curpos = Cvar_VariableString("fs_game")) || !*curpos)
+ curpos = BASEGAME;
+
+ Com_sprintf(filepath, sizeof(filepath), "%s/%s", curpos, sv_banFile->string);
+
+ if((filelen = FS_SV_FOpenFileRead(filepath, &readfrom)) >= 0)
+ {
+ if(filelen < 2)
+ {
+ // Don't bother if file is too short.
+ FS_FCloseFile(readfrom);
+ return;
+ }
+
+ curpos = textbuf = Z_Malloc(filelen);
+
+ filelen = FS_Read(textbuf, filelen, readfrom);
+ FS_FCloseFile(readfrom);
+
+ endpos = textbuf + filelen;
+
+ for(index = 0; index < SERVER_MAXBANS && curpos + 2 < endpos; index++)
+ {
+ // find the end of the address string
+ for(maskpos = curpos + 2; maskpos < endpos && *maskpos != ' '; maskpos++);
+
+ if(maskpos + 1 >= endpos)
+ break;
+
+ *maskpos = '\0';
+ maskpos++;
+
+ // find the end of the subnet specifier
+ for(newlinepos = maskpos; newlinepos < endpos && *newlinepos != '\n'; newlinepos++);
+
+ if(newlinepos >= endpos)
+ break;
+
+ *newlinepos = '\0';
+
+ if(NET_StringToAdr(curpos + 2, &serverBans[index].ip, NA_UNSPEC))
+ {
+ serverBans[index].isexception = (curpos[0] != '0');
+ serverBans[index].subnet = atoi(maskpos);
+
+ if(serverBans[index].ip.type == NA_IP &&
+ (serverBans[index].subnet < 1 || serverBans[index].subnet > 32))
+ {
+ serverBans[index].subnet = 32;
+ }
+ else if(serverBans[index].ip.type == NA_IP6 &&
+ (serverBans[index].subnet < 1 || serverBans[index].subnet > 128))
+ {
+ serverBans[index].subnet = 128;
+ }
+ }
+
+ curpos = newlinepos + 1;
+ }
+
+ serverBansCount = index;
+
+ Z_Free(textbuf);
+ }
+}
+
+/*
+==================
+SV_WriteBans_f
+
+Save bans to file.
+==================
+*/
+static void SV_WriteBans(void)
+{
+ int index;
+ fileHandle_t writeto;
+ char *curpos, filepath[MAX_QPATH];
+
+ if(!sv_banFile->string || !*sv_banFile->string)
+ return;
+
+ if(!(curpos = Cvar_VariableString("fs_game")) || !*curpos)
+ curpos = BASEGAME;
+
+ Com_sprintf(filepath, sizeof(filepath), "%s/%s", curpos, sv_banFile->string);
+
+ if((writeto = FS_SV_FOpenFileWrite(filepath)))
+ {
+ char writebuf[128];
+ serverBan_t *curban;
+
+ for(index = 0; index < serverBansCount; index++)
+ {
+ curban = &serverBans[index];
+
+ Com_sprintf(writebuf, sizeof(writebuf), "%d %s %d\n",
+ curban->isexception, NET_AdrToString(curban->ip), curban->subnet);
+ FS_Write(writebuf, strlen(writebuf), writeto);
+ }
+
+ FS_FCloseFile(writeto);
+ }
+}
+
+/*
+==================
+SV_DelBanEntryFromList
+
+Remove a ban or an exception from the list.
+==================
+*/
+
+static qboolean SV_DelBanEntryFromList(int index)
+{
+ if(index == serverBansCount - 1)
+ serverBansCount--;
+ else if(index < sizeof(serverBans) / sizeof(*serverBans) - 1)
+ {
+ memmove(serverBans + index, serverBans + index + 1, (serverBansCount - index - 1) * sizeof(*serverBans));
+ serverBansCount--;
+ }
+ else
+ return qtrue;
+
+ return qfalse;
+}
+
+/*
+==================
+SV_ParseCIDRNotation
+
+Parse a CIDR notation type string and return a netadr_t and suffix by reference
+==================
+*/
+
+static qboolean SV_ParseCIDRNotation(netadr_t *dest, int *mask, char *adrstr)
+{
+ char *suffix;
+
+ suffix = strchr(adrstr, '/');
+ if(suffix)
+ {
+ *suffix = '\0';
+ suffix++;
+ }
+
+ if(!NET_StringToAdr(adrstr, dest, NA_UNSPEC))
+ return qtrue;
+
+ if(suffix)
+ {
+ *mask = atoi(suffix);
+
+ if(dest->type == NA_IP)
+ {
+ if(*mask < 1 || *mask > 32)
+ *mask = 32;
+ }
+ else
+ {
+ if(*mask < 1 || *mask > 128)
+ *mask = 128;
+ }
+ }
+ else if(dest->type == NA_IP)
+ *mask = 32;
+ else
+ *mask = 128;
+
+ return qfalse;
+}
+
+/*
+==================
+SV_AddBanToList
+
+Ban a user from being able to play on this server based on his ip address.
+==================
+*/
+
+static void SV_AddBanToList(qboolean isexception)
+{
+ char *banstring;
+ char addy2[NET_ADDRSTRMAXLEN];
+ netadr_t ip;
+ int index, argc, mask;
+ serverBan_t *curban;
+
+ argc = Cmd_Argc();
+
+ if(argc < 2 || argc > 3)
+ {
+ Com_Printf ("Usage: %s (ip[/subnet] | clientnum [subnet])\n", Cmd_Argv(0));
+ return;
+ }
+
+ if(serverBansCount > sizeof(serverBans) / sizeof(*serverBans))
+ {
+ Com_Printf ("Error: Maximum number of bans/exceptions exceeded.\n");
+ return;
+ }
+
+ banstring = Cmd_Argv(1);
+
+ if(strchr(banstring, '.') || strchr(banstring, ':'))
+ {
+ // This is an ip address, not a client num.
+
+ if(SV_ParseCIDRNotation(&ip, &mask, banstring))
+ {
+ Com_Printf("Error: Invalid address %s\n", banstring);
+ return;
+ }
+ }
+ else
+ {
+ client_t *cl;
+
+ // client num.
+ if(!com_sv_running->integer)
+ {
+ Com_Printf("Server is not running.\n");
+ return;
+ }
+
+ cl = SV_GetPlayerByNum();
+
+ if(!cl)
+ {
+ Com_Printf("Error: Playernum %s does not exist.\n", Cmd_Argv(1));
+ return;
+ }
+
+ ip = cl->netchan.remoteAddress;
+
+ if(argc == 3)
+ {
+ mask = atoi(Cmd_Argv(2));
+
+ if(ip.type == NA_IP)
+ {
+ if(mask < 1 || mask > 32)
+ mask = 32;
+ }
+ else
+ {
+ if(mask < 1 || mask > 128)
+ mask = 128;
+ }
+ }
+ else
+ mask = (ip.type == NA_IP6) ? 128 : 32;
+ }
+
+ if(ip.type != NA_IP && ip.type != NA_IP6)
+ {
+ Com_Printf("Error: Can ban players connected via the internet only.\n");
+ return;
+ }
+
+ // first check whether a conflicting ban exists that would supersede the new one.
+ for(index = 0; index < serverBansCount; index++)
+ {
+ curban = &serverBans[index];
+
+ if(curban->subnet <= mask)
+ {
+ if((curban->isexception || !isexception) && NET_CompareBaseAdrMask(curban->ip, ip, curban->subnet))
+ {
+ Q_strncpyz(addy2, NET_AdrToString(ip), sizeof(addy2));
+
+ Com_Printf("Error: %s %s/%d supersedes %s %s/%d\n", curban->isexception ? "Exception" : "Ban",
+ NET_AdrToString(curban->ip), curban->subnet,
+ isexception ? "exception" : "ban", addy2, mask);
+ return;
+ }
+ }
+ if(curban->subnet >= mask)
+ {
+ if(!curban->isexception && isexception && NET_CompareBaseAdrMask(curban->ip, ip, mask))
+ {
+ Q_strncpyz(addy2, NET_AdrToString(curban->ip), sizeof(addy2));
+
+ Com_Printf("Error: %s %s/%d supersedes already existing %s %s/%d\n", isexception ? "Exception" : "Ban",
+ NET_AdrToString(ip), mask,
+ curban->isexception ? "exception" : "ban", addy2, curban->subnet);
+ return;
+ }
+ }
+ }
+
+ // now delete bans that are superseded by the new one
+ index = 0;
+ while(index < serverBansCount)
+ {
+ curban = &serverBans[index];
+
+ if(curban->subnet > mask && (!curban->isexception || isexception) && NET_CompareBaseAdrMask(curban->ip, ip, mask))
+ SV_DelBanEntryFromList(index);
+ else
+ index++;
+ }
+
+ serverBans[serverBansCount].ip = ip;
+ serverBans[serverBansCount].subnet = mask;
+ serverBans[serverBansCount].isexception = isexception;
+
+ serverBansCount++;
+
+ SV_WriteBans();
+
+ Com_Printf("Added %s: %s/%d\n", isexception ? "ban exception" : "ban",
+ NET_AdrToString(ip), mask);
+}
+
+/*
+==================
+SV_DelBanFromList
+
+Remove a ban or an exception from the list.
+==================
+*/
+
+static void SV_DelBanFromList(qboolean isexception)
+{
+ int index, count = 0, todel, mask;
+ netadr_t ip;
+ char *banstring;
+
+ if(Cmd_Argc() != 2)
+ {
+ Com_Printf ("Usage: %s (ip[/subnet] | num)\n", Cmd_Argv(0));
+ return;
+ }
+
+ banstring = Cmd_Argv(1);
+
+ if(strchr(banstring, '.') || strchr(banstring, ':'))
+ {
+ serverBan_t *curban;
+
+ if(SV_ParseCIDRNotation(&ip, &mask, banstring))
+ {
+ Com_Printf("Error: Invalid address %s\n", banstring);
+ return;
+ }
+
+ index = 0;
+
+ while(index < serverBansCount)
+ {
+ curban = &serverBans[index];
+
+ if(curban->isexception == isexception &&
+ curban->subnet >= mask &&
+ NET_CompareBaseAdrMask(curban->ip, ip, mask))
+ {
+ Com_Printf("Deleting %s %s/%d\n",
+ isexception ? "exception" : "ban",
+ NET_AdrToString(curban->ip), curban->subnet);
+
+ SV_DelBanEntryFromList(index);
+ }
+ else
+ index++;
+ }
+ }
+ else
+ {
+ todel = atoi(Cmd_Argv(1));
+
+ if(todel < 1 || todel > serverBansCount)
+ {
+ Com_Printf("Error: Invalid ban number given\n");
+ return;
+ }
+
+ for(index = 0; index < serverBansCount; index++)
+ {
+ if(serverBans[index].isexception == isexception)
+ {
+ count++;
+
+ if(count == todel)
+ {
+ Com_Printf("Deleting %s %s/%d\n",
+ isexception ? "exception" : "ban",
+ NET_AdrToString(serverBans[index].ip), serverBans[index].subnet);
+
+ SV_DelBanEntryFromList(index);
+
+ break;
+ }
+ }
+ }
+ }
+
+ SV_WriteBans();
+}
+
+
+/*
+==================
+SV_ListBans_f
+
+List all bans and exceptions on console
+==================
+*/
+
+static void SV_ListBans_f(void)
+{
+ int index, count;
+ serverBan_t *ban;
+
+ // List all bans
+ for(index = count = 0; index < serverBansCount; index++)
+ {
+ ban = &serverBans[index];
+ if(!ban->isexception)
+ {
+ count++;
+
+ Com_Printf("Ban #%d: %s/%d\n", count,
+ NET_AdrToString(ban->ip), ban->subnet);
+ }
+ }
+ // List all exceptions
+ for(index = count = 0; index < serverBansCount; index++)
+ {
+ ban = &serverBans[index];
+ if(ban->isexception)
+ {
+ count++;
+
+ Com_Printf("Except #%d: %s/%d\n", count,
+ NET_AdrToString(ban->ip), ban->subnet);
+ }
+ }
+}
+
+/*
+==================
+SV_FlushBans_f
+
+Delete all bans and exceptions.
+==================
+*/
+
+static void SV_FlushBans_f(void)
+{
+ serverBansCount = 0;
+
+ // empty the ban file.
+ SV_WriteBans();
+
+ Com_Printf("All bans and exceptions have been deleted.\n");
+}
+
+static void SV_BanAddr_f(void)
+{
+ SV_AddBanToList(qfalse);
+}
+
+static void SV_ExceptAddr_f(void)
+{
+ SV_AddBanToList(qtrue);
+}
+
+static void SV_BanDel_f(void)
+{
+ SV_DelBanFromList(qfalse);
+}
+
+static void SV_ExceptDel_f(void)
+{
+ SV_DelBanFromList(qtrue);
+}
+
+/*
+==================
+SV_KickNum_f
+
+Kick a user off of the server FIXME: move to game
+==================
+*/
+static void SV_KickNum_f( void ) {
+ client_t *cl;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( Cmd_Argc() != 2 ) {
+ Com_Printf ("Usage: kicknum <client number>\n");
+ return;
+ }
+
+ cl = SV_GetPlayerByNum();
+ if ( !cl ) {
+ return;
+ }
+ if( cl->netchan.remoteAddress.type == NA_LOOPBACK ) {
+ SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
+ return;
+ }
+
+ SV_DropClient( cl, "was kicked" );
+ cl->lastPacketTime = svs.time; // in case there is a funny zombie
+}
+
+/*
+================
+SV_Status_f
+================
+*/
+static void SV_Status_f( void ) {
+ int i, j, l;
+ client_t *cl;
+ playerState_t *ps;
+ const char *s;
+ int ping;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ Com_Printf ("map: %s\n", sv_mapname->string );
+
+ Com_Printf ("num score ping name lastmsg address qport rate\n");
+ Com_Printf ("--- ----- ---- --------------- ------- --------------------- ----- -----\n");
+ for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++)
+ {
+ if (!cl->state)
+ continue;
+ Com_Printf ("%3i ", i);
+ ps = SV_GameClientNum( i );
+ Com_Printf ("%5i ", ps->persistant[PERS_SCORE]);
+
+ if (cl->state == CS_CONNECTED)
+ Com_Printf ("CNCT ");
+ else if (cl->state == CS_ZOMBIE)
+ Com_Printf ("ZMBI ");
+ else
+ {
+ ping = cl->ping < 9999 ? cl->ping : 9999;
+ Com_Printf ("%4i ", ping);
+ }
+
+ Com_Printf ("%s", cl->name);
+ // TTimo adding a ^7 to reset the color
+ // NOTE: colored names in status breaks the padding (WONTFIX)
+ Com_Printf ("^7");
+ l = 16 - strlen(cl->name);
+ for (j=0 ; j<l ; j++)
+ Com_Printf (" ");
+
+ Com_Printf ("%7i ", svs.time - cl->lastPacketTime );
+
+ s = NET_AdrToString( cl->netchan.remoteAddress );
+ Com_Printf ("%s", s);
+ l = 22 - strlen(s);
+ for (j=0 ; j<l ; j++)
+ Com_Printf (" ");
+
+ Com_Printf ("%5i", cl->netchan.qport);
+
+ Com_Printf (" %5i", cl->rate);
+
+ Com_Printf ("\n");
+ }
+ Com_Printf ("\n");
+}
+
+/*
+==================
+SV_ConSay_f
+==================
+*/
+static void SV_ConSay_f(void) {
+ char *p;
+ char text[1024];
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( Cmd_Argc () < 2 ) {
+ return;
+ }
+
+ strcpy (text, "console: ");
+ p = Cmd_Args();
+
+ if ( *p == '"' ) {
+ p++;
+ p[strlen(p)-1] = 0;
+ }
+
+ strcat(text, p);
+
+ SV_SendServerCommand(NULL, "chat \"%s\"", text);
+}
+
+
+/*
+==================
+SV_Heartbeat_f
+
+Also called by SV_DropClient, SV_DirectConnect, and SV_SpawnServer
+==================
+*/
+void SV_Heartbeat_f( void ) {
+ svs.nextHeartbeatTime = -9999999;
+}
+
+
+/*
+===========
+SV_Serverinfo_f
+
+Examine the serverinfo string
+===========
+*/
+static void SV_Serverinfo_f( void ) {
+ Com_Printf ("Server info settings:\n");
+ Info_Print ( Cvar_InfoString( CVAR_SERVERINFO ) );
+}
+
+
+/*
+===========
+SV_Systeminfo_f
+
+Examine or change the serverinfo string
+===========
+*/
+static void SV_Systeminfo_f( void ) {
+ Com_Printf ("System info settings:\n");
+ Info_Print ( Cvar_InfoString( CVAR_SYSTEMINFO ) );
+}
+
+
+/*
+===========
+SV_DumpUser_f
+
+Examine all a users info strings FIXME: move to game
+===========
+*/
+static void SV_DumpUser_f( void ) {
+ client_t *cl;
+
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ if ( Cmd_Argc() != 2 ) {
+ Com_Printf ("Usage: info <userid>\n");
+ return;
+ }
+
+ cl = SV_GetPlayerByHandle();
+ if ( !cl ) {
+ return;
+ }
+
+ Com_Printf( "userinfo\n" );
+ Com_Printf( "--------\n" );
+ Info_Print( cl->userinfo );
+}
+
+
+/*
+=================
+SV_KillServer
+=================
+*/
+static void SV_KillServer_f( void ) {
+ SV_Shutdown( "killserver" );
+}
+
+//===========================================================
+
+/*
+==================
+SV_CompleteMapName
+==================
+*/
+static void SV_CompleteMapName( char *args, int argNum ) {
+ if( argNum == 2 ) {
+ Field_CompleteFilename( "maps", "bsp", qtrue );
+ }
+}
+
+/*
+==================
+SV_AddOperatorCommands
+==================
+*/
+void SV_AddOperatorCommands( void ) {
+ static qboolean initialized;
+
+ if ( initialized ) {
+ return;
+ }
+ initialized = qtrue;
+
+ Cmd_AddCommand ("heartbeat", SV_Heartbeat_f);
+ Cmd_AddCommand ("kick", SV_Kick_f);
+#ifndef STANDALONE
+ if(!Cvar_VariableIntegerValue("com_standalone"))
+ {
+ Cmd_AddCommand ("banUser", SV_Ban_f);
+ Cmd_AddCommand ("banClient", SV_BanNum_f);
+ }
+#endif
+ Cmd_AddCommand ("clientkick", SV_KickNum_f);
+ Cmd_AddCommand ("status", SV_Status_f);
+ Cmd_AddCommand ("serverinfo", SV_Serverinfo_f);
+ Cmd_AddCommand ("systeminfo", SV_Systeminfo_f);
+ Cmd_AddCommand ("dumpuser", SV_DumpUser_f);
+ Cmd_AddCommand ("map_restart", SV_MapRestart_f);
+ Cmd_AddCommand ("sectorlist", SV_SectorList_f);
+ Cmd_AddCommand ("map", SV_Map_f);
+ Cmd_SetCommandCompletionFunc( "map", SV_CompleteMapName );
+#ifndef PRE_RELEASE_DEMO
+ Cmd_AddCommand ("devmap", SV_Map_f);
+ Cmd_SetCommandCompletionFunc( "devmap", SV_CompleteMapName );
+ Cmd_AddCommand ("spmap", SV_Map_f);
+ Cmd_SetCommandCompletionFunc( "spmap", SV_CompleteMapName );
+ Cmd_AddCommand ("spdevmap", SV_Map_f);
+ Cmd_SetCommandCompletionFunc( "spdevmap", SV_CompleteMapName );
+#endif
+ Cmd_AddCommand ("killserver", SV_KillServer_f);
+ if( com_dedicated->integer ) {
+ Cmd_AddCommand ("say", SV_ConSay_f);
+ }
+
+ Cmd_AddCommand("rehashbans", SV_RehashBans_f);
+ Cmd_AddCommand("listbans", SV_ListBans_f);
+ Cmd_AddCommand("banaddr", SV_BanAddr_f);
+ Cmd_AddCommand("exceptaddr", SV_ExceptAddr_f);
+ Cmd_AddCommand("bandel", SV_BanDel_f);
+ Cmd_AddCommand("exceptdel", SV_ExceptDel_f);
+ Cmd_AddCommand("flushbans", SV_FlushBans_f);
+}
+
+/*
+==================
+SV_RemoveOperatorCommands
+==================
+*/
+void SV_RemoveOperatorCommands( void ) {
+#if 0
+ // removing these won't let the server start again
+ Cmd_RemoveCommand ("heartbeat");
+ Cmd_RemoveCommand ("kick");
+ Cmd_RemoveCommand ("banUser");
+ Cmd_RemoveCommand ("banClient");
+ Cmd_RemoveCommand ("status");
+ Cmd_RemoveCommand ("serverinfo");
+ Cmd_RemoveCommand ("systeminfo");
+ Cmd_RemoveCommand ("dumpuser");
+ Cmd_RemoveCommand ("map_restart");
+ Cmd_RemoveCommand ("sectorlist");
+ Cmd_RemoveCommand ("say");
+#endif
+}
+
diff --git a/code/server/sv_client.c b/code/server/sv_client.c
new file mode 100644
index 0000000..1195c88
--- /dev/null
+++ b/code/server/sv_client.c
@@ -0,0 +1,1914 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+// sv_client.c -- server code for dealing with clients
+
+#include "server.h"
+
+static void SV_CloseDownload( client_t *cl );
+
+/*
+=================
+SV_GetChallenge
+
+A "getchallenge" OOB command has been received
+Returns a challenge number that can be used
+in a subsequent connectResponse command.
+We do this to prevent denial of service attacks that
+flood the server with invalid connection IPs. With a
+challenge, they must give a valid IP address.
+
+If we are authorizing, a challenge request will cause a packet
+to be sent to the authorize server.
+
+When an authorizeip is returned, a challenge response will be
+sent to that ip.
+
+ioquake3: we added a possibility for clients to add a challenge
+to their packets, to make it more difficult for malicious servers
+to hi-jack client connections.
+Also, the auth stuff is completely disabled for com_standalone games
+as well as IPv6 connections, since there is no way to use the
+v4-only auth server for these new types of connections.
+=================
+*/
+void SV_GetChallenge(netadr_t from)
+{
+ int i;
+ int oldest;
+ int oldestTime;
+ const char *clientChallenge = Cmd_Argv(1);
+ challenge_t *challenge;
+
+ // ignore if we are in single player
+ if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) {
+ return;
+ }
+
+ oldest = 0;
+ oldestTime = 0x7fffffff;
+
+ // see if we already have a challenge for this ip
+ challenge = &svs.challenges[0];
+ for (i = 0 ; i < MAX_CHALLENGES ; i++, challenge++) {
+ if (!challenge->connected && NET_CompareAdr( from, challenge->adr ) ) {
+ break;
+ }
+ if ( challenge->time < oldestTime ) {
+ oldestTime = challenge->time;
+ oldest = i;
+ }
+ }
+
+ if (i == MAX_CHALLENGES)
+ {
+ // this is the first time this client has asked for a challenge
+ challenge = &svs.challenges[oldest];
+ challenge->clientChallenge = 0;
+ challenge->adr = from;
+ challenge->firstTime = svs.time;
+ challenge->time = svs.time;
+ challenge->connected = qfalse;
+ }
+
+ // always generate a new challenge number, so the client cannot circumvent sv_maxping
+ challenge->challenge = ( (rand() << 16) ^ rand() ) ^ svs.time;
+ challenge->wasrefused = qfalse;
+
+
+#ifndef STANDALONE
+ // Drop the authorize stuff if this client is coming in via v6 as the auth server does not support ipv6.
+ // Drop also for addresses coming in on local LAN and for stand-alone games independent from id's assets.
+ if(challenge->adr.type == NA_IP && !Cvar_VariableIntegerValue("com_standalone") && !Sys_IsLANAddress(from))
+ {
+ // look up the authorize server's IP
+ if (svs.authorizeAddress.type == NA_BAD)
+ {
+ Com_Printf( "Resolving %s\n", AUTHORIZE_SERVER_NAME );
+
+ if (NET_StringToAdr(AUTHORIZE_SERVER_NAME, &svs.authorizeAddress, NA_IP))
+ {
+ svs.authorizeAddress.port = BigShort( PORT_AUTHORIZE );
+ Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME,
+ svs.authorizeAddress.ip[0], svs.authorizeAddress.ip[1],
+ svs.authorizeAddress.ip[2], svs.authorizeAddress.ip[3],
+ BigShort( svs.authorizeAddress.port ) );
+ }
+ }
+
+ // we couldn't contact the auth server, let them in.
+ if(svs.authorizeAddress.type == NA_BAD)
+ Com_Printf("Couldn't resolve auth server address\n");
+
+ // if they have been challenging for a long time and we
+ // haven't heard anything from the authorize server, go ahead and
+ // let them in, assuming the id server is down
+ else if(svs.time - challenge->firstTime > AUTHORIZE_TIMEOUT)
+ Com_DPrintf( "authorize server timed out\n" );
+ else
+ {
+ // otherwise send their ip to the authorize server
+ cvar_t *fs;
+ char game[1024];
+
+ // If the client provided us with a client challenge, store it...
+ if(*clientChallenge)
+ challenge->clientChallenge = atoi(clientChallenge);
+
+ Com_DPrintf( "sending getIpAuthorize for %s\n", NET_AdrToString( from ));
+
+ strcpy(game, BASEGAME);
+ fs = Cvar_Get ("fs_game", "", CVAR_INIT|CVAR_SYSTEMINFO );
+ if (fs && fs->string[0] != 0) {
+ strcpy(game, fs->string);
+ }
+
+ // the 0 is for backwards compatibility with obsolete sv_allowanonymous flags
+ // getIpAuthorize <challenge> <IP> <game> 0 <auth-flag>
+ NET_OutOfBandPrint( NS_SERVER, svs.authorizeAddress,
+ "getIpAuthorize %i %i.%i.%i.%i %s 0 %s", challenge->challenge,
+ from.ip[0], from.ip[1], from.ip[2], from.ip[3], game, sv_strictAuth->string );
+
+ return;
+ }
+ }
+#endif
+
+ challenge->pingTime = svs.time;
+ NET_OutOfBandPrint( NS_SERVER, challenge->adr, "challengeResponse %i %s", challenge->challenge, clientChallenge);
+}
+
+#ifndef STANDALONE
+/*
+====================
+SV_AuthorizeIpPacket
+
+A packet has been returned from the authorize server.
+If we have a challenge adr for that ip, send the
+challengeResponse to it
+====================
+*/
+void SV_AuthorizeIpPacket( netadr_t from ) {
+ int challenge;
+ int i;
+ char *s;
+ char *r;
+ challenge_t *challengeptr;
+
+ if ( !NET_CompareBaseAdr( from, svs.authorizeAddress ) ) {
+ Com_Printf( "SV_AuthorizeIpPacket: not from authorize server\n" );
+ return;
+ }
+
+ challenge = atoi( Cmd_Argv( 1 ) );
+
+ for (i = 0 ; i < MAX_CHALLENGES ; i++) {
+ if ( svs.challenges[i].challenge == challenge ) {
+ break;
+ }
+ }
+ if ( i == MAX_CHALLENGES ) {
+ Com_Printf( "SV_AuthorizeIpPacket: challenge not found\n" );
+ return;
+ }
+
+ challengeptr = &svs.challenges[i];
+
+ // send a packet back to the original client
+ challengeptr->pingTime = svs.time;
+ s = Cmd_Argv( 2 );
+ r = Cmd_Argv( 3 ); // reason
+
+ if ( !Q_stricmp( s, "demo" ) ) {
+ // they are a demo client trying to connect to a real server
+ NET_OutOfBandPrint( NS_SERVER, challengeptr->adr, "print\nServer is not a demo server\n" );
+ // clear the challenge record so it won't timeout and let them through
+ Com_Memset( challengeptr, 0, sizeof( *challengeptr ) );
+ return;
+ }
+ if ( !Q_stricmp( s, "accept" ) ) {
+ NET_OutOfBandPrint(NS_SERVER, challengeptr->adr,
+ "challengeResponse %d %d", challengeptr->challenge, challengeptr->clientChallenge);
+ return;
+ }
+ if ( !Q_stricmp( s, "unknown" ) ) {
+ if (!r) {
+ NET_OutOfBandPrint( NS_SERVER, challengeptr->adr, "print\nAwaiting CD key authorization\n" );
+ } else {
+ NET_OutOfBandPrint( NS_SERVER, challengeptr->adr, "print\n%s\n", r);
+ }
+ // clear the challenge record so it won't timeout and let them through
+ Com_Memset( challengeptr, 0, sizeof( *challengeptr ) );
+ return;
+ }
+
+ // authorization failed
+ if (!r) {
+ NET_OutOfBandPrint( NS_SERVER, challengeptr->adr, "print\nSomeone is using this CD Key\n" );
+ } else {
+ NET_OutOfBandPrint( NS_SERVER, challengeptr->adr, "print\n%s\n", r );
+ }
+
+ // clear the challenge record so it won't timeout and let them through
+ Com_Memset( challengeptr, 0, sizeof(*challengeptr) );
+}
+#endif
+
+/*
+==================
+SV_IsBanned
+
+Check whether a certain address is banned
+==================
+*/
+
+static qboolean SV_IsBanned(netadr_t *from, qboolean isexception)
+{
+ int index;
+ serverBan_t *curban;
+
+ if(!isexception)
+ {
+ // If this is a query for a ban, first check whether the client is excepted
+ if(SV_IsBanned(from, qtrue))
+ return qfalse;
+ }
+
+ for(index = 0; index < serverBansCount; index++)
+ {
+ curban = &serverBans[index];
+
+ if(curban->isexception == isexception)
+ {
+ if(NET_CompareBaseAdrMask(curban->ip, *from, curban->subnet))
+ return qtrue;
+ }
+ }
+
+ return qfalse;
+}
+
+/*
+==================
+SV_DirectConnect
+
+A "connect" OOB command has been received
+==================
+*/
+
+void SV_DirectConnect( netadr_t from ) {
+ char userinfo[MAX_INFO_STRING];
+ int i;
+ client_t *cl, *newcl;
+ client_t temp;
+ sharedEntity_t *ent;
+ int clientNum;
+ int version;
+ int qport;
+ int challenge;
+ char *password;
+ int startIndex;
+ intptr_t denied;
+ int count;
+ char *ip;
+
+ Com_DPrintf ("SVC_DirectConnect ()\n");
+
+ // Check whether this client is banned.
+ if(SV_IsBanned(&from, qfalse))
+ {
+ NET_OutOfBandPrint(NS_SERVER, from, "print\nYou are banned from this server.\n");
+ return;
+ }
+
+ Q_strncpyz( userinfo, Cmd_Argv(1), sizeof(userinfo) );
+
+ version = atoi( Info_ValueForKey( userinfo, "protocol" ) );
+ if ( version != PROTOCOL_VERSION ) {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer uses protocol version %i.\n", PROTOCOL_VERSION );
+ Com_DPrintf (" rejected connect from version %i\n", version);
+ return;
+ }
+
+ challenge = atoi( Info_ValueForKey( userinfo, "challenge" ) );
+ qport = atoi( Info_ValueForKey( userinfo, "qport" ) );
+
+ // quick reject
+ 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 )
+ && ( cl->netchan.qport == qport
+ || from.port == cl->netchan.remoteAddress.port ) ) {
+ if (( svs.time - cl->lastConnectTime)
+ < (sv_reconnectlimit->integer * 1000)) {
+ Com_DPrintf ("%s:reconnect rejected : too soon\n", NET_AdrToString (from));
+ return;
+ }
+ break;
+ }
+ }
+
+ // don't let "ip" overflow userinfo string
+ if ( NET_IsLocalAddress (from) )
+ ip = "localhost";
+ else
+ ip = (char *)NET_AdrToString( from );
+ if( ( strlen( ip ) + strlen( userinfo ) + 4 ) >= MAX_INFO_STRING ) {
+ NET_OutOfBandPrint( NS_SERVER, from,
+ "print\nUserinfo string length exceeded. "
+ "Try removing setu cvars from your config.\n" );
+ return;
+ }
+ Info_SetValueForKey( userinfo, "ip", ip );
+
+ // see if the challenge is valid (LAN clients don't need to challenge)
+ if (!NET_IsLocalAddress(from))
+ {
+ int ping;
+ challenge_t *challengeptr;
+
+ for (i=0; i<MAX_CHALLENGES; i++)
+ {
+ if (NET_CompareAdr(from, svs.challenges[i].adr))
+ {
+ if(challenge == svs.challenges[i].challenge)
+ break;
+ }
+ }
+
+ if (i == MAX_CHALLENGES)
+ {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nNo or bad challenge for your address.\n" );
+ return;
+ }
+
+ challengeptr = &svs.challenges[i];
+
+ if(challengeptr->wasrefused)
+ {
+ // Return silently, so that error messages written by the server keep being displayed.
+ return;
+ }
+
+ ping = svs.time - challengeptr->pingTime;
+
+ // never reject a LAN client based on ping
+ if ( !Sys_IsLANAddress( from ) ) {
+ if ( sv_minPing->value && ping < sv_minPing->value ) {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer is for high pings only\n" );
+ Com_DPrintf ("Client %i rejected on a too low ping\n", i);
+ challengeptr->wasrefused = qtrue;
+ return;
+ }
+ if ( sv_maxPing->value && ping > sv_maxPing->value ) {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer is for low pings only\n" );
+ Com_DPrintf ("Client %i rejected on a too high ping\n", i);
+ challengeptr->wasrefused = qtrue;
+ return;
+ }
+ }
+
+ Com_Printf("Client %i connecting with %i challenge ping\n", i, ping);
+ challengeptr->connected = qtrue;
+ }
+
+ newcl = &temp;
+ Com_Memset (newcl, 0, sizeof(client_t));
+
+ // if there is already a slot for this ip, reuse it
+ 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 )
+ && ( cl->netchan.qport == qport
+ || from.port == cl->netchan.remoteAddress.port ) ) {
+ Com_Printf ("%s:reconnect\n", NET_AdrToString (from));
+ newcl = cl;
+
+ // this doesn't work because it nukes the players userinfo
+
+// // disconnect the client from the game first so any flags the
+// // player might have are dropped
+// VM_Call( gvm, GAME_CLIENT_DISCONNECT, newcl - svs.clients );
+ //
+ goto gotnewcl;
+ }
+ }
+
+ // find a client slot
+ // if "sv_privateClients" is set > 0, then that number
+ // of client slots will be reserved for connections that
+ // have "password" set to the value of "sv_privatePassword"
+ // Info requests will report the maxclients as if the private
+ // slots didn't exist, to prevent people from trying to connect
+ // to a full server.
+ // This is to allow us to reserve a couple slots here on our
+ // servers so we can play without having to kick people.
+
+ // check for privateClient password
+ password = Info_ValueForKey( userinfo, "password" );
+ if ( !strcmp( password, sv_privatePassword->string ) ) {
+ startIndex = 0;
+ } else {
+ // skip past the reserved slots
+ startIndex = sv_privateClients->integer;
+ }
+
+ newcl = NULL;
+ for ( i = startIndex; i < sv_maxclients->integer ; i++ ) {
+ cl = &svs.clients[i];
+ if (cl->state == CS_FREE) {
+ newcl = cl;
+ break;
+ }
+ }
+
+ if ( !newcl ) {
+ if ( NET_IsLocalAddress( from ) ) {
+ count = 0;
+ for ( i = startIndex; i < sv_maxclients->integer ; i++ ) {
+ cl = &svs.clients[i];
+ if (cl->netchan.remoteAddress.type == NA_BOT) {
+ count++;
+ }
+ }
+ // if they're all bots
+ if (count >= sv_maxclients->integer - startIndex) {
+ SV_DropClient(&svs.clients[sv_maxclients->integer - 1], "only bots on server");
+ newcl = &svs.clients[sv_maxclients->integer - 1];
+ }
+ else {
+ Com_Error( ERR_FATAL, "server is full on local connect\n" );
+ return;
+ }
+ }
+ else {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer is full.\n" );
+ Com_DPrintf ("Rejected a connection.\n");
+ return;
+ }
+ }
+
+ // we got a newcl, so reset the reliableSequence and reliableAcknowledge
+ cl->reliableAcknowledge = 0;
+ cl->reliableSequence = 0;
+
+gotnewcl:
+ // build a new connection
+ // accept the new client
+ // this is the only place a client_t is ever initialized
+ *newcl = temp;
+ clientNum = newcl - svs.clients;
+ ent = SV_GentityNum( clientNum );
+ newcl->gentity = ent;
+
+ // save the challenge
+ newcl->challenge = challenge;
+
+ // save the address
+ Netchan_Setup (NS_SERVER, &newcl->netchan , from, qport);
+ // init the netchan queue
+ newcl->netchan_end_queue = &newcl->netchan_start_queue;
+
+ // save the userinfo
+ Q_strncpyz( newcl->userinfo, userinfo, sizeof(newcl->userinfo) );
+
+ // get the game a chance to reject this connection or modify the userinfo
+ denied = VM_Call( gvm, GAME_CLIENT_CONNECT, clientNum, qtrue, qfalse ); // firstTime = qtrue
+ if ( denied ) {
+ // we can't just use VM_ArgPtr, because that is only valid inside a VM_Call
+ char *str = VM_ExplicitArgPtr( gvm, denied );
+
+ NET_OutOfBandPrint( NS_SERVER, from, "print\n%s\n", str );
+ Com_DPrintf ("Game rejected a connection: %s.\n", str);
+ return;
+ }
+
+ SV_UserinfoChanged( newcl );
+
+ // send the connect packet to the client
+ NET_OutOfBandPrint( NS_SERVER, from, "connectResponse" );
+
+ Com_DPrintf( "Going from CS_FREE to CS_CONNECTED for %s\n", newcl->name );
+
+ newcl->state = CS_CONNECTED;
+ newcl->nextSnapshotTime = svs.time;
+ newcl->lastPacketTime = svs.time;
+ newcl->lastConnectTime = svs.time;
+
+ // when we receive the first packet from the client, we will
+ // notice that it is from a different serverid and that the
+ // gamestate message was not just sent, forcing a retransmit
+ newcl->gamestateMessageNum = -1;
+
+ // if this was the first client on the server, or the last client
+ // the server can hold, send a heartbeat to the master.
+ count = 0;
+ for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) {
+ if ( svs.clients[i].state >= CS_CONNECTED ) {
+ count++;
+ }
+ }
+ if ( count == 1 || count == sv_maxclients->integer ) {
+ SV_Heartbeat_f();
+ }
+}
+
+
+/*
+=====================
+SV_DropClient
+
+Called when the player is totally leaving the server, either willingly
+or unwillingly. This is NOT called if the entire server is quiting
+or crashing -- SV_FinalMessage() will handle that
+=====================
+*/
+void SV_DropClient( client_t *drop, const char *reason ) {
+ int i;
+ challenge_t *challenge;
+ const qboolean isBot = drop->netchan.remoteAddress.type == NA_BOT;
+
+ if ( drop->state == CS_ZOMBIE ) {
+ return; // already dropped
+ }
+
+ if ( !isBot ) {
+ // see if we already have a challenge for this ip
+ challenge = &svs.challenges[0];
+
+ for (i = 0 ; i < MAX_CHALLENGES ; i++, challenge++)
+ {
+ if(NET_CompareAdr(drop->netchan.remoteAddress, challenge->adr))
+ {
+ Com_Memset(challenge, 0, sizeof(*challenge));
+ break;
+ }
+ }
+ }
+
+ // Kill any download
+ SV_CloseDownload( drop );
+
+ // tell everyone why they got dropped
+ SV_SendServerCommand( NULL, "print \"%s" S_COLOR_WHITE " %s\n\"", drop->name, reason );
+
+ if (drop->download) {
+ FS_FCloseFile( drop->download );
+ drop->download = 0;
+ }
+
+ // call the prog function for removing a client
+ // this will remove the body, among other things
+ VM_Call( gvm, GAME_CLIENT_DISCONNECT, drop - svs.clients );
+
+ // add the disconnect command
+ SV_SendServerCommand( drop, "disconnect \"%s\"", reason);
+
+ if ( isBot ) {
+ SV_BotFreeClient( drop - svs.clients );
+ }
+
+ // nuke user info
+ SV_SetUserinfo( drop - svs.clients, "" );
+
+ if ( isBot ) {
+ // bots shouldn't go zombie, as there's no real net connection.
+ drop->state = CS_FREE;
+ } else {
+ Com_DPrintf( "Going to CS_ZOMBIE for %s\n", drop->name );
+ drop->state = CS_ZOMBIE; // become free in a few seconds
+ }
+
+ // if this was the last client on the server, send a heartbeat
+ // to the master so it is known the server is empty
+ // send a heartbeat now so the master will get up to date info
+ // if there is already a slot for this ip, reuse it
+ for (i=0 ; i < sv_maxclients->integer ; i++ ) {
+ if ( svs.clients[i].state >= CS_CONNECTED ) {
+ break;
+ }
+ }
+ if ( i == sv_maxclients->integer ) {
+ SV_Heartbeat_f();
+ }
+}
+
+/*
+================
+SV_SendClientGameState
+
+Sends the first message from the server to a connected client.
+This will be sent on the initial connection and upon each new map load.
+
+It will be resent if the client acknowledges a later message but has
+the wrong gamestate.
+================
+*/
+static void SV_SendClientGameState( client_t *client ) {
+ int start;
+ entityState_t *base, nullstate;
+ msg_t msg;
+ byte msgBuffer[MAX_MSGLEN];
+
+ Com_DPrintf ("SV_SendClientGameState() for %s\n", client->name);
+ Com_DPrintf( "Going from CS_CONNECTED to CS_PRIMED for %s\n", client->name );
+ client->state = CS_PRIMED;
+ client->pureAuthentic = 0;
+ client->gotCP = qfalse;
+
+ // when we receive the first packet from the client, we will
+ // notice that it is from a different serverid and that the
+ // gamestate message was not just sent, forcing a retransmit
+ client->gamestateMessageNum = client->netchan.outgoingSequence;
+
+ MSG_Init( &msg, msgBuffer, sizeof( msgBuffer ) );
+
+ // NOTE, MRE: all server->client messages now acknowledge
+ // let the client know which reliable clientCommands we have received
+ MSG_WriteLong( &msg, client->lastClientCommand );
+
+ // send any server commands waiting to be sent first.
+ // we have to do this cause we send the client->reliableSequence
+ // with a gamestate and it sets the clc.serverCommandSequence at
+ // the client side
+ SV_UpdateServerCommandsToClient( client, &msg );
+
+ // send the gamestate
+ MSG_WriteByte( &msg, svc_gamestate );
+ MSG_WriteLong( &msg, client->reliableSequence );
+
+ // write the configstrings
+ for ( start = 0 ; start < MAX_CONFIGSTRINGS ; start++ ) {
+ if (sv.configstrings[start][0]) {
+ MSG_WriteByte( &msg, svc_configstring );
+ MSG_WriteShort( &msg, start );
+ MSG_WriteBigString( &msg, sv.configstrings[start] );
+ }
+ }
+
+ // write the baselines
+ Com_Memset( &nullstate, 0, sizeof( nullstate ) );
+ for ( start = 0 ; start < MAX_GENTITIES; start++ ) {
+ base = &sv.svEntities[start].baseline;
+ if ( !base->number ) {
+ continue;
+ }
+ MSG_WriteByte( &msg, svc_baseline );
+ MSG_WriteDeltaEntity( &msg, &nullstate, base, qtrue );
+ }
+
+ MSG_WriteByte( &msg, svc_EOF );
+
+ MSG_WriteLong( &msg, client - svs.clients);
+
+ // write the checksum feed
+ MSG_WriteLong( &msg, sv.checksumFeed);
+
+ // deliver this to the client
+ SV_SendMessageToClient( &msg, client );
+}
+
+
+/*
+==================
+SV_ClientEnterWorld
+==================
+*/
+void SV_ClientEnterWorld( client_t *client, usercmd_t *cmd ) {
+ int clientNum;
+ sharedEntity_t *ent;
+
+ Com_DPrintf( "Going from CS_PRIMED to CS_ACTIVE for %s\n", client->name );
+ client->state = CS_ACTIVE;
+
+ // resend all configstrings using the cs commands since these are
+ // no longer sent when the client is CS_PRIMED
+ SV_UpdateConfigstrings( client );
+
+ // set up the entity for the client
+ clientNum = client - svs.clients;
+ ent = SV_GentityNum( clientNum );
+ ent->s.number = clientNum;
+ client->gentity = ent;
+
+ client->deltaMessage = -1;
+ client->nextSnapshotTime = svs.time; // generate a snapshot immediately
+ client->lastUsercmd = *cmd;
+
+ // call the game begin function
+ VM_Call( gvm, GAME_CLIENT_BEGIN, client - svs.clients );
+}
+
+/*
+============================================================
+
+CLIENT COMMAND EXECUTION
+
+============================================================
+*/
+
+/*
+==================
+SV_CloseDownload
+
+clear/free any download vars
+==================
+*/
+static void SV_CloseDownload( client_t *cl ) {
+ int i;
+
+ // EOF
+ if (cl->download) {
+ FS_FCloseFile( cl->download );
+ }
+ cl->download = 0;
+ *cl->downloadName = 0;
+
+ // Free the temporary buffer space
+ for (i = 0; i < MAX_DOWNLOAD_WINDOW; i++) {
+ if (cl->downloadBlocks[i]) {
+ Z_Free( cl->downloadBlocks[i] );
+ cl->downloadBlocks[i] = NULL;
+ }
+ }
+
+}
+
+/*
+==================
+SV_StopDownload_f
+
+Abort a download if in progress
+==================
+*/
+static void SV_StopDownload_f( client_t *cl ) {
+ if (*cl->downloadName)
+ Com_DPrintf( "clientDownload: %d : file \"%s\" aborted\n", (int) (cl - svs.clients), cl->downloadName );
+
+ SV_CloseDownload( cl );
+}
+
+/*
+==================
+SV_DoneDownload_f
+
+Downloads are finished
+==================
+*/
+static void SV_DoneDownload_f( client_t *cl ) {
+ Com_DPrintf( "clientDownload: %s Done\n", cl->name);
+ // resend the game state to update any clients that entered during the download
+ SV_SendClientGameState(cl);
+}
+
+/*
+==================
+SV_NextDownload_f
+
+The argument will be the last acknowledged block from the client, it should be
+the same as cl->downloadClientBlock
+==================
+*/
+static void SV_NextDownload_f( client_t *cl )
+{
+ int block = atoi( Cmd_Argv(1) );
+
+ if (block == cl->downloadClientBlock) {
+ Com_DPrintf( "clientDownload: %d : client acknowledge of block %d\n", (int) (cl - svs.clients), block );
+
+ // Find out if we are done. A zero-length block indicates EOF
+ if (cl->downloadBlockSize[cl->downloadClientBlock % MAX_DOWNLOAD_WINDOW] == 0) {
+ Com_Printf( "clientDownload: %d : file \"%s\" completed\n", (int) (cl - svs.clients), cl->downloadName );
+ SV_CloseDownload( cl );
+ return;
+ }
+
+ cl->downloadSendTime = svs.time;
+ cl->downloadClientBlock++;
+ return;
+ }
+ // We aren't getting an acknowledge for the correct block, drop the client
+ // FIXME: this is bad... the client will never parse the disconnect message
+ // because the cgame isn't loaded yet
+ SV_DropClient( cl, "broken download" );
+}
+
+/*
+==================
+SV_BeginDownload_f
+==================
+*/
+static void SV_BeginDownload_f( client_t *cl ) {
+
+ // Kill any existing download
+ SV_CloseDownload( cl );
+
+ // cl->downloadName is non-zero now, SV_WriteDownloadToClient will see this and open
+ // the file itself
+ Q_strncpyz( cl->downloadName, Cmd_Argv(1), sizeof(cl->downloadName) );
+}
+
+/*
+==================
+SV_WriteDownloadToClient
+
+Check to see if the client wants a file, open it if needed and start pumping the client
+Fill up msg with data
+==================
+*/
+void SV_WriteDownloadToClient( client_t *cl , msg_t *msg )
+{
+ int curindex;
+ int rate;
+ int blockspersnap;
+ int idPack = 0, missionPack = 0, unreferenced = 1;
+ char errorMessage[1024];
+ char pakbuf[MAX_QPATH], *pakptr;
+ int numRefPaks;
+
+ if (!*cl->downloadName)
+ return; // Nothing being downloaded
+
+ if (!cl->download) {
+ // Chop off filename extension.
+ Com_sprintf(pakbuf, sizeof(pakbuf), "%s", cl->downloadName);
+ pakptr = Q_strrchr(pakbuf, '.');
+
+ if(pakptr)
+ {
+ *pakptr = '\0';
+
+ // Check for pk3 filename extension
+ if(!Q_stricmp(pakptr + 1, "pk3"))
+ {
+ const char *referencedPaks = FS_ReferencedPakNames();
+
+ // Check whether the file appears in the list of referenced
+ // paks to prevent downloading of arbitrary files.
+ Cmd_TokenizeStringIgnoreQuotes(referencedPaks);
+ numRefPaks = Cmd_Argc();
+
+ for(curindex = 0; curindex < numRefPaks; curindex++)
+ {
+ if(!FS_FilenameCompare(Cmd_Argv(curindex), pakbuf))
+ {
+ unreferenced = 0;
+
+ // now that we know the file is referenced,
+ // check whether it's legal to download it.
+ missionPack = FS_idPak(pakbuf, "missionpack");
+ idPack = missionPack || FS_idPak(pakbuf, BASEGAME);
+
+ break;
+ }
+ }
+ }
+ }
+
+ cl->download = 0;
+
+ // We open the file here
+ if ( !(sv_allowDownload->integer & DLF_ENABLE) ||
+ (sv_allowDownload->integer & DLF_NO_UDP) ||
+ idPack || unreferenced ||
+ ( cl->downloadSize = FS_SV_FOpenFileRead( cl->downloadName, &cl->download ) ) < 0 ) {
+ // cannot auto-download file
+ if(unreferenced)
+ {
+ Com_Printf("clientDownload: %d : \"%s\" is not referenced and cannot be downloaded.\n", (int) (cl - svs.clients), cl->downloadName);
+ Com_sprintf(errorMessage, sizeof(errorMessage), "File \"%s\" is not referenced and cannot be downloaded.", cl->downloadName);
+ }
+ else if (idPack) {
+ Com_Printf("clientDownload: %d : \"%s\" cannot download id pk3 files\n", (int) (cl - svs.clients), cl->downloadName);
+ if (missionPack) {
+ Com_sprintf(errorMessage, sizeof(errorMessage), "Cannot autodownload Team Arena file \"%s\"\n"
+ "The Team Arena mission pack can be found in your local game store.", cl->downloadName);
+ }
+ else {
+ Com_sprintf(errorMessage, sizeof(errorMessage), "Cannot autodownload id pk3 file \"%s\"", cl->downloadName);
+ }
+ }
+ else if ( !(sv_allowDownload->integer & DLF_ENABLE) ||
+ (sv_allowDownload->integer & DLF_NO_UDP) ) {
+
+ Com_Printf("clientDownload: %d : \"%s\" download disabled", (int) (cl - svs.clients), cl->downloadName);
+ if (sv_pure->integer) {
+ Com_sprintf(errorMessage, sizeof(errorMessage), "Could not download \"%s\" because autodownloading is disabled on the server.\n\n"
+ "You will need to get this file elsewhere before you "
+ "can connect to this pure server.\n", cl->downloadName);
+ } else {
+ Com_sprintf(errorMessage, sizeof(errorMessage), "Could not download \"%s\" because autodownloading is disabled on the server.\n\n"
+ "The server you are connecting to is not a pure server, "
+ "set autodownload to No in your settings and you might be "
+ "able to join the game anyway.\n", cl->downloadName);
+ }
+ } else {
+ // NOTE TTimo this is NOT supposed to happen unless bug in our filesystem scheme?
+ // if the pk3 is referenced, it must have been found somewhere in the filesystem
+ Com_Printf("clientDownload: %d : \"%s\" file not found on server\n", (int) (cl - svs.clients), cl->downloadName);
+ Com_sprintf(errorMessage, sizeof(errorMessage), "File \"%s\" not found on server for autodownloading.\n", cl->downloadName);
+ }
+ MSG_WriteByte( msg, svc_download );
+ MSG_WriteShort( msg, 0 ); // client is expecting block zero
+ MSG_WriteLong( msg, -1 ); // illegal file size
+ MSG_WriteString( msg, errorMessage );
+
+ *cl->downloadName = 0;
+
+ if(cl->download)
+ FS_FCloseFile(cl->download);
+
+ return;
+ }
+
+ Com_Printf( "clientDownload: %d : beginning \"%s\"\n", (int) (cl - svs.clients), cl->downloadName );
+
+ // Init
+ cl->downloadCurrentBlock = cl->downloadClientBlock = cl->downloadXmitBlock = 0;
+ cl->downloadCount = 0;
+ cl->downloadEOF = qfalse;
+ }
+
+ // Perform any reads that we need to
+ while (cl->downloadCurrentBlock - cl->downloadClientBlock < MAX_DOWNLOAD_WINDOW &&
+ cl->downloadSize != cl->downloadCount) {
+
+ curindex = (cl->downloadCurrentBlock % MAX_DOWNLOAD_WINDOW);
+
+ if (!cl->downloadBlocks[curindex])
+ cl->downloadBlocks[curindex] = Z_Malloc( MAX_DOWNLOAD_BLKSIZE );
+
+ cl->downloadBlockSize[curindex] = FS_Read( cl->downloadBlocks[curindex], MAX_DOWNLOAD_BLKSIZE, cl->download );
+
+ if (cl->downloadBlockSize[curindex] < 0) {
+ // EOF right now
+ cl->downloadCount = cl->downloadSize;
+ break;
+ }
+
+ cl->downloadCount += cl->downloadBlockSize[curindex];
+
+ // Load in next block
+ cl->downloadCurrentBlock++;
+ }
+
+ // Check to see if we have eof condition and add the EOF block
+ if (cl->downloadCount == cl->downloadSize &&
+ !cl->downloadEOF &&
+ cl->downloadCurrentBlock - cl->downloadClientBlock < MAX_DOWNLOAD_WINDOW) {
+
+ cl->downloadBlockSize[cl->downloadCurrentBlock % MAX_DOWNLOAD_WINDOW] = 0;
+ cl->downloadCurrentBlock++;
+
+ cl->downloadEOF = qtrue; // We have added the EOF block
+ }
+
+ // Loop up to window size times based on how many blocks we can fit in the
+ // client snapMsec and rate
+
+ // based on the rate, how many bytes can we fit in the snapMsec time of the client
+ // normal rate / snapshotMsec calculation
+ rate = cl->rate;
+ if ( sv_maxRate->integer ) {
+ if ( sv_maxRate->integer < 1000 ) {
+ Cvar_Set( "sv_MaxRate", "1000" );
+ }
+ if ( sv_maxRate->integer < rate ) {
+ rate = sv_maxRate->integer;
+ }
+ }
+ if ( sv_minRate->integer ) {
+ if ( sv_minRate->integer < 1000 )
+ Cvar_Set( "sv_minRate", "1000" );
+ if ( sv_minRate->integer > rate )
+ rate = sv_minRate->integer;
+ }
+
+ if (!rate) {
+ blockspersnap = 1;
+ } else {
+ blockspersnap = ( (rate * cl->snapshotMsec) / 1000 + MAX_DOWNLOAD_BLKSIZE ) /
+ MAX_DOWNLOAD_BLKSIZE;
+ }
+
+ if (blockspersnap < 0)
+ blockspersnap = 1;
+
+ while (blockspersnap--) {
+
+ // Write out the next section of the file, if we have already reached our window,
+ // automatically start retransmitting
+
+ if (cl->downloadClientBlock == cl->downloadCurrentBlock)
+ return; // Nothing to transmit
+
+ if (cl->downloadXmitBlock == cl->downloadCurrentBlock) {
+ // We have transmitted the complete window, should we start resending?
+
+ //FIXME: This uses a hardcoded one second timeout for lost blocks
+ //the timeout should be based on client rate somehow
+ if (svs.time - cl->downloadSendTime > 1000)
+ cl->downloadXmitBlock = cl->downloadClientBlock;
+ else
+ return;
+ }
+
+ // Send current block
+ curindex = (cl->downloadXmitBlock % MAX_DOWNLOAD_WINDOW);
+
+ MSG_WriteByte( msg, svc_download );
+ MSG_WriteShort( msg, cl->downloadXmitBlock );
+
+ // block zero is special, contains file size
+ if ( cl->downloadXmitBlock == 0 )
+ MSG_WriteLong( msg, cl->downloadSize );
+
+ MSG_WriteShort( msg, cl->downloadBlockSize[curindex] );
+
+ // Write the block
+ if ( cl->downloadBlockSize[curindex] ) {
+ MSG_WriteData( msg, cl->downloadBlocks[curindex], cl->downloadBlockSize[curindex] );
+ }
+
+ Com_DPrintf( "clientDownload: %d : writing block %d\n", (int) (cl - svs.clients), cl->downloadXmitBlock );
+
+ // Move on to the next block
+ // It will get sent with next snap shot. The rate will keep us in line.
+ cl->downloadXmitBlock++;
+
+ cl->downloadSendTime = svs.time;
+ }
+}
+
+#ifdef USE_VOIP
+/*
+==================
+SV_WriteVoipToClient
+
+Check to see if there is any VoIP queued for a client, and send if there is.
+==================
+*/
+void SV_WriteVoipToClient( client_t *cl, msg_t *msg )
+{
+ voipServerPacket_t *packet = &cl->voipPacket[0];
+ int totalbytes = 0;
+ int i;
+
+ if (*cl->downloadName) {
+ cl->queuedVoipPackets = 0;
+ return; // no VoIP allowed if download is going, to save bandwidth.
+ }
+
+ // Write as many VoIP packets as we reasonably can...
+ for (i = 0; i < cl->queuedVoipPackets; i++, packet++) {
+ totalbytes += packet->len;
+ if (totalbytes > MAX_DOWNLOAD_BLKSIZE)
+ break;
+
+ // You have to start with a svc_EOF, so legacy clients drop the
+ // rest of this packet. Otherwise, those without VoIP support will
+ // see the svc_voip command, then panic and disconnect.
+ // Generally we don't send VoIP packets to legacy clients, but this
+ // serves as both a safety measure and a means to keep demo files
+ // compatible.
+ MSG_WriteByte( msg, svc_EOF );
+ MSG_WriteByte( msg, svc_extension );
+ MSG_WriteByte( msg, svc_voip );
+ MSG_WriteShort( msg, packet->sender );
+ MSG_WriteByte( msg, (byte) packet->generation );
+ MSG_WriteLong( msg, packet->sequence );
+ MSG_WriteByte( msg, packet->frames );
+ MSG_WriteShort( msg, packet->len );
+ MSG_WriteData( msg, packet->data, packet->len );
+ }
+
+ // !!! FIXME: I hate this queue system.
+ cl->queuedVoipPackets -= i;
+ if (cl->queuedVoipPackets > 0) {
+ memmove( &cl->voipPacket[0], &cl->voipPacket[i],
+ sizeof (voipServerPacket_t) * i);
+ }
+}
+#endif
+
+
+/*
+=================
+SV_Disconnect_f
+
+The client is going to disconnect, so remove the connection immediately FIXME: move to game?
+=================
+*/
+static void SV_Disconnect_f( client_t *cl ) {
+ SV_DropClient( cl, "disconnected" );
+}
+
+/*
+=================
+SV_VerifyPaks_f
+
+If we are pure, disconnect the client if they do no meet the following conditions:
+
+1. the first two checksums match our view of cgame and ui
+2. there are no any additional checksums that we do not have
+
+This routine would be a bit simpler with a goto but i abstained
+
+=================
+*/
+static void SV_VerifyPaks_f( client_t *cl ) {
+ int nChkSum1, nChkSum2, nClientPaks, nServerPaks, i, j, nCurArg;
+ int nClientChkSum[1024];
+ int nServerChkSum[1024];
+ const char *pPaks, *pArg;
+ qboolean bGood = qtrue;
+
+ // if we are pure, we "expect" the client to load certain things from
+ // certain pk3 files, namely we want the client to have loaded the
+ // ui and cgame that we think should be loaded based on the pure setting
+ //
+ if ( sv_pure->integer != 0 ) {
+
+ bGood = qtrue;
+ nChkSum1 = nChkSum2 = 0;
+ // we run the game, so determine which cgame and ui the client "should" be running
+ bGood = (FS_FileIsInPAK("vm/cgame.qvm", &nChkSum1) == 1);
+ if (bGood)
+ bGood = (FS_FileIsInPAK("vm/ui.qvm", &nChkSum2) == 1);
+
+ nClientPaks = Cmd_Argc();
+
+ // start at arg 2 ( skip serverId cl_paks )
+ nCurArg = 1;
+
+ pArg = Cmd_Argv(nCurArg++);
+ if(!pArg) {
+ bGood = qfalse;
+ }
+ else
+ {
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=475
+ // we may get incoming cp sequences from a previous checksumFeed, which we need to ignore
+ // since serverId is a frame count, it always goes up
+ if (atoi(pArg) < sv.checksumFeedServerId)
+ {
+ Com_DPrintf("ignoring outdated cp command from client %s\n", cl->name);
+ return;
+ }
+ }
+
+ // we basically use this while loop to avoid using 'goto' :)
+ while (bGood) {
+
+ // must be at least 6: "cl_paks cgame ui @ firstref ... numChecksums"
+ // numChecksums is encoded
+ if (nClientPaks < 6) {
+ bGood = qfalse;
+ break;
+ }
+ // verify first to be the cgame checksum
+ pArg = Cmd_Argv(nCurArg++);
+ if (!pArg || *pArg == '@' || atoi(pArg) != nChkSum1 ) {
+ bGood = qfalse;
+ break;
+ }
+ // verify the second to be the ui checksum
+ pArg = Cmd_Argv(nCurArg++);
+ if (!pArg || *pArg == '@' || atoi(pArg) != nChkSum2 ) {
+ bGood = qfalse;
+ break;
+ }
+ // should be sitting at the delimeter now
+ pArg = Cmd_Argv(nCurArg++);
+ if (*pArg != '@') {
+ bGood = qfalse;
+ break;
+ }
+ // store checksums since tokenization is not re-entrant
+ for (i = 0; nCurArg < nClientPaks; i++) {
+ nClientChkSum[i] = atoi(Cmd_Argv(nCurArg++));
+ }
+
+ // store number to compare against (minus one cause the last is the number of checksums)
+ nClientPaks = i - 1;
+
+ // make sure none of the client check sums are the same
+ // so the client can't send 5 the same checksums
+ for (i = 0; i < nClientPaks; i++) {
+ for (j = 0; j < nClientPaks; j++) {
+ if (i == j)
+ continue;
+ if (nClientChkSum[i] == nClientChkSum[j]) {
+ bGood = qfalse;
+ break;
+ }
+ }
+ if (bGood == qfalse)
+ break;
+ }
+ if (bGood == qfalse)
+ break;
+
+ // get the pure checksums of the pk3 files loaded by the server
+ pPaks = FS_LoadedPakPureChecksums();
+ Cmd_TokenizeString( pPaks );
+ nServerPaks = Cmd_Argc();
+ if (nServerPaks > 1024)
+ nServerPaks = 1024;
+
+ for (i = 0; i < nServerPaks; i++) {
+ nServerChkSum[i] = atoi(Cmd_Argv(i));
+ }
+
+ // check if the client has provided any pure checksums of pk3 files not loaded by the server
+ for (i = 0; i < nClientPaks; i++) {
+ for (j = 0; j < nServerPaks; j++) {
+ if (nClientChkSum[i] == nServerChkSum[j]) {
+ break;
+ }
+ }
+ if (j >= nServerPaks) {
+ bGood = qfalse;
+ break;
+ }
+ }
+ if ( bGood == qfalse ) {
+ break;
+ }
+
+ // check if the number of checksums was correct
+ nChkSum1 = sv.checksumFeed;
+ for (i = 0; i < nClientPaks; i++) {
+ nChkSum1 ^= nClientChkSum[i];
+ }
+ nChkSum1 ^= nClientPaks;
+ if (nChkSum1 != nClientChkSum[nClientPaks]) {
+ bGood = qfalse;
+ break;
+ }
+
+ // break out
+ break;
+ }
+
+ cl->gotCP = qtrue;
+
+ if (bGood) {
+ cl->pureAuthentic = 1;
+ }
+ else {
+ cl->pureAuthentic = 0;
+ cl->nextSnapshotTime = -1;
+ cl->state = CS_ACTIVE;
+ SV_SendClientSnapshot( cl );
+ SV_DropClient( cl, "Unpure client detected. Invalid .PK3 files referenced!" );
+ }
+ }
+}
+
+/*
+=================
+SV_ResetPureClient_f
+=================
+*/
+static void SV_ResetPureClient_f( client_t *cl ) {
+ cl->pureAuthentic = 0;
+ cl->gotCP = qfalse;
+}
+
+/*
+=================
+SV_UserinfoChanged
+
+Pull specific info from a newly changed userinfo string
+into a more C friendly form.
+=================
+*/
+void SV_UserinfoChanged( client_t *cl ) {
+ char *val;
+ char *ip;
+ int i;
+ int len;
+
+ // name for C code
+ Q_strncpyz( cl->name, Info_ValueForKey (cl->userinfo, "name"), sizeof(cl->name) );
+
+ // rate command
+
+ // if the client is on the same subnet as the server and we aren't running an
+ // internet public server, assume they don't need a rate choke
+ if ( Sys_IsLANAddress( cl->netchan.remoteAddress ) && com_dedicated->integer != 2 && sv_lanForceRate->integer == 1) {
+ cl->rate = 99999; // lans should not rate limit
+ } else {
+ val = Info_ValueForKey (cl->userinfo, "rate");
+ if (strlen(val)) {
+ i = atoi(val);
+ cl->rate = i;
+ if (cl->rate < 1000) {
+ cl->rate = 1000;
+ } else if (cl->rate > 90000) {
+ cl->rate = 90000;
+ }
+ } else {
+ cl->rate = 3000;
+ }
+ }
+ val = Info_ValueForKey (cl->userinfo, "handicap");
+ if (strlen(val)) {
+ i = atoi(val);
+ if (i<=0 || i>100 || strlen(val) > 4) {
+ Info_SetValueForKey( cl->userinfo, "handicap", "100" );
+ }
+ }
+
+ // snaps command
+ val = Info_ValueForKey (cl->userinfo, "snaps");
+ if (strlen(val)) {
+ i = atoi(val);
+ if ( i < 1 ) {
+ i = 1;
+ } else if ( i > sv_fps->integer ) {
+ i = sv_fps->integer;
+ }
+ cl->snapshotMsec = 1000/i;
+ } else {
+ cl->snapshotMsec = 50;
+ }
+
+#ifdef USE_VOIP
+ // in the future, (val) will be a protocol version string, so only
+ // accept explicitly 1, not generally non-zero.
+ val = Info_ValueForKey (cl->userinfo, "cl_voip");
+ cl->hasVoip = (atoi(val) == 1) ? qtrue : qfalse;
+#endif
+
+ // TTimo
+ // maintain the IP information
+ // the banning code relies on this being consistently present
+ if( NET_IsLocalAddress(cl->netchan.remoteAddress) )
+ ip = "localhost";
+ else
+ ip = (char*)NET_AdrToString( cl->netchan.remoteAddress );
+
+ val = Info_ValueForKey( cl->userinfo, "ip" );
+ if( val[0] )
+ len = strlen( ip ) - strlen( val ) + strlen( cl->userinfo );
+ else
+ len = strlen( ip ) + 4 + strlen( cl->userinfo );
+
+ if( len >= MAX_INFO_STRING )
+ SV_DropClient( cl, "userinfo string length exceeded" );
+ else
+ Info_SetValueForKey( cl->userinfo, "ip", ip );
+
+}
+
+
+/*
+==================
+SV_UpdateUserinfo_f
+==================
+*/
+static void SV_UpdateUserinfo_f( client_t *cl ) {
+ Q_strncpyz( cl->userinfo, Cmd_Argv(1), sizeof(cl->userinfo) );
+
+ SV_UserinfoChanged( cl );
+ // call prog code to allow overrides
+ VM_Call( gvm, GAME_CLIENT_USERINFO_CHANGED, cl - svs.clients );
+}
+
+
+#ifdef USE_VOIP
+static
+void SV_UpdateVoipIgnore(client_t *cl, const char *idstr, qboolean ignore)
+{
+ if ((*idstr >= '0') && (*idstr <= '9')) {
+ const int id = atoi(idstr);
+ if ((id >= 0) && (id < MAX_CLIENTS)) {
+ cl->ignoreVoipFromClient[id] = ignore;
+ }
+ }
+}
+
+/*
+==================
+SV_Voip_f
+==================
+*/
+static void SV_Voip_f( client_t *cl ) {
+ const char *cmd = Cmd_Argv(1);
+ if (strcmp(cmd, "ignore") == 0) {
+ SV_UpdateVoipIgnore(cl, Cmd_Argv(2), qtrue);
+ } else if (strcmp(cmd, "unignore") == 0) {
+ SV_UpdateVoipIgnore(cl, Cmd_Argv(2), qfalse);
+ } else if (strcmp(cmd, "muteall") == 0) {
+ cl->muteAllVoip = qtrue;
+ } else if (strcmp(cmd, "unmuteall") == 0) {
+ cl->muteAllVoip = qfalse;
+ }
+}
+#endif
+
+
+typedef struct {
+ char *name;
+ void (*func)( client_t *cl );
+} ucmd_t;
+
+static ucmd_t ucmds[] = {
+ {"userinfo", SV_UpdateUserinfo_f},
+ {"disconnect", SV_Disconnect_f},
+ {"cp", SV_VerifyPaks_f},
+ {"vdr", SV_ResetPureClient_f},
+ {"download", SV_BeginDownload_f},
+ {"nextdl", SV_NextDownload_f},
+ {"stopdl", SV_StopDownload_f},
+ {"donedl", SV_DoneDownload_f},
+
+#ifdef USE_VOIP
+ {"voip", SV_Voip_f},
+#endif
+
+ {NULL, NULL}
+};
+
+/*
+==================
+SV_ExecuteClientCommand
+
+Also called by bot code
+==================
+*/
+void SV_ExecuteClientCommand( client_t *cl, const char *s, qboolean clientOK ) {
+ ucmd_t *u;
+ qboolean bProcessed = qfalse;
+
+ Cmd_TokenizeString( s );
+
+ // see if it is a server level command
+ for (u=ucmds ; u->name ; u++) {
+ if (!strcmp (Cmd_Argv(0), u->name) ) {
+ u->func( cl );
+ bProcessed = qtrue;
+ break;
+ }
+ }
+
+ if (clientOK) {
+ // pass unknown strings to the game
+ if (!u->name && sv.state == SS_GAME) {
+ Cmd_Args_Sanitize();
+ VM_Call( gvm, GAME_CLIENT_COMMAND, cl - svs.clients );
+ }
+ }
+ else if (!bProcessed)
+ Com_DPrintf( "client text ignored for %s: %s\n", cl->name, Cmd_Argv(0) );
+}
+
+/*
+===============
+SV_ClientCommand
+===============
+*/
+static qboolean SV_ClientCommand( client_t *cl, msg_t *msg ) {
+ int seq;
+ const char *s;
+ qboolean clientOk = qtrue;
+
+ seq = MSG_ReadLong( msg );
+ s = MSG_ReadString( msg );
+
+ // see if we have already executed it
+ if ( cl->lastClientCommand >= seq ) {
+ return qtrue;
+ }
+
+ Com_DPrintf( "clientCommand: %s : %i : %s\n", cl->name, seq, s );
+
+ // drop the connection if we have somehow lost commands
+ if ( seq > cl->lastClientCommand + 1 ) {
+ Com_Printf( "Client %s lost %i clientCommands\n", cl->name,
+ seq - cl->lastClientCommand + 1 );
+ SV_DropClient( cl, "Lost reliable commands" );
+ return qfalse;
+ }
+
+ // malicious users may try using too many string commands
+ // to lag other players. If we decide that we want to stall
+ // the command, we will stop processing the rest of the packet,
+ // including the usercmd. This causes flooders to lag themselves
+ // but not other people
+ // We don't do this when the client hasn't been active yet since its
+ // normal to spam a lot of commands when downloading
+ if ( !com_cl_running->integer &&
+ cl->state >= CS_ACTIVE &&
+ sv_floodProtect->integer &&
+ svs.time < cl->nextReliableTime ) {
+ // ignore any other text messages from this client but let them keep playing
+ // TTimo - moved the ignored verbose to the actual processing in SV_ExecuteClientCommand, only printing if the core doesn't intercept
+ clientOk = qfalse;
+ }
+
+ // don't allow another command for one second
+ cl->nextReliableTime = svs.time + 1000;
+
+ SV_ExecuteClientCommand( cl, s, clientOk );
+
+ cl->lastClientCommand = seq;
+ Com_sprintf(cl->lastClientCommandString, sizeof(cl->lastClientCommandString), "%s", s);
+
+ return qtrue; // continue procesing
+}
+
+
+//==================================================================================
+
+
+/*
+==================
+SV_ClientThink
+
+Also called by bot code
+==================
+*/
+void SV_ClientThink (client_t *cl, usercmd_t *cmd) {
+ cl->lastUsercmd = *cmd;
+
+ if ( cl->state != CS_ACTIVE ) {
+ return; // may have been kicked during the last usercmd
+ }
+
+ VM_Call( gvm, GAME_CLIENT_THINK, cl - svs.clients );
+}
+
+/*
+==================
+SV_UserMove
+
+The message usually contains all the movement commands
+that were in the last three packets, so that the information
+in dropped packets can be recovered.
+
+On very fast clients, there may be multiple usercmd packed into
+each of the backup packets.
+==================
+*/
+static void SV_UserMove( client_t *cl, msg_t *msg, qboolean delta ) {
+ int i, key;
+ int cmdCount;
+ usercmd_t nullcmd;
+ usercmd_t cmds[MAX_PACKET_USERCMDS];
+ usercmd_t *cmd, *oldcmd;
+
+ if ( delta ) {
+ cl->deltaMessage = cl->messageAcknowledge;
+ } else {
+ cl->deltaMessage = -1;
+ }
+
+ cmdCount = MSG_ReadByte( msg );
+
+ if ( cmdCount < 1 ) {
+ Com_Printf( "cmdCount < 1\n" );
+ return;
+ }
+
+ if ( cmdCount > MAX_PACKET_USERCMDS ) {
+ Com_Printf( "cmdCount > MAX_PACKET_USERCMDS\n" );
+ return;
+ }
+
+ // use the checksum feed in the key
+ key = sv.checksumFeed;
+ // also use the message acknowledge
+ key ^= cl->messageAcknowledge;
+ // also use the last acknowledged server command in the key
+ key ^= MSG_HashKey(cl->reliableCommands[ cl->reliableAcknowledge & (MAX_RELIABLE_COMMANDS-1) ], 32);
+
+ Com_Memset( &nullcmd, 0, sizeof(nullcmd) );
+ oldcmd = &nullcmd;
+ for ( i = 0 ; i < cmdCount ; i++ ) {
+ cmd = &cmds[i];
+ MSG_ReadDeltaUsercmdKey( msg, key, oldcmd, cmd );
+ oldcmd = cmd;
+ }
+
+ // save time for ping calculation
+ cl->frames[ cl->messageAcknowledge & PACKET_MASK ].messageAcked = svs.time;
+
+ // TTimo
+ // catch the no-cp-yet situation before SV_ClientEnterWorld
+ // if CS_ACTIVE, then it's time to trigger a new gamestate emission
+ // if not, then we are getting remaining parasite usermove commands, which we should ignore
+ if (sv_pure->integer != 0 && cl->pureAuthentic == 0 && !cl->gotCP) {
+ if (cl->state == CS_ACTIVE)
+ {
+ // we didn't get a cp yet, don't assume anything and just send the gamestate all over again
+ Com_DPrintf( "%s: didn't get cp command, resending gamestate\n", cl->name);
+ SV_SendClientGameState( cl );
+ }
+ return;
+ }
+
+ // if this is the first usercmd we have received
+ // this gamestate, put the client into the world
+ if ( cl->state == CS_PRIMED ) {
+ SV_ClientEnterWorld( cl, &cmds[0] );
+ // the moves can be processed normaly
+ }
+
+ // a bad cp command was sent, drop the client
+ if (sv_pure->integer != 0 && cl->pureAuthentic == 0) {
+ SV_DropClient( cl, "Cannot validate pure client!");
+ return;
+ }
+
+ if ( cl->state != CS_ACTIVE ) {
+ cl->deltaMessage = -1;
+ return;
+ }
+
+ // usually, the first couple commands will be duplicates
+ // of ones we have previously received, but the servertimes
+ // in the commands will cause them to be immediately discarded
+ for ( i = 0 ; i < cmdCount ; i++ ) {
+ // if this is a cmd from before a map_restart ignore it
+ if ( cmds[i].serverTime > cmds[cmdCount-1].serverTime ) {
+ continue;
+ }
+ // extremely lagged or cmd from before a map_restart
+ //if ( cmds[i].serverTime > svs.time + 3000 ) {
+ // continue;
+ //}
+ // 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;
+ }
+ SV_ClientThink (cl, &cmds[ i ]);
+ }
+}
+
+
+#ifdef USE_VOIP
+static
+qboolean SV_ShouldIgnoreVoipSender(const client_t *cl)
+{
+ if (!sv_voip->integer)
+ return qtrue; // VoIP disabled on this server.
+ else if (!cl->hasVoip) // client doesn't have VoIP support?!
+ return qtrue;
+
+ // !!! FIXME: implement player blacklist.
+
+ return qfalse; // don't ignore.
+}
+
+static
+void SV_UserVoip( client_t *cl, msg_t *msg ) {
+ const int sender = (int) (cl - svs.clients);
+ const int generation = MSG_ReadByte(msg);
+ const int sequence = MSG_ReadLong(msg);
+ const int frames = MSG_ReadByte(msg);
+ const int recip1 = MSG_ReadLong(msg);
+ const int recip2 = MSG_ReadLong(msg);
+ const int recip3 = MSG_ReadLong(msg);
+ const int packetsize = MSG_ReadShort(msg);
+ byte encoded[sizeof (cl->voipPacket[0].data)];
+ client_t *client = NULL;
+ voipServerPacket_t *packet = NULL;
+ int i;
+
+ if (generation < 0)
+ return; // short/invalid packet, bail.
+ else if (sequence < 0)
+ return; // short/invalid packet, bail.
+ else if (frames < 0)
+ return; // short/invalid packet, bail.
+ else if (recip1 < 0)
+ return; // short/invalid packet, bail.
+ else if (recip2 < 0)
+ return; // short/invalid packet, bail.
+ else if (recip3 < 0)
+ return; // short/invalid packet, bail.
+ else if (packetsize < 0)
+ return; // short/invalid packet, bail.
+
+ if (packetsize > sizeof (encoded)) { // overlarge packet?
+ int bytesleft = packetsize;
+ while (bytesleft) {
+ int br = bytesleft;
+ if (br > sizeof (encoded))
+ br = sizeof (encoded);
+ MSG_ReadData(msg, encoded, br);
+ bytesleft -= br;
+ }
+ return; // overlarge packet, bail.
+ }
+
+ MSG_ReadData(msg, encoded, packetsize);
+
+ if (SV_ShouldIgnoreVoipSender(cl))
+ return; // Blacklisted, disabled, etc.
+
+ // !!! FIXME: see if we read past end of msg...
+
+ // !!! FIXME: reject if not speex narrowband codec.
+ // !!! FIXME: decide if this is bogus data?
+
+ // (the three recip* values are 31 bits each (ignores sign bit so we can
+ // get a -1 error from MSG_ReadLong() ... ), allowing for 93 clients.)
+ assert( sv_maxclients->integer < 93 );
+
+ // decide who needs this VoIP packet sent to them...
+ for (i = 0, client = svs.clients; i < sv_maxclients->integer ; i++, client++) {
+ if (client->state != CS_ACTIVE)
+ continue; // not in the game yet, don't send to this guy.
+ else if (i == sender)
+ continue; // don't send voice packet back to original author.
+ else if (!client->hasVoip)
+ continue; // no VoIP support, or support disabled.
+ else if (client->muteAllVoip)
+ continue; // client is ignoring everyone.
+ else if (client->ignoreVoipFromClient[sender])
+ continue; // client is ignoring this talker.
+ else if (*cl->downloadName) // !!! FIXME: possible to DoS?
+ continue; // no VoIP allowed if downloading, to save bandwidth.
+ else if ( ((i >= 0) && (i < 31)) && ((recip1 & (1 << (i-0))) == 0) )
+ continue; // not addressed to this player.
+ else if ( ((i >= 31) && (i < 62)) && ((recip2 & (1 << (i-31))) == 0) )
+ continue; // not addressed to this player.
+ else if ( ((i >= 62) && (i < 93)) && ((recip3 & (1 << (i-62))) == 0) )
+ continue; // not addressed to this player.
+
+ // Transmit this packet to the client.
+ // !!! FIXME: I don't like this queueing system.
+ if (client->queuedVoipPackets >= (sizeof (client->voipPacket) / sizeof (client->voipPacket[0]))) {
+ Com_Printf("Too many VoIP packets queued for client #%d\n", i);
+ continue; // no room for another packet right now.
+ }
+
+ packet = &client->voipPacket[client->queuedVoipPackets];
+ packet->sender = sender;
+ packet->frames = frames;
+ packet->len = packetsize;
+ packet->generation = generation;
+ packet->sequence = sequence;
+ memcpy(packet->data, encoded, packetsize);
+ client->queuedVoipPackets++;
+ }
+}
+#endif
+
+
+
+/*
+===========================================================================
+
+USER CMD EXECUTION
+
+===========================================================================
+*/
+
+/*
+===================
+SV_ExecuteClientMessage
+
+Parse a client packet
+===================
+*/
+void SV_ExecuteClientMessage( client_t *cl, msg_t *msg ) {
+ int c;
+ int serverId;
+
+ MSG_Bitstream(msg);
+
+ serverId = MSG_ReadLong( msg );
+ cl->messageAcknowledge = MSG_ReadLong( msg );
+
+ if (cl->messageAcknowledge < 0) {
+ // usually only hackers create messages like this
+ // it is more annoying for them to let them hanging
+#ifndef NDEBUG
+ SV_DropClient( cl, "DEBUG: illegible client message" );
+#endif
+ return;
+ }
+
+ cl->reliableAcknowledge = MSG_ReadLong( msg );
+
+ // NOTE: when the client message is fux0red the acknowledgement numbers
+ // can be out of range, this could cause the server to send thousands of server
+ // commands which the server thinks are not yet acknowledged in SV_UpdateServerCommandsToClient
+ if (cl->reliableAcknowledge < cl->reliableSequence - MAX_RELIABLE_COMMANDS) {
+ // usually only hackers create messages like this
+ // it is more annoying for them to let them hanging
+#ifndef NDEBUG
+ SV_DropClient( cl, "DEBUG: illegible client message" );
+#endif
+ cl->reliableAcknowledge = cl->reliableSequence;
+ return;
+ }
+ // if this is a usercmd from a previous gamestate,
+ // ignore it or retransmit the current gamestate
+ //
+ // if the client was downloading, let it stay at whatever serverId and
+ // gamestate it was at. This allows it to keep downloading even when
+ // the gamestate changes. After the download is finished, we'll
+ // notice and send it a new game state
+ //
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=536
+ // don't drop as long as previous command was a nextdl, after a dl is done, downloadName is set back to ""
+ // but we still need to read the next message to move to next download or send gamestate
+ // I don't like this hack though, it must have been working fine at some point, suspecting the fix is somewhere else
+ if ( serverId != sv.serverId && !*cl->downloadName && !strstr(cl->lastClientCommandString, "nextdl") ) {
+ if ( serverId >= sv.restartedServerId && serverId < sv.serverId ) { // TTimo - use a comparison here to catch multiple map_restart
+ // they just haven't caught the map_restart yet
+ Com_DPrintf("%s : ignoring pre map_restart / outdated client message\n", cl->name);
+ return;
+ }
+ // if we can tell that the client has dropped the last
+ // gamestate we sent them, resend it
+ if ( cl->messageAcknowledge > cl->gamestateMessageNum ) {
+ Com_DPrintf( "%s : dropped gamestate, resending\n", cl->name );
+ SV_SendClientGameState( cl );
+ }
+ return;
+ }
+
+ // this client has acknowledged the new gamestate so it's
+ // safe to start sending it the real time again
+ if( cl->oldServerTime && serverId == sv.serverId ){
+ Com_DPrintf( "%s acknowledged gamestate\n", cl->name );
+ cl->oldServerTime = 0;
+ }
+
+ // read optional clientCommand strings
+ do {
+ c = MSG_ReadByte( msg );
+
+ // See if this is an extension command after the EOF, which means we
+ // got data that a legacy server should ignore.
+ if ((c == clc_EOF) && (MSG_LookaheadByte( msg ) == clc_extension)) {
+ MSG_ReadByte( msg ); // throw the clc_extension byte away.
+ c = MSG_ReadByte( msg ); // something legacy servers can't do!
+ // sometimes you get a clc_extension at end of stream...dangling
+ // bits in the huffman decoder giving a bogus value?
+ if (c == -1) {
+ c = clc_EOF;
+ }
+ }
+
+ if ( c == clc_EOF ) {
+ break;
+ }
+
+ if ( c != clc_clientCommand ) {
+ break;
+ }
+ if ( !SV_ClientCommand( cl, msg ) ) {
+ return; // we couldn't execute it because of the flood protection
+ }
+ if (cl->state == CS_ZOMBIE) {
+ return; // disconnect command
+ }
+ } while ( 1 );
+
+ // read the usercmd_t
+ if ( c == clc_move ) {
+ SV_UserMove( cl, msg, qtrue );
+ } else if ( c == clc_moveNoDelta ) {
+ SV_UserMove( cl, msg, qfalse );
+ } else if ( c == clc_voip ) {
+#ifdef USE_VOIP
+ SV_UserVoip( cl, msg );
+#endif
+ } else if ( c != clc_EOF ) {
+ Com_Printf( "WARNING: bad command byte for client %i\n", (int) (cl - svs.clients) );
+ }
+// if ( msg->readcount != msg->cursize ) {
+// Com_Printf( "WARNING: Junk at end of packet for client %i\n", cl - svs.clients );
+// }
+}
diff --git a/code/server/sv_game.c b/code/server/sv_game.c
new file mode 100644
index 0000000..fe78650
--- /dev/null
+++ b/code/server/sv_game.c
@@ -0,0 +1,965 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+// sv_game.c -- interface to the game dll
+
+#include "server.h"
+
+#include "../botlib/botlib.h"
+
+botlib_export_t *botlib_export;
+
+void SV_GameError( const char *string ) {
+ Com_Error( ERR_DROP, "%s", string );
+}
+
+void SV_GamePrint( const char *string ) {
+ Com_Printf( "%s", string );
+}
+
+// these functions must be used instead of pointer arithmetic, because
+// the game allocates gentities with private information after the server shared part
+int SV_NumForGentity( sharedEntity_t *ent ) {
+ int num;
+
+ num = ( (byte *)ent - (byte *)sv.gentities ) / sv.gentitySize;
+
+ return num;
+}
+
+sharedEntity_t *SV_GentityNum( int num ) {
+ sharedEntity_t *ent;
+
+ ent = (sharedEntity_t *)((byte *)sv.gentities + sv.gentitySize*(num));
+
+ return ent;
+}
+
+playerState_t *SV_GameClientNum( int num ) {
+ playerState_t *ps;
+
+ ps = (playerState_t *)((byte *)sv.gameClients + sv.gameClientSize*(num));
+
+ return ps;
+}
+
+svEntity_t *SV_SvEntityForGentity( sharedEntity_t *gEnt ) {
+ if ( !gEnt || gEnt->s.number < 0 || gEnt->s.number >= MAX_GENTITIES ) {
+ Com_Error( ERR_DROP, "SV_SvEntityForGentity: bad gEnt" );
+ }
+ return &sv.svEntities[ gEnt->s.number ];
+}
+
+sharedEntity_t *SV_GEntityForSvEntity( svEntity_t *svEnt ) {
+ int num;
+
+ num = svEnt - sv.svEntities;
+ return SV_GentityNum( num );
+}
+
+/*
+===============
+SV_GameSendServerCommand
+
+Sends a command string to a client
+===============
+*/
+void SV_GameSendServerCommand( int clientNum, const char *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_GameDropClient
+
+Disconnects the client with a message
+===============
+*/
+void SV_GameDropClient( int clientNum, const char *reason ) {
+ if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) {
+ return;
+ }
+ SV_DropClient( svs.clients + clientNum, reason );
+}
+
+
+/*
+=================
+SV_SetBrushModel
+
+sets mins and maxs for inline bmodels
+=================
+*/
+void SV_SetBrushModel( sharedEntity_t *ent, const char *name ) {
+ clipHandle_t h;
+ vec3_t mins, maxs;
+
+ if (!name) {
+ Com_Error( ERR_DROP, "SV_SetBrushModel: NULL" );
+ }
+
+ if (name[0] != '*') {
+ Com_Error( ERR_DROP, "SV_SetBrushModel: %s isn't a brush model", name );
+ }
+
+
+ ent->s.modelindex = atoi( name + 1 );
+
+ h = CM_InlineModel( ent->s.modelindex );
+ CM_ModelBounds( h, mins, maxs );
+ VectorCopy (mins, ent->r.mins);
+ VectorCopy (maxs, ent->r.maxs);
+ ent->r.bmodel = qtrue;
+
+ ent->r.contents = -1; // we don't know exactly what is in the brushes
+
+ SV_LinkEntity( ent ); // FIXME: remove
+}
+
+
+
+/*
+=================
+SV_inPVS
+
+Also checks portalareas so that doors block sight
+=================
+*/
+qboolean SV_inPVS (const vec3_t p1, const vec3_t p2)
+{
+ int leafnum;
+ int cluster;
+ int area1, area2;
+ byte *mask;
+
+ leafnum = CM_PointLeafnum (p1);
+ cluster = CM_LeafCluster (leafnum);
+ area1 = CM_LeafArea (leafnum);
+ mask = CM_ClusterPVS (cluster);
+
+ leafnum = CM_PointLeafnum (p2);
+ cluster = CM_LeafCluster (leafnum);
+ area2 = CM_LeafArea (leafnum);
+ if ( mask && (!(mask[cluster>>3] & (1<<(cluster&7)) ) ) )
+ return qfalse;
+ if (!CM_AreasConnected (area1, area2))
+ return qfalse; // a door blocks sight
+ return qtrue;
+}
+
+
+/*
+=================
+SV_inPVSIgnorePortals
+
+Does NOT check portalareas
+=================
+*/
+qboolean SV_inPVSIgnorePortals( const vec3_t p1, const vec3_t p2)
+{
+ int leafnum;
+ int cluster;
+ byte *mask;
+
+ leafnum = CM_PointLeafnum (p1);
+ cluster = CM_LeafCluster (leafnum);
+ mask = CM_ClusterPVS (cluster);
+
+ leafnum = CM_PointLeafnum (p2);
+ cluster = CM_LeafCluster (leafnum);
+
+ if ( mask && (!(mask[cluster>>3] & (1<<(cluster&7)) ) ) )
+ return qfalse;
+
+ return qtrue;
+}
+
+
+/*
+========================
+SV_AdjustAreaPortalState
+========================
+*/
+void SV_AdjustAreaPortalState( sharedEntity_t *ent, qboolean open ) {
+ svEntity_t *svEnt;
+
+ svEnt = SV_SvEntityForGentity( ent );
+ if ( svEnt->areanum2 == -1 ) {
+ return;
+ }
+ CM_AdjustAreaPortalState( svEnt->areanum, svEnt->areanum2, open );
+}
+
+
+/*
+==================
+SV_GameAreaEntities
+==================
+*/
+qboolean SV_EntityContact( vec3_t mins, vec3_t maxs, const sharedEntity_t *gEnt, int capsule ) {
+ const float *origin, *angles;
+ clipHandle_t ch;
+ trace_t trace;
+
+ // check for exact collision
+ origin = gEnt->r.currentOrigin;
+ angles = gEnt->r.currentAngles;
+
+ ch = SV_ClipHandleForEntity( gEnt );
+ CM_TransformedBoxTrace ( &trace, vec3_origin, vec3_origin, mins, maxs,
+ ch, -1, origin, angles, capsule );
+
+ return trace.startsolid;
+}
+
+
+/*
+===============
+SV_GetServerinfo
+
+===============
+*/
+void SV_GetServerinfo( char *buffer, int bufferSize ) {
+ if ( bufferSize < 1 ) {
+ Com_Error( ERR_DROP, "SV_GetServerinfo: bufferSize == %i", bufferSize );
+ }
+ Q_strncpyz( buffer, Cvar_InfoString( CVAR_SERVERINFO ), bufferSize );
+}
+
+/*
+===============
+SV_LocateGameData
+
+===============
+*/
+void SV_LocateGameData( sharedEntity_t *gEnts, int numGEntities, int sizeofGEntity_t,
+ playerState_t *clients, int sizeofGameClient ) {
+ sv.gentities = gEnts;
+ sv.gentitySize = sizeofGEntity_t;
+ sv.num_entities = numGEntities;
+
+ sv.gameClients = clients;
+ sv.gameClientSize = sizeofGameClient;
+}
+
+
+/*
+===============
+SV_GetUsercmd
+
+===============
+*/
+void SV_GetUsercmd( int clientNum, usercmd_t *cmd ) {
+ if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) {
+ Com_Error( ERR_DROP, "SV_GetUsercmd: bad clientNum:%i", clientNum );
+ }
+ *cmd = svs.clients[clientNum].lastUsercmd;
+}
+
+//==============================================
+
+static int FloatAsInt( float f ) {
+ floatint_t fi;
+ fi.f = f;
+ return fi.i;
+}
+
+/*
+====================
+SV_GameSystemCalls
+
+The module is making a system call
+====================
+*/
+intptr_t SV_GameSystemCalls( intptr_t *args ) {
+ switch( args[0] ) {
+ case G_PRINT:
+ Com_Printf( "%s", (const char*)VMA(1) );
+ return 0;
+ case G_ERROR:
+ Com_Error( ERR_DROP, "%s", (const char*)VMA(1) );
+ return 0;
+ case G_MILLISECONDS:
+ return Sys_Milliseconds();
+ case G_CVAR_REGISTER:
+ Cvar_Register( VMA(1), VMA(2), VMA(3), args[4] );
+ return 0;
+ case G_CVAR_UPDATE:
+ Cvar_Update( VMA(1) );
+ return 0;
+ case G_CVAR_SET:
+ Cvar_Set( (const char *)VMA(1), (const char *)VMA(2) );
+ return 0;
+ case G_CVAR_VARIABLE_INTEGER_VALUE:
+ return Cvar_VariableIntegerValue( (const char *)VMA(1) );
+ case G_CVAR_VARIABLE_STRING_BUFFER:
+ Cvar_VariableStringBuffer( VMA(1), VMA(2), args[3] );
+ return 0;
+ case G_ARGC:
+ return Cmd_Argc();
+ case G_ARGV:
+ Cmd_ArgvBuffer( args[1], VMA(2), args[3] );
+ return 0;
+ case G_SEND_CONSOLE_COMMAND:
+ Cbuf_ExecuteText( args[1], VMA(2) );
+ return 0;
+
+ case G_FS_FOPEN_FILE:
+ return FS_FOpenFileByMode( VMA(1), VMA(2), args[3] );
+ case G_FS_READ:
+ FS_Read2( VMA(1), args[2], args[3] );
+ return 0;
+ case G_FS_WRITE:
+ FS_Write( VMA(1), args[2], args[3] );
+ return 0;
+ case G_FS_FCLOSE_FILE:
+ FS_FCloseFile( args[1] );
+ return 0;
+ case G_FS_GETFILELIST:
+ return FS_GetFileList( VMA(1), VMA(2), VMA(3), args[4] );
+ case G_FS_SEEK:
+ return FS_Seek( args[1], args[2], args[3] );
+
+ case G_LOCATE_GAME_DATA:
+ SV_LocateGameData( VMA(1), args[2], args[3], VMA(4), args[5] );
+ return 0;
+ case G_DROP_CLIENT:
+ SV_GameDropClient( args[1], VMA(2) );
+ return 0;
+ case G_SEND_SERVER_COMMAND:
+ SV_GameSendServerCommand( args[1], VMA(2) );
+ return 0;
+ case G_LINKENTITY:
+ SV_LinkEntity( VMA(1) );
+ return 0;
+ case G_UNLINKENTITY:
+ SV_UnlinkEntity( VMA(1) );
+ return 0;
+ case G_ENTITIES_IN_BOX:
+ return SV_AreaEntities( VMA(1), VMA(2), VMA(3), args[4] );
+ case G_ENTITY_CONTACT:
+ return SV_EntityContact( VMA(1), VMA(2), VMA(3), /*int capsule*/ qfalse );
+ case G_ENTITY_CONTACTCAPSULE:
+ return SV_EntityContact( VMA(1), VMA(2), VMA(3), /*int capsule*/ qtrue );
+ case G_TRACE:
+ SV_Trace( VMA(1), VMA(2), VMA(3), VMA(4), VMA(5), args[6], args[7], /*int capsule*/ qfalse );
+ return 0;
+ case G_TRACECAPSULE:
+ SV_Trace( VMA(1), VMA(2), VMA(3), VMA(4), VMA(5), args[6], args[7], /*int capsule*/ qtrue );
+ return 0;
+ case G_POINT_CONTENTS:
+ return SV_PointContents( VMA(1), args[2] );
+ case G_SET_BRUSH_MODEL:
+ SV_SetBrushModel( VMA(1), VMA(2) );
+ return 0;
+ case G_IN_PVS:
+ return SV_inPVS( VMA(1), VMA(2) );
+ case G_IN_PVS_IGNORE_PORTALS:
+ return SV_inPVSIgnorePortals( VMA(1), VMA(2) );
+
+ case G_SET_CONFIGSTRING:
+ SV_SetConfigstring( args[1], VMA(2) );
+ return 0;
+ case G_GET_CONFIGSTRING:
+ SV_GetConfigstring( args[1], VMA(2), args[3] );
+ return 0;
+ case G_SET_USERINFO:
+ SV_SetUserinfo( args[1], VMA(2) );
+ return 0;
+ case G_GET_USERINFO:
+ SV_GetUserinfo( args[1], VMA(2), args[3] );
+ return 0;
+ case G_GET_SERVERINFO:
+ SV_GetServerinfo( VMA(1), args[2] );
+ return 0;
+ case G_ADJUST_AREA_PORTAL_STATE:
+ SV_AdjustAreaPortalState( VMA(1), args[2] );
+ return 0;
+ case G_AREAS_CONNECTED:
+ return CM_AreasConnected( args[1], args[2] );
+
+ case G_BOT_ALLOCATE_CLIENT:
+ return SV_BotAllocateClient();
+ case G_BOT_FREE_CLIENT:
+ SV_BotFreeClient( args[1] );
+ return 0;
+
+ case G_GET_USERCMD:
+ SV_GetUsercmd( args[1], VMA(2) );
+ return 0;
+ case G_GET_ENTITY_TOKEN:
+ {
+ const char *s;
+
+ s = COM_Parse( &sv.entityParsePoint );
+ Q_strncpyz( VMA(1), s, args[2] );
+ if ( !sv.entityParsePoint && !s[0] ) {
+ return qfalse;
+ } else {
+ return qtrue;
+ }
+ }
+
+ case G_DEBUG_POLYGON_CREATE:
+ return BotImport_DebugPolygonCreate( args[1], args[2], VMA(3) );
+ case G_DEBUG_POLYGON_DELETE:
+ BotImport_DebugPolygonDelete( args[1] );
+ return 0;
+ case G_REAL_TIME:
+ return Com_RealTime( VMA(1) );
+ case G_SNAPVECTOR:
+ Sys_SnapVector( VMA(1) );
+ return 0;
+
+ //====================================
+
+ case BOTLIB_SETUP:
+ return SV_BotLibSetup();
+ case BOTLIB_SHUTDOWN:
+ return SV_BotLibShutdown();
+ case BOTLIB_LIBVAR_SET:
+ return botlib_export->BotLibVarSet( VMA(1), VMA(2) );
+ case BOTLIB_LIBVAR_GET:
+ return botlib_export->BotLibVarGet( VMA(1), VMA(2), args[3] );
+
+ case BOTLIB_PC_ADD_GLOBAL_DEFINE:
+ return botlib_export->PC_AddGlobalDefine( VMA(1) );
+ case BOTLIB_PC_LOAD_SOURCE:
+ return botlib_export->PC_LoadSourceHandle( VMA(1) );
+ case BOTLIB_PC_FREE_SOURCE:
+ return botlib_export->PC_FreeSourceHandle( args[1] );
+ case BOTLIB_PC_READ_TOKEN:
+ return botlib_export->PC_ReadTokenHandle( args[1], VMA(2) );
+ case BOTLIB_PC_SOURCE_FILE_AND_LINE:
+ return botlib_export->PC_SourceFileAndLine( args[1], VMA(2), VMA(3) );
+
+ case BOTLIB_START_FRAME:
+ return botlib_export->BotLibStartFrame( VMF(1) );
+ case BOTLIB_LOAD_MAP:
+ return botlib_export->BotLibLoadMap( VMA(1) );
+ case BOTLIB_UPDATENTITY:
+ return botlib_export->BotLibUpdateEntity( args[1], VMA(2) );
+ case BOTLIB_TEST:
+ return botlib_export->Test( args[1], VMA(2), VMA(3), VMA(4) );
+
+ case BOTLIB_GET_SNAPSHOT_ENTITY:
+ return SV_BotGetSnapshotEntity( args[1], args[2] );
+ case BOTLIB_GET_CONSOLE_MESSAGE:
+ return SV_BotGetConsoleMessage( args[1], VMA(2), args[3] );
+ case BOTLIB_USER_COMMAND:
+ SV_ClientThink( &svs.clients[args[1]], VMA(2) );
+ return 0;
+
+ case BOTLIB_AAS_BBOX_AREAS:
+ return botlib_export->aas.AAS_BBoxAreas( VMA(1), VMA(2), VMA(3), args[4] );
+ case BOTLIB_AAS_AREA_INFO:
+ return botlib_export->aas.AAS_AreaInfo( args[1], VMA(2) );
+ case BOTLIB_AAS_ALTERNATIVE_ROUTE_GOAL:
+ return botlib_export->aas.AAS_AlternativeRouteGoals( VMA(1), args[2], VMA(3), args[4], args[5], VMA(6), args[7], args[8] );
+ case BOTLIB_AAS_ENTITY_INFO:
+ botlib_export->aas.AAS_EntityInfo( args[1], VMA(2) );
+ return 0;
+
+ case BOTLIB_AAS_INITIALIZED:
+ return botlib_export->aas.AAS_Initialized();
+ case BOTLIB_AAS_PRESENCE_TYPE_BOUNDING_BOX:
+ botlib_export->aas.AAS_PresenceTypeBoundingBox( args[1], VMA(2), VMA(3) );
+ return 0;
+ case BOTLIB_AAS_TIME:
+ return FloatAsInt( botlib_export->aas.AAS_Time() );
+
+ case BOTLIB_AAS_POINT_AREA_NUM:
+ return botlib_export->aas.AAS_PointAreaNum( VMA(1) );
+ case BOTLIB_AAS_POINT_REACHABILITY_AREA_INDEX:
+ return botlib_export->aas.AAS_PointReachabilityAreaIndex( VMA(1) );
+ case BOTLIB_AAS_TRACE_AREAS:
+ return botlib_export->aas.AAS_TraceAreas( VMA(1), VMA(2), VMA(3), VMA(4), args[5] );
+
+ case BOTLIB_AAS_POINT_CONTENTS:
+ return botlib_export->aas.AAS_PointContents( VMA(1) );
+ case BOTLIB_AAS_NEXT_BSP_ENTITY:
+ return botlib_export->aas.AAS_NextBSPEntity( args[1] );
+ case BOTLIB_AAS_VALUE_FOR_BSP_EPAIR_KEY:
+ return botlib_export->aas.AAS_ValueForBSPEpairKey( args[1], VMA(2), VMA(3), args[4] );
+ case BOTLIB_AAS_VECTOR_FOR_BSP_EPAIR_KEY:
+ return botlib_export->aas.AAS_VectorForBSPEpairKey( args[1], VMA(2), VMA(3) );
+ case BOTLIB_AAS_FLOAT_FOR_BSP_EPAIR_KEY:
+ return botlib_export->aas.AAS_FloatForBSPEpairKey( args[1], VMA(2), VMA(3) );
+ case BOTLIB_AAS_INT_FOR_BSP_EPAIR_KEY:
+ return botlib_export->aas.AAS_IntForBSPEpairKey( args[1], VMA(2), VMA(3) );
+
+ case BOTLIB_AAS_AREA_REACHABILITY:
+ return botlib_export->aas.AAS_AreaReachability( args[1] );
+
+ case BOTLIB_AAS_AREA_TRAVEL_TIME_TO_GOAL_AREA:
+ return botlib_export->aas.AAS_AreaTravelTimeToGoalArea( args[1], VMA(2), args[3], args[4] );
+ case BOTLIB_AAS_ENABLE_ROUTING_AREA:
+ return botlib_export->aas.AAS_EnableRoutingArea( args[1], args[2] );
+ case BOTLIB_AAS_PREDICT_ROUTE:
+ return botlib_export->aas.AAS_PredictRoute( VMA(1), args[2], VMA(3), args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11] );
+
+ case BOTLIB_AAS_SWIMMING:
+ return botlib_export->aas.AAS_Swimming( VMA(1) );
+ case BOTLIB_AAS_PREDICT_CLIENT_MOVEMENT:
+ return botlib_export->aas.AAS_PredictClientMovement( VMA(1), args[2], VMA(3), args[4], args[5],
+ VMA(6), VMA(7), args[8], args[9], VMF(10), args[11], args[12], args[13] );
+
+ case BOTLIB_EA_SAY:
+ botlib_export->ea.EA_Say( args[1], VMA(2) );
+ return 0;
+ case BOTLIB_EA_SAY_TEAM:
+ botlib_export->ea.EA_SayTeam( args[1], VMA(2) );
+ return 0;
+ case BOTLIB_EA_COMMAND:
+ botlib_export->ea.EA_Command( args[1], VMA(2) );
+ return 0;
+
+ case BOTLIB_EA_ACTION:
+ botlib_export->ea.EA_Action( args[1], args[2] );
+ break;
+ case BOTLIB_EA_GESTURE:
+ botlib_export->ea.EA_Gesture( args[1] );
+ return 0;
+ case BOTLIB_EA_TALK:
+ botlib_export->ea.EA_Talk( args[1] );
+ return 0;
+ case BOTLIB_EA_ATTACK:
+ botlib_export->ea.EA_Attack( args[1] );
+ return 0;
+ case BOTLIB_EA_USE:
+ botlib_export->ea.EA_Use( args[1] );
+ return 0;
+ case BOTLIB_EA_RESPAWN:
+ botlib_export->ea.EA_Respawn( args[1] );
+ return 0;
+ case BOTLIB_EA_CROUCH:
+ botlib_export->ea.EA_Crouch( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE_UP:
+ botlib_export->ea.EA_MoveUp( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE_DOWN:
+ botlib_export->ea.EA_MoveDown( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE_FORWARD:
+ botlib_export->ea.EA_MoveForward( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE_BACK:
+ botlib_export->ea.EA_MoveBack( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE_LEFT:
+ botlib_export->ea.EA_MoveLeft( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE_RIGHT:
+ botlib_export->ea.EA_MoveRight( args[1] );
+ return 0;
+
+ case BOTLIB_EA_SELECT_WEAPON:
+ botlib_export->ea.EA_SelectWeapon( args[1], args[2] );
+ return 0;
+ case BOTLIB_EA_JUMP:
+ botlib_export->ea.EA_Jump( args[1] );
+ return 0;
+ case BOTLIB_EA_DELAYED_JUMP:
+ botlib_export->ea.EA_DelayedJump( args[1] );
+ return 0;
+ case BOTLIB_EA_MOVE:
+ botlib_export->ea.EA_Move( args[1], VMA(2), VMF(3) );
+ return 0;
+ case BOTLIB_EA_VIEW:
+ botlib_export->ea.EA_View( args[1], VMA(2) );
+ return 0;
+
+ case BOTLIB_EA_END_REGULAR:
+ botlib_export->ea.EA_EndRegular( args[1], VMF(2) );
+ return 0;
+ case BOTLIB_EA_GET_INPUT:
+ botlib_export->ea.EA_GetInput( args[1], VMF(2), VMA(3) );
+ return 0;
+ case BOTLIB_EA_RESET_INPUT:
+ botlib_export->ea.EA_ResetInput( args[1] );
+ return 0;
+
+ case BOTLIB_AI_LOAD_CHARACTER:
+ return botlib_export->ai.BotLoadCharacter( VMA(1), VMF(2) );
+ case BOTLIB_AI_FREE_CHARACTER:
+ botlib_export->ai.BotFreeCharacter( args[1] );
+ return 0;
+ case BOTLIB_AI_CHARACTERISTIC_FLOAT:
+ return FloatAsInt( botlib_export->ai.Characteristic_Float( args[1], args[2] ) );
+ case BOTLIB_AI_CHARACTERISTIC_BFLOAT:
+ return FloatAsInt( botlib_export->ai.Characteristic_BFloat( args[1], args[2], VMF(3), VMF(4) ) );
+ case BOTLIB_AI_CHARACTERISTIC_INTEGER:
+ return botlib_export->ai.Characteristic_Integer( args[1], args[2] );
+ case BOTLIB_AI_CHARACTERISTIC_BINTEGER:
+ return botlib_export->ai.Characteristic_BInteger( args[1], args[2], args[3], args[4] );
+ case BOTLIB_AI_CHARACTERISTIC_STRING:
+ botlib_export->ai.Characteristic_String( args[1], args[2], VMA(3), args[4] );
+ return 0;
+
+ case BOTLIB_AI_ALLOC_CHAT_STATE:
+ return botlib_export->ai.BotAllocChatState();
+ case BOTLIB_AI_FREE_CHAT_STATE:
+ botlib_export->ai.BotFreeChatState( args[1] );
+ return 0;
+ case BOTLIB_AI_QUEUE_CONSOLE_MESSAGE:
+ botlib_export->ai.BotQueueConsoleMessage( args[1], args[2], VMA(3) );
+ return 0;
+ case BOTLIB_AI_REMOVE_CONSOLE_MESSAGE:
+ botlib_export->ai.BotRemoveConsoleMessage( args[1], args[2] );
+ return 0;
+ case BOTLIB_AI_NEXT_CONSOLE_MESSAGE:
+ return botlib_export->ai.BotNextConsoleMessage( args[1], VMA(2) );
+ case BOTLIB_AI_NUM_CONSOLE_MESSAGE:
+ return botlib_export->ai.BotNumConsoleMessages( args[1] );
+ case BOTLIB_AI_INITIAL_CHAT:
+ botlib_export->ai.BotInitialChat( args[1], VMA(2), args[3], VMA(4), VMA(5), VMA(6), VMA(7), VMA(8), VMA(9), VMA(10), VMA(11) );
+ return 0;
+ case BOTLIB_AI_NUM_INITIAL_CHATS:
+ return botlib_export->ai.BotNumInitialChats( args[1], VMA(2) );
+ case BOTLIB_AI_REPLY_CHAT:
+ return botlib_export->ai.BotReplyChat( args[1], VMA(2), args[3], args[4], VMA(5), VMA(6), VMA(7), VMA(8), VMA(9), VMA(10), VMA(11), VMA(12) );
+ case BOTLIB_AI_CHAT_LENGTH:
+ return botlib_export->ai.BotChatLength( args[1] );
+ case BOTLIB_AI_ENTER_CHAT:
+ botlib_export->ai.BotEnterChat( args[1], args[2], args[3] );
+ return 0;
+ case BOTLIB_AI_GET_CHAT_MESSAGE:
+ botlib_export->ai.BotGetChatMessage( args[1], VMA(2), args[3] );
+ return 0;
+ case BOTLIB_AI_STRING_CONTAINS:
+ return botlib_export->ai.StringContains( VMA(1), VMA(2), args[3] );
+ case BOTLIB_AI_FIND_MATCH:
+ return botlib_export->ai.BotFindMatch( VMA(1), VMA(2), args[3] );
+ case BOTLIB_AI_MATCH_VARIABLE:
+ botlib_export->ai.BotMatchVariable( VMA(1), args[2], VMA(3), args[4] );
+ return 0;
+ case BOTLIB_AI_UNIFY_WHITE_SPACES:
+ botlib_export->ai.UnifyWhiteSpaces( VMA(1) );
+ return 0;
+ case BOTLIB_AI_REPLACE_SYNONYMS:
+ botlib_export->ai.BotReplaceSynonyms( VMA(1), args[2] );
+ return 0;
+ case BOTLIB_AI_LOAD_CHAT_FILE:
+ return botlib_export->ai.BotLoadChatFile( args[1], VMA(2), VMA(3) );
+ case BOTLIB_AI_SET_CHAT_GENDER:
+ botlib_export->ai.BotSetChatGender( args[1], args[2] );
+ return 0;
+ case BOTLIB_AI_SET_CHAT_NAME:
+ botlib_export->ai.BotSetChatName( args[1], VMA(2), args[3] );
+ return 0;
+
+ case BOTLIB_AI_RESET_GOAL_STATE:
+ botlib_export->ai.BotResetGoalState( args[1] );
+ return 0;
+ case BOTLIB_AI_RESET_AVOID_GOALS:
+ botlib_export->ai.BotResetAvoidGoals( args[1] );
+ return 0;
+ case BOTLIB_AI_REMOVE_FROM_AVOID_GOALS:
+ botlib_export->ai.BotRemoveFromAvoidGoals( args[1], args[2] );
+ return 0;
+ case BOTLIB_AI_PUSH_GOAL:
+ botlib_export->ai.BotPushGoal( args[1], VMA(2) );
+ return 0;
+ case BOTLIB_AI_POP_GOAL:
+ botlib_export->ai.BotPopGoal( args[1] );
+ return 0;
+ case BOTLIB_AI_EMPTY_GOAL_STACK:
+ botlib_export->ai.BotEmptyGoalStack( args[1] );
+ return 0;
+ case BOTLIB_AI_DUMP_AVOID_GOALS:
+ botlib_export->ai.BotDumpAvoidGoals( args[1] );
+ return 0;
+ case BOTLIB_AI_DUMP_GOAL_STACK:
+ botlib_export->ai.BotDumpGoalStack( args[1] );
+ return 0;
+ case BOTLIB_AI_GOAL_NAME:
+ botlib_export->ai.BotGoalName( args[1], VMA(2), args[3] );
+ return 0;
+ case BOTLIB_AI_GET_TOP_GOAL:
+ return botlib_export->ai.BotGetTopGoal( args[1], VMA(2) );
+ case BOTLIB_AI_GET_SECOND_GOAL:
+ return botlib_export->ai.BotGetSecondGoal( args[1], VMA(2) );
+ case BOTLIB_AI_CHOOSE_LTG_ITEM:
+ return botlib_export->ai.BotChooseLTGItem( args[1], VMA(2), VMA(3), args[4] );
+ case BOTLIB_AI_CHOOSE_NBG_ITEM:
+ return botlib_export->ai.BotChooseNBGItem( args[1], VMA(2), VMA(3), args[4], VMA(5), VMF(6) );
+ case BOTLIB_AI_TOUCHING_GOAL:
+ return botlib_export->ai.BotTouchingGoal( VMA(1), VMA(2) );
+ case BOTLIB_AI_ITEM_GOAL_IN_VIS_BUT_NOT_VISIBLE:
+ return botlib_export->ai.BotItemGoalInVisButNotVisible( args[1], VMA(2), VMA(3), VMA(4) );
+ case BOTLIB_AI_GET_LEVEL_ITEM_GOAL:
+ return botlib_export->ai.BotGetLevelItemGoal( args[1], VMA(2), VMA(3) );
+ case BOTLIB_AI_GET_NEXT_CAMP_SPOT_GOAL:
+ return botlib_export->ai.BotGetNextCampSpotGoal( args[1], VMA(2) );
+ case BOTLIB_AI_GET_MAP_LOCATION_GOAL:
+ return botlib_export->ai.BotGetMapLocationGoal( VMA(1), VMA(2) );
+ case BOTLIB_AI_AVOID_GOAL_TIME:
+ return FloatAsInt( botlib_export->ai.BotAvoidGoalTime( args[1], args[2] ) );
+ case BOTLIB_AI_SET_AVOID_GOAL_TIME:
+ botlib_export->ai.BotSetAvoidGoalTime( args[1], args[2], VMF(3));
+ return 0;
+ case BOTLIB_AI_INIT_LEVEL_ITEMS:
+ botlib_export->ai.BotInitLevelItems();
+ return 0;
+ case BOTLIB_AI_UPDATE_ENTITY_ITEMS:
+ botlib_export->ai.BotUpdateEntityItems();
+ return 0;
+ case BOTLIB_AI_LOAD_ITEM_WEIGHTS:
+ return botlib_export->ai.BotLoadItemWeights( args[1], VMA(2) );
+ case BOTLIB_AI_FREE_ITEM_WEIGHTS:
+ botlib_export->ai.BotFreeItemWeights( args[1] );
+ return 0;
+ case BOTLIB_AI_INTERBREED_GOAL_FUZZY_LOGIC:
+ botlib_export->ai.BotInterbreedGoalFuzzyLogic( args[1], args[2], args[3] );
+ return 0;
+ case BOTLIB_AI_SAVE_GOAL_FUZZY_LOGIC:
+ botlib_export->ai.BotSaveGoalFuzzyLogic( args[1], VMA(2) );
+ return 0;
+ case BOTLIB_AI_MUTATE_GOAL_FUZZY_LOGIC:
+ botlib_export->ai.BotMutateGoalFuzzyLogic( args[1], VMF(2) );
+ return 0;
+ case BOTLIB_AI_ALLOC_GOAL_STATE:
+ return botlib_export->ai.BotAllocGoalState( args[1] );
+ case BOTLIB_AI_FREE_GOAL_STATE:
+ botlib_export->ai.BotFreeGoalState( args[1] );
+ return 0;
+
+ case BOTLIB_AI_RESET_MOVE_STATE:
+ botlib_export->ai.BotResetMoveState( args[1] );
+ return 0;
+ case BOTLIB_AI_ADD_AVOID_SPOT:
+ botlib_export->ai.BotAddAvoidSpot( args[1], VMA(2), VMF(3), args[4] );
+ return 0;
+ case BOTLIB_AI_MOVE_TO_GOAL:
+ botlib_export->ai.BotMoveToGoal( VMA(1), args[2], VMA(3), args[4] );
+ return 0;
+ case BOTLIB_AI_MOVE_IN_DIRECTION:
+ return botlib_export->ai.BotMoveInDirection( args[1], VMA(2), VMF(3), args[4] );
+ case BOTLIB_AI_RESET_AVOID_REACH:
+ botlib_export->ai.BotResetAvoidReach( args[1] );
+ return 0;
+ case BOTLIB_AI_RESET_LAST_AVOID_REACH:
+ botlib_export->ai.BotResetLastAvoidReach( args[1] );
+ return 0;
+ case BOTLIB_AI_REACHABILITY_AREA:
+ return botlib_export->ai.BotReachabilityArea( VMA(1), args[2] );
+ case BOTLIB_AI_MOVEMENT_VIEW_TARGET:
+ return botlib_export->ai.BotMovementViewTarget( args[1], VMA(2), args[3], VMF(4), VMA(5) );
+ case BOTLIB_AI_PREDICT_VISIBLE_POSITION:
+ return botlib_export->ai.BotPredictVisiblePosition( VMA(1), args[2], VMA(3), args[4], VMA(5) );
+ case BOTLIB_AI_ALLOC_MOVE_STATE:
+ return botlib_export->ai.BotAllocMoveState();
+ case BOTLIB_AI_FREE_MOVE_STATE:
+ botlib_export->ai.BotFreeMoveState( args[1] );
+ return 0;
+ case BOTLIB_AI_INIT_MOVE_STATE:
+ botlib_export->ai.BotInitMoveState( args[1], VMA(2) );
+ return 0;
+
+ case BOTLIB_AI_CHOOSE_BEST_FIGHT_WEAPON:
+ return botlib_export->ai.BotChooseBestFightWeapon( args[1], VMA(2) );
+ case BOTLIB_AI_GET_WEAPON_INFO:
+ botlib_export->ai.BotGetWeaponInfo( args[1], args[2], VMA(3) );
+ return 0;
+ case BOTLIB_AI_LOAD_WEAPON_WEIGHTS:
+ return botlib_export->ai.BotLoadWeaponWeights( args[1], VMA(2) );
+ case BOTLIB_AI_ALLOC_WEAPON_STATE:
+ return botlib_export->ai.BotAllocWeaponState();
+ case BOTLIB_AI_FREE_WEAPON_STATE:
+ botlib_export->ai.BotFreeWeaponState( args[1] );
+ return 0;
+ case BOTLIB_AI_RESET_WEAPON_STATE:
+ botlib_export->ai.BotResetWeaponState( args[1] );
+ return 0;
+
+ case BOTLIB_AI_GENETIC_PARENTS_AND_CHILD_SELECTION:
+ return botlib_export->ai.GeneticParentsAndChildSelection(args[1], VMA(2), VMA(3), VMA(4), VMA(5));
+
+ case TRAP_MEMSET:
+ Com_Memset( VMA(1), args[2], args[3] );
+ return 0;
+
+ case TRAP_MEMCPY:
+ Com_Memcpy( VMA(1), VMA(2), args[3] );
+ return 0;
+
+ case TRAP_STRNCPY:
+ strncpy( VMA(1), VMA(2), args[3] );
+ return args[1];
+
+ case TRAP_SIN:
+ return FloatAsInt( sin( VMF(1) ) );
+
+ case TRAP_COS:
+ return FloatAsInt( cos( VMF(1) ) );
+
+ case TRAP_ATAN2:
+ return FloatAsInt( atan2( VMF(1), VMF(2) ) );
+
+ case TRAP_SQRT:
+ return FloatAsInt( sqrt( VMF(1) ) );
+
+ case TRAP_MATRIXMULTIPLY:
+ MatrixMultiply( VMA(1), VMA(2), VMA(3) );
+ return 0;
+
+ case TRAP_ANGLEVECTORS:
+ AngleVectors( VMA(1), VMA(2), VMA(3), VMA(4) );
+ return 0;
+
+ case TRAP_PERPENDICULARVECTOR:
+ PerpendicularVector( VMA(1), VMA(2) );
+ return 0;
+
+ case TRAP_FLOOR:
+ return FloatAsInt( floor( VMF(1) ) );
+
+ case TRAP_CEIL:
+ return FloatAsInt( ceil( VMF(1) ) );
+
+
+ default:
+ Com_Error( ERR_DROP, "Bad game system trap: %ld", (long int) args[0] );
+ }
+ return -1;
+}
+
+/*
+===============
+SV_ShutdownGameProgs
+
+Called every time a map changes
+===============
+*/
+void SV_ShutdownGameProgs( void ) {
+ if ( !gvm ) {
+ return;
+ }
+ VM_Call( gvm, GAME_SHUTDOWN, qfalse );
+ VM_Free( gvm );
+ gvm = NULL;
+}
+
+/*
+==================
+SV_InitGameVM
+
+Called for both a full init and a restart
+==================
+*/
+static void SV_InitGameVM( qboolean restart ) {
+ int i;
+
+ // start the entity parsing at the beginning
+ sv.entityParsePoint = CM_EntityString();
+
+ // clear all gentity pointers that might still be set from
+ // a previous level
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=522
+ // now done before GAME_INIT call
+ for ( i = 0 ; i < sv_maxclients->integer ; i++ ) {
+ svs.clients[i].gentity = NULL;
+ }
+
+ // use the current msec count for a random seed
+ // init for this gamestate
+ VM_Call (gvm, GAME_INIT, sv.time, Com_Milliseconds(), restart);
+}
+
+
+
+/*
+===================
+SV_RestartGameProgs
+
+Called on a map_restart, but not on a normal map change
+===================
+*/
+void SV_RestartGameProgs( void ) {
+ if ( !gvm ) {
+ return;
+ }
+ VM_Call( gvm, GAME_SHUTDOWN, qtrue );
+
+ // do a restart instead of a free
+ gvm = VM_Restart( gvm );
+ if ( !gvm ) {
+ Com_Error( ERR_FATAL, "VM_Restart on game failed" );
+ }
+
+ SV_InitGameVM( qtrue );
+}
+
+
+/*
+===============
+SV_InitGameProgs
+
+Called on a normal map change, not on a map_restart
+===============
+*/
+void SV_InitGameProgs( void ) {
+ cvar_t *var;
+ //FIXME these are temp while I make bots run in vm
+ extern int bot_enable;
+
+ var = Cvar_Get( "bot_enable", "1", CVAR_LATCH );
+ if ( var ) {
+ bot_enable = var->integer;
+ }
+ else {
+ bot_enable = 0;
+ }
+
+ // load the dll or bytecode
+ gvm = VM_Create( "qagame", SV_GameSystemCalls, Cvar_VariableValue( "vm_game" ) );
+ if ( !gvm ) {
+ Com_Error( ERR_FATAL, "VM_Create on game failed" );
+ }
+
+ SV_InitGameVM( qfalse );
+}
+
+
+/*
+====================
+SV_GameCommand
+
+See if the current console command is claimed by the game
+====================
+*/
+qboolean SV_GameCommand( void ) {
+ if ( sv.state != SS_GAME ) {
+ return qfalse;
+ }
+
+ return VM_Call( gvm, GAME_CONSOLE_COMMAND );
+}
+
diff --git a/code/server/sv_init.c b/code/server/sv_init.c
new file mode 100644
index 0000000..bac7fa4
--- /dev/null
+++ b/code/server/sv_init.c
@@ -0,0 +1,771 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "server.h"
+
+
+/*
+===============
+SV_SendConfigstring
+
+Creates and sends the server command necessary to update the CS index for the
+given client
+===============
+*/
+static void SV_SendConfigstring(client_t *client, int index)
+{
+ int maxChunkSize = MAX_STRING_CHARS - 24;
+ int len;
+
+ len = strlen(sv.configstrings[index]);
+
+ 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, &sv.configstrings[index][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,
+ sv.configstrings[index] );
+ }
+}
+
+/*
+===============
+SV_UpdateConfigstrings
+
+Called when a client goes from CS_PRIMED to CS_ACTIVE. Updates all
+Configstring indexes that have changed while the client was in CS_PRIMED
+===============
+*/
+void SV_UpdateConfigstrings(client_t *client)
+{
+ int index;
+
+ for( index = 0; index <= MAX_CONFIGSTRINGS; index++ ) {
+ // if the CS hasn't changed since we went to CS_PRIMED, ignore
+ if(!client->csUpdated[index])
+ continue;
+
+ // do not always send server info to all clients
+ if ( index == CS_SERVERINFO && client->gentity &&
+ (client->gentity->r.svFlags & SVF_NOSERVERINFO) ) {
+ continue;
+ }
+ SV_SendConfigstring(client, index);
+ client->csUpdated[index] = qfalse;
+ }
+}
+
+/*
+===============
+SV_SetConfigstring
+
+===============
+*/
+void SV_SetConfigstring (int index, const char *val) {
+ int i;
+ 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_ACTIVE ) {
+ if ( client->state == CS_PRIMED )
+ client->csUpdated[ index ] = qtrue;
+ continue;
+ }
+ // do not always send server info to all clients
+ if ( index == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO) ) {
+ continue;
+ }
+
+ SV_SendConfigstring(client, index);
+ }
+ }
+}
+
+/*
+===============
+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
+================
+*/
+static 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
+
+===============
+*/
+static 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.
+===============
+*/
+static 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;
+
+ // Don't respect sv_killserver unless a server is actually running
+ if ( sv_killserver->integer ) {
+ Cvar_Set( "sv_killserver", "0" );
+ }
+
+ Cvar_Set( "sv_running", "1" );
+
+ // Join the ipv6 multicast group now that a map is running so clients can scan for us on the local network.
+ NET_JoinMulticast6();
+}
+
+
+/*
+==================
+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
+================
+*/
+static 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
+================
+*/
+static 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();
+
+#ifndef DEDICATED
+ // Restart renderer
+ CL_StartHunkUsers( qtrue );
+#endif
+
+ // 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) );
+
+ for (i=0 ; i<sv_maxclients->integer ; i++) {
+ // save when the server started for each client already connected
+ if (svs.clients[i].state >= CS_CONNECTED) {
+ svs.clients[i].oldServerTime = sv.time;
+ }
+ }
+
+ // 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
+ 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, sv.time);
+ SV_BotFrame (sv.time);
+ sv.time += 100;
+ 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, sv.time);
+ SV_BotFrame (sv.time);
+ sv.time += 100;
+ 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_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_minRate = Cvar_Get ("sv_minRate", "0", CVAR_ARCHIVE | CVAR_SERVERINFO );
+ 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 );
+ sv_pure = Cvar_Get ("sv_pure", "1", CVAR_SYSTEMINFO );
+#ifdef USE_VOIP
+ sv_voip = Cvar_Get ("sv_voip", "1", CVAR_SYSTEMINFO | CVAR_LATCH);
+ Cvar_CheckRange( sv_voip, 0, 1, qtrue );
+#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);
+ Cvar_Get ("sv_dlURL", "", CVAR_SERVERINFO | CVAR_ARCHIVE);
+ 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 );
+ sv_banFile = Cvar_Get("sv_banFile", "serverbans.dat", 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();
+
+ // Load saved bans
+ Cbuf_AddText("rehashbans\n");
+}
+
+
+/*
+==================
+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\n\"\n", message );
+ SV_SendServerCommand( cl, "disconnect \"%s\"", message );
+ }
+ // 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 (%s) -----\n", finalmsg );
+
+ NET_LeaveMulticast6();
+
+ 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
+ if( sv_killserver->integer != 2 )
+ CL_Disconnect( qfalse );
+}
+
diff --git a/code/server/sv_main.c b/code/server/sv_main.c
new file mode 100644
index 0000000..30dad56
--- /dev/null
+++ b/code/server/sv_main.c
@@ -0,0 +1,1131 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "server.h"
+
+#ifdef USE_VOIP
+cvar_t *sv_voip;
+#endif
+
+serverStatic_t svs; // persistant server info
+server_t sv; // local server
+vm_t *gvm = NULL; // game virtual machine
+
+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_minRate;
+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;
+cvar_t *sv_banFile;
+
+serverBan_t serverBans[SERVER_MAXBANS];
+int serverBansCount = 0;
+
+/*
+=============================================================================
+
+EVENT MESSAGES
+
+=============================================================================
+*/
+
+/*
+===============
+SV_ExpandNewlines
+
+Converts newlines to "\n" so a line prints nicer
+===============
+*/
+static 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
+
+FIXME: This is ugly
+======================
+*/
+#if 0 // unused
+static 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;
+}
+#endif
+
+/*
+======================
+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;
+// }
+
+ // do not send commands until the gamestate has been sent
+ if( client->state < CS_PRIMED )
+ 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);
+
+ // Fix to http://aluigi.altervista.org/adv/q3msgboom-adv.txt
+ // The actual cause of the bug is probably further downstream
+ // and should maybe be addressed later, but this certainly
+ // fixes the problem for now
+ if ( strlen ((char *)message) > 1022 ) {
+ return;
+ }
+
+ 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++) {
+ 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][2]; // [2] for v4 and v6 address for the same address string.
+ int i;
+ int res;
+ int netenabled;
+
+ netenabled = Cvar_VariableIntegerValue("net_enabled");
+
+ // "dedicated 1" is for lan play, "dedicated 2" is for inet public play
+ if (!com_dedicated || com_dedicated->integer != 2 || !(netenabled & (NET_ENABLEV4 | NET_ENABLEV6)))
+ 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 || (adr[i][0].type == NA_BAD && adr[i][1].type == NA_BAD))
+ {
+ sv_master[i]->modified = qfalse;
+
+ if(netenabled & NET_ENABLEV4)
+ {
+ Com_Printf("Resolving %s (IPv4)\n", sv_master[i]->string);
+ res = NET_StringToAdr(sv_master[i]->string, &adr[i][0], NA_IP);
+
+ if(res == 2)
+ {
+ // if no port was specified, use the default master port
+ adr[i][0].port = BigShort(PORT_MASTER);
+ }
+
+ if(res)
+ Com_Printf( "%s resolved to %s\n", sv_master[i]->string, NET_AdrToStringwPort(adr[i][0]));
+ else
+ Com_Printf( "%s has no IPv4 address.\n", sv_master[i]->string);
+ }
+
+ if(netenabled & NET_ENABLEV6)
+ {
+ Com_Printf("Resolving %s (IPv6)\n", sv_master[i]->string);
+ res = NET_StringToAdr(sv_master[i]->string, &adr[i][1], NA_IP6);
+
+ if(res == 2)
+ {
+ // if no port was specified, use the default master port
+ adr[i][1].port = BigShort(PORT_MASTER);
+ }
+
+ if(res)
+ Com_Printf( "%s resolved to %s\n", sv_master[i]->string, NET_AdrToStringwPort(adr[i][1]));
+ else
+ Com_Printf( "%s has no IPv6 address.\n", sv_master[i]->string);
+ }
+
+ if(adr[i][0].type == NA_BAD && adr[i][1].type == NA_BAD)
+ {
+ // 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;
+ }
+ }
+
+
+ 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
+
+ if(adr[i][0].type != NA_BAD)
+ NET_OutOfBandPrint( NS_SERVER, adr[i][0], "heartbeat %s\n", HEARTBEAT_GAME );
+ if(adr[i][1].type != NA_BAD)
+ NET_OutOfBandPrint( NS_SERVER, adr[i][1], "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
+
+==============================================================================
+*/
+
+typedef struct leakyBucket_s leakyBucket_t;
+struct leakyBucket_s {
+ netadrtype_t type;
+
+ union {
+ byte _4[4];
+ byte _6[16];
+ } ipv;
+
+ int lastTime;
+ signed char burst;
+
+ long hash;
+
+ leakyBucket_t *prev, *next;
+};
+
+// This is deliberately quite large to make it more of an effort to DoS
+#define MAX_BUCKETS 16384
+#define MAX_HASHES 1024
+
+static leakyBucket_t buckets[ MAX_BUCKETS ];
+static leakyBucket_t *bucketHashes[ MAX_HASHES ];
+
+/*
+================
+SVC_HashForAddress
+================
+*/
+static long SVC_HashForAddress( netadr_t address ) {
+ byte *ip = NULL;
+ size_t size = 0;
+ int i;
+ long hash = 0;
+
+ switch ( address.type ) {
+ case NA_IP: ip = address.ip; size = 4; break;
+ case NA_IP6: ip = address.ip6; size = 16; break;
+ default: break;
+ }
+
+ for ( i = 0; i < size; i++ ) {
+ hash += (long)( ip[ i ] ) * ( i + 119 );
+ }
+
+ hash = ( hash ^ ( hash >> 10 ) ^ ( hash >> 20 ) );
+ hash &= ( MAX_HASHES - 1 );
+
+ return hash;
+}
+
+/*
+================
+SVC_BucketForAddress
+
+Find or allocate a bucket for an address
+================
+*/
+static leakyBucket_t *SVC_BucketForAddress( netadr_t address, int burst, int period ) {
+ leakyBucket_t *bucket = NULL;
+ int i;
+ long hash = SVC_HashForAddress( address );
+ int now = Sys_Milliseconds();
+
+ for ( bucket = bucketHashes[ hash ]; bucket; bucket = bucket->next ) {
+ switch ( bucket->type ) {
+ case NA_IP:
+ if ( memcmp( bucket->ipv._4, address.ip, 4 ) == 0 ) {
+ return bucket;
+ }
+ break;
+
+ case NA_IP6:
+ if ( memcmp( bucket->ipv._6, address.ip6, 16 ) == 0 ) {
+ return bucket;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ for ( i = 0; i < MAX_BUCKETS; i++ ) {
+ int interval;
+
+ bucket = &buckets[ i ];
+ interval = now - bucket->lastTime;
+
+ // Reclaim expired buckets
+ if ( bucket->lastTime > 0 && interval > ( burst * period ) ) {
+ if ( bucket->prev != NULL ) {
+ bucket->prev->next = bucket->next;
+ } else {
+ bucketHashes[ bucket->hash ] = bucket->next;
+ }
+
+ if ( bucket->next != NULL ) {
+ bucket->next->prev = bucket->prev;
+ }
+
+ Com_Memset( bucket, 0, sizeof( leakyBucket_t ) );
+ }
+
+ if ( bucket->type == NA_BAD ) {
+ bucket->type = address.type;
+ switch ( address.type ) {
+ case NA_IP: Com_Memcpy( bucket->ipv._4, address.ip, 4 ); break;
+ case NA_IP6: Com_Memcpy( bucket->ipv._6, address.ip6, 16 ); break;
+ default: break;
+ }
+
+ bucket->lastTime = now;
+ bucket->burst = 0;
+ bucket->hash = hash;
+
+ // Add to the head of the relevant hash chain
+ bucket->next = bucketHashes[ hash ];
+ if ( bucketHashes[ hash ] != NULL ) {
+ bucketHashes[ hash ]->prev = bucket;
+ }
+
+ bucket->prev = NULL;
+ bucketHashes[ hash ] = bucket;
+
+ return bucket;
+ }
+ }
+
+ // Couldn't allocate a bucket for this address
+ return NULL;
+}
+
+/*
+================
+SVC_RateLimit
+================
+*/
+static qboolean SVC_RateLimit( leakyBucket_t *bucket, int burst, int period ) {
+ if ( bucket != NULL ) {
+ int now = Sys_Milliseconds();
+ int interval = now - bucket->lastTime;
+ int expired = interval / period;
+ int expiredRemainder = interval % period;
+
+ if ( expired > bucket->burst ) {
+ bucket->burst = 0;
+ bucket->lastTime = now;
+ } else {
+ bucket->burst -= expired;
+ bucket->lastTime = now - expiredRemainder;
+ }
+
+ if ( bucket->burst < burst ) {
+ bucket->burst++;
+
+ return qfalse;
+ }
+ }
+
+ return qtrue;
+}
+
+/*
+================
+SVC_RateLimitAddress
+
+Rate limit for a particular address
+================
+*/
+static qboolean SVC_RateLimitAddress( netadr_t from, int burst, int period ) {
+ leakyBucket_t *bucket = SVC_BucketForAddress( from, burst, period );
+
+ return SVC_RateLimit( bucket, burst, period );
+}
+
+/*
+================
+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.
+================
+*/
+static 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];
+ static leakyBucket_t bucket;
+
+ // ignore if we are in single player
+ if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER ) {
+ return;
+ }
+
+ // Prevent using getstatus as an amplifier
+ if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
+ Com_DPrintf( "SVC_Status: rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString( from ) );
+ return;
+ }
+
+ // Allow getstatus to be DoSed relatively easily, but prevent
+ // excess outbound bandwidth usage when being flooded inbound
+ if ( SVC_RateLimit( &bucket, 10, 100 ) ) {
+ Com_DPrintf( "SVC_Status: rate limit exceeded, dropping request\n" );
+ 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) );
+
+ 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;
+ }
+
+ /*
+ * Check whether Cmd_Argv(1) has a sane length. This was not done in the original Quake3 version which led
+ * to the Infostring bug discovered by Luigi Auriemma. See http://aluigi.altervista.org/ for the advisory.
+ */
+
+ // A maximum challenge length of 128 should be more than plenty.
+ if(strlen(Cmd_Argv(1)) > 128)
+ 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 ) );
+
+#ifdef USE_VOIP
+ if (sv_voip->integer) {
+ Info_SetValueForKey( infostring, "voip", va("%i", sv_voip->integer ) );
+ }
+#endif
+
+ 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
+
+================
+*/
+static 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
+===============
+*/
+static void SVC_RemoteCommand( netadr_t from, msg_t *msg ) {
+ qboolean valid;
+ 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];
+ char *cmd_aux;
+
+ // Prevent using rcon as an amplifier and make dictionary attacks impractical
+ if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
+ Com_DPrintf( "SVC_Status: rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString( from ) );
+ return;
+ }
+
+ if ( !strlen( sv_rconPassword->string ) ||
+ strcmp (Cmd_Argv(1), sv_rconPassword->string) ) {
+ static leakyBucket_t bucket;
+
+ // Make DoS via rcon impractical
+ if ( SVC_RateLimit( &bucket, 10, 1000 ) ) {
+ Com_DPrintf( "SVC_Status: rate limit exceeded, dropping request\n" );
+ return;
+ }
+
+ valid = qfalse;
+ Com_Printf ("Bad rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) );
+ } else {
+ valid = qtrue;
+ Com_Printf ("Rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(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.
+=================
+*/
+static 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", (char *) &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 );
+#ifndef STANDALONE
+ } else if (!Q_stricmp(c, "ipAuthorize")) {
+ SV_AuthorizeIpPacket( from );
+#endif
+ } 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_PacketEvent
+=================
+*/
+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
+===================
+*/
+static 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
+==================
+*/
+static 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
+==================
+*/
+static 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");
+ Cvar_Set( "sv_killserver", "0" );
+ return;
+ }
+
+ if (!com_sv_running->integer)
+ {
+ // Running as a server, but no map loaded
+#ifdef DEDICATED
+ // Block until something interesting happens
+ Sys_Sleep(-1);
+#endif
+
+ 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 * com_timescale->value;
+ // don't let it scale below 1ms
+ if(frameMsec < 1)
+ {
+ Cvar_Set("timescale", va("%f", sv_fps->integer / 1000.0f));
+ frameMsec = 1;
+ }
+
+ sv.timeResidual += msec;
+
+ if (!com_dedicated->integer) SV_BotFrame (sv.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( va( "map %s\n", Cvar_VariableString( "mapname" ) ) );
+ 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( va( "map %s\n", Cvar_VariableString( "mapname" ) ) );
+ return;
+ }
+
+ if( sv.restartTime && sv.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 (sv.time);
+
+ // run the game simulation in chunks
+ while ( sv.timeResidual >= frameMsec ) {
+ sv.timeResidual -= frameMsec;
+ svs.time += frameMsec;
+ sv.time += frameMsec;
+
+ // let everything in the world think and move
+ VM_Call (gvm, GAME_RUN_FRAME, sv.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();
+}
+
+//============================================================================
+
diff --git a/code/server/sv_net_chan.c b/code/server/sv_net_chan.c
new file mode 100644
index 0000000..5b2f500
--- /dev/null
+++ b/code/server/sv_net_chan.c
@@ -0,0 +1,209 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "../qcommon/q_shared.h"
+#include "../qcommon/qcommon.h"
+#include "server.h"
+
+/*
+==============
+SV_Netchan_Encode
+
+ // first four bytes of the data are always:
+ long reliableAcknowledge;
+
+==============
+*/
+static void SV_Netchan_Encode( client_t *client, msg_t *msg ) {
+ long i, index;
+ byte key, *string;
+ int srdc, sbit;
+ qboolean soob;
+
+ if ( msg->cursize < SV_ENCODE_START ) {
+ return;
+ }
+
+ srdc = msg->readcount;
+ sbit = msg->bit;
+ soob = msg->oob;
+
+ msg->bit = 0;
+ msg->readcount = 0;
+ msg->oob = qfalse;
+
+ /* reliableAcknowledge = */ MSG_ReadLong(msg);
+
+ msg->oob = soob;
+ msg->bit = sbit;
+ msg->readcount = srdc;
+
+ string = (byte *)client->lastClientCommandString;
+ index = 0;
+ // xor the client challenge with the netchan sequence number
+ key = client->challenge ^ client->netchan.outgoingSequence;
+ for (i = SV_ENCODE_START; i < msg->cursize; i++) {
+ // modify the key with the last received and with this message acknowledged client command
+ if (!string[index])
+ index = 0;
+ if (string[index] > 127 || string[index] == '%') {
+ key ^= '.' << (i & 1);
+ }
+ else {
+ key ^= string[index] << (i & 1);
+ }
+ index++;
+ // encode the data with this key
+ *(msg->data + i) = *(msg->data + i) ^ key;
+ }
+}
+
+/*
+==============
+SV_Netchan_Decode
+
+ // first 12 bytes of the data are always:
+ long serverId;
+ long messageAcknowledge;
+ long reliableAcknowledge;
+
+==============
+*/
+static void SV_Netchan_Decode( client_t *client, msg_t *msg ) {
+ int serverId, messageAcknowledge, reliableAcknowledge;
+ int i, index, srdc, sbit;
+ qboolean soob;
+ byte key, *string;
+
+ srdc = msg->readcount;
+ sbit = msg->bit;
+ soob = msg->oob;
+
+ msg->oob = qfalse;
+
+ serverId = MSG_ReadLong(msg);
+ messageAcknowledge = MSG_ReadLong(msg);
+ reliableAcknowledge = MSG_ReadLong(msg);
+
+ msg->oob = soob;
+ msg->bit = sbit;
+ msg->readcount = srdc;
+
+ string = (byte *)client->reliableCommands[ reliableAcknowledge & (MAX_RELIABLE_COMMANDS-1) ];
+ index = 0;
+ //
+ key = client->challenge ^ serverId ^ messageAcknowledge;
+ for (i = msg->readcount + SV_DECODE_START; i < msg->cursize; i++) {
+ // modify the key with the last sent and acknowledged server command
+ if (!string[index])
+ index = 0;
+ if (string[index] > 127 || string[index] == '%') {
+ key ^= '.' << (i & 1);
+ }
+ else {
+ key ^= string[index] << (i & 1);
+ }
+ index++;
+ // decode the data with this key
+ *(msg->data + i) = *(msg->data + i) ^ key;
+ }
+}
+
+/*
+=================
+SV_Netchan_TransmitNextFragment
+=================
+*/
+void SV_Netchan_TransmitNextFragment( client_t *client ) {
+ Netchan_TransmitNextFragment( &client->netchan );
+ if (!client->netchan.unsentFragments)
+ {
+ // make sure the netchan queue has been properly initialized (you never know)
+ if ((!client->netchan_end_queue) && (client->state >= CS_CONNECTED)) {
+ Com_Error(ERR_DROP, "netchan queue is not properly initialized in SV_Netchan_TransmitNextFragment\n");
+ }
+ // the last fragment was transmitted, check wether we have queued messages
+ if (client->netchan_start_queue) {
+ netchan_buffer_t *netbuf;
+ Com_DPrintf("#462 Netchan_TransmitNextFragment: popping a queued message for transmit\n");
+ netbuf = client->netchan_start_queue;
+ SV_Netchan_Encode( client, &netbuf->msg );
+ Netchan_Transmit( &client->netchan, netbuf->msg.cursize, netbuf->msg.data );
+ // pop from queue
+ client->netchan_start_queue = netbuf->next;
+ if (!client->netchan_start_queue) {
+ Com_DPrintf("#462 Netchan_TransmitNextFragment: emptied queue\n");
+ client->netchan_end_queue = &client->netchan_start_queue;
+ }
+ else
+ Com_DPrintf("#462 Netchan_TransmitNextFragment: remaining queued message\n");
+ Z_Free(netbuf);
+ }
+ }
+}
+
+
+/*
+===============
+SV_Netchan_Transmit
+TTimo
+https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=462
+if there are some unsent fragments (which may happen if the snapshots
+and the gamestate are fragmenting, and collide on send for instance)
+then buffer them and make sure they get sent in correct order
+================
+*/
+
+void SV_Netchan_Transmit( client_t *client, msg_t *msg) { //int length, const byte *data ) {
+ MSG_WriteByte( msg, svc_EOF );
+ if (client->netchan.unsentFragments) {
+ netchan_buffer_t *netbuf;
+ Com_DPrintf("#462 SV_Netchan_Transmit: unsent fragments, stacked\n");
+ netbuf = (netchan_buffer_t *)Z_Malloc(sizeof(netchan_buffer_t));
+ // store the msg, we can't store it encoded, as the encoding depends on stuff we still have to finish sending
+ MSG_Copy(&netbuf->msg, netbuf->msgBuffer, sizeof( netbuf->msgBuffer ), msg);
+ netbuf->next = NULL;
+ // insert it in the queue, the message will be encoded and sent later
+ *client->netchan_end_queue = netbuf;
+ client->netchan_end_queue = &(*client->netchan_end_queue)->next;
+ // emit the next fragment of the current message for now
+ Netchan_TransmitNextFragment(&client->netchan);
+ } else {
+ SV_Netchan_Encode( client, msg );
+ Netchan_Transmit( &client->netchan, msg->cursize, msg->data );
+ }
+}
+
+/*
+=================
+Netchan_SV_Process
+=================
+*/
+qboolean SV_Netchan_Process( client_t *client, msg_t *msg ) {
+ int ret;
+ ret = Netchan_Process( &client->netchan, msg );
+ if (!ret)
+ return qfalse;
+ SV_Netchan_Decode( client, msg );
+ return qtrue;
+}
+
diff --git a/code/server/sv_rankings.c b/code/server/sv_rankings.c
new file mode 100644
index 0000000..90a90aa
--- /dev/null
+++ b/code/server/sv_rankings.c
@@ -0,0 +1,1537 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+// sv_rankings.c -- global rankings interface
+
+#include "server.h"
+#include "..\rankings\1.0\gr\grapi.h"
+#include "..\rankings\1.0\gr\grlog.h"
+
+typedef struct
+{
+ GR_CONTEXT context;
+ uint64_t game_id;
+ uint64_t match;
+ uint64_t player_id;
+ GR_PLAYER_TOKEN token;
+ grank_status_t grank_status;
+ grank_status_t final_status; // status to set after cleanup
+ uint32_t grank; // global rank
+ char name[32];
+} ranked_player_t;
+
+static int s_rankings_contexts = 0;
+static qboolean s_rankings_active = qfalse;
+static GR_CONTEXT s_server_context = 0;
+static uint64_t s_server_match = 0;
+static char* s_rankings_game_key = NULL;
+static uint64_t s_rankings_game_id = 0;
+static ranked_player_t* s_ranked_players = NULL;
+static qboolean s_server_quitting = qfalse;
+static const char s_ascii_encoding[] =
+ "0123456789abcdef"
+ "ghijklmnopqrstuv"
+ "wxyzABCDEFGHIJKL"
+ "MNOPQRSTUVWXYZ[]";
+
+// private functions
+static void SV_RankNewGameCBF( GR_NEWGAME* gr_newgame, void* cbf_arg );
+static void SV_RankUserCBF( GR_LOGIN* gr_login, void* cbf_arg );
+static void SV_RankJoinGameCBF( GR_JOINGAME* gr_joingame, void* cbf_arg );
+static void SV_RankSendReportsCBF( GR_STATUS* gr_status, void* cbf_arg );
+static void SV_RankCleanupCBF( GR_STATUS* gr_status, void* cbf_arg );
+static void SV_RankCloseContext( ranked_player_t* ranked_player );
+static int SV_RankAsciiEncode( char* dest, const unsigned char* src,
+ int src_len );
+static int SV_RankAsciiDecode( unsigned char* dest, const char* src,
+ int src_len );
+static void SV_RankEncodeGameID( uint64_t game_id, char* result,
+ int len );
+static uint64_t SV_RankDecodePlayerID( const char* string );
+static void SV_RankDecodePlayerKey( const char* string, GR_PLAYER_TOKEN key );
+static char* SV_RankStatusString( GR_STATUS status );
+static void SV_RankError( const char* fmt, ... );
+static char SV_RankGameKey[64];
+
+/*
+================
+SV_RankBegin
+================
+*/
+void SV_RankBegin( char *gamekey )
+{
+ GR_INIT init;
+ GR_STATUS status;
+
+ assert( s_rankings_contexts == 0 );
+ assert( !s_rankings_active );
+ assert( s_ranked_players == NULL );
+
+ if( sv_enableRankings->integer == 0 || Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER )
+ {
+ s_rankings_active = qfalse;
+ if( sv_rankingsActive->integer == 1 )
+ {
+ Cvar_Set( "sv_rankingsActive", "0" );
+ }
+ return;
+ }
+
+ // only allow official game key on pure servers
+ if( strcmp(gamekey, GR_GAMEKEY) == 0 )
+ {
+/*
+ if( Cvar_VariableValue("sv_pure") != 1 )
+ {
+ Cvar_Set( "sv_enableRankings", "0" );
+ return;
+ }
+*/
+
+ // substitute game-specific game key
+ switch( (int)Cvar_VariableValue("g_gametype") )
+ {
+ case GT_FFA:
+ gamekey = "Q3 Free For All";
+ break;
+ case GT_TOURNAMENT:
+ gamekey = "Q3 Tournament";
+ break;
+ case GT_TEAM:
+ gamekey = "Q3 Team Deathmatch";
+ break;
+ case GT_CTF:
+ gamekey = "Q3 Capture the Flag";
+ break;
+ case GT_1FCTF:
+ gamekey = "Q3 One Flag CTF";
+ break;
+ case GT_OBELISK:
+ gamekey = "Q3 Overload";
+ break;
+ case GT_HARVESTER:
+ gamekey = "Q3 Harvester";
+ break;
+ default:
+ break;
+ }
+ }
+ s_rankings_game_key = gamekey;
+
+ // initialize rankings
+ GRankLogLevel( GRLOG_OFF );
+ memset(SV_RankGameKey,0,sizeof(SV_RankGameKey));
+ strncpy(SV_RankGameKey,gamekey,sizeof(SV_RankGameKey)-1);
+ init = GRankInit( 1, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
+ s_server_context = init.context;
+ s_rankings_contexts++;
+ Com_DPrintf( "SV_RankBegin(); GR_GAMEKEY is %s\n", gamekey );
+ Com_DPrintf( "SV_RankBegin(); s_rankings_contexts=%d\n",s_rankings_contexts );
+ Com_DPrintf( "SV_RankBegin(); s_server_context=%d\n",init.context );
+
+ // new game
+ if(!strlen(Cvar_VariableString( "sv_leagueName" )))
+ {
+ status = GRankNewGameAsync
+ (
+ s_server_context,
+ SV_RankNewGameCBF,
+ NULL,
+ GR_OPT_LEAGUENAME,
+ (void*)(Cvar_VariableString( "sv_leagueName" )),
+ GR_OPT_END
+ );
+ }
+ else
+ {
+ status = GRankNewGameAsync
+ (
+ s_server_context,
+ SV_RankNewGameCBF,
+ NULL,
+ GR_OPT_END
+ );
+ }
+
+ if( status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankBegin: Expected GR_STATUS_PENDING, got %s",
+ SV_RankStatusString( status ) );
+ return;
+ }
+
+ // logging
+ if( com_developer->value )
+ {
+ GRankLogLevel( GRLOG_TRACE );
+ }
+
+ // allocate rankings info for each player
+ s_ranked_players = Z_Malloc( sv_maxclients->value *
+ sizeof(ranked_player_t) );
+ memset( (void*)s_ranked_players, 0 ,sv_maxclients->value
+ * sizeof(ranked_player_t));
+}
+
+/*
+================
+SV_RankEnd
+================
+*/
+void SV_RankEnd( void )
+{
+ GR_STATUS status;
+ int i;
+
+ Com_DPrintf( "SV_RankEnd();\n" );
+
+ if( !s_rankings_active )
+ {
+ // cleanup after error during game
+ if( s_ranked_players != NULL )
+ {
+ for( i = 0; i < sv_maxclients->value; i++ )
+ {
+ if( s_ranked_players[i].context != 0 )
+ {
+ SV_RankCloseContext( &(s_ranked_players[i]) );
+ }
+ }
+ }
+ if( s_server_context != 0 )
+ {
+ SV_RankCloseContext( NULL );
+ }
+
+ return;
+ }
+
+ for( i = 0; i < sv_maxclients->value; i++ )
+ {
+ if( s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE )
+ {
+ SV_RankUserLogout( i );
+ Com_DPrintf( "SV_RankEnd: SV_RankUserLogout %d\n",i );
+ }
+ }
+
+ assert( s_server_context != 0 );
+
+ // send match reports, proceed to SV_RankSendReportsCBF
+ status = GRankSendReportsAsync
+ (
+ s_server_context,
+ 0,
+ SV_RankSendReportsCBF,
+ NULL,
+ GR_OPT_END
+ );
+
+ if( status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankEnd: Expected GR_STATUS_PENDING, got %s",
+ SV_RankStatusString( status ) );
+ }
+
+ s_rankings_active = qfalse;
+ Cvar_Set( "sv_rankingsActive", "0" );
+}
+
+/*
+================
+SV_RankPoll
+================
+*/
+void SV_RankPoll( void )
+{
+ GRankPoll();
+}
+
+/*
+================
+SV_RankCheckInit
+================
+*/
+qboolean SV_RankCheckInit( void )
+{
+ return (s_rankings_contexts > 0);
+}
+
+/*
+================
+SV_RankActive
+================
+*/
+qboolean SV_RankActive( void )
+{
+ return s_rankings_active;
+}
+
+/*
+=================
+SV_RankUserStatus
+=================
+*/
+grank_status_t SV_RankUserStatus( int index )
+{
+ if( !s_rankings_active )
+ {
+ return GR_STATUS_ERROR;
+ }
+
+ assert( s_ranked_players != NULL );
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+
+ return s_ranked_players[index].grank_status;
+}
+
+/*
+================
+SV_RankUserGRank
+================
+*/
+int SV_RankUserGrank( int index )
+{
+ if( !s_rankings_active )
+ {
+ return 0;
+ }
+
+ assert( s_ranked_players != NULL );
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+
+ return s_ranked_players[index].grank;
+}
+
+/*
+================
+SV_RankUserReset
+================
+*/
+void SV_RankUserReset( int index )
+{
+ if( !s_rankings_active )
+ {
+ return;
+ }
+
+ assert( s_ranked_players != NULL );
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+
+ switch( s_ranked_players[index].grank_status )
+ {
+ case QGR_STATUS_SPECTATOR:
+ case QGR_STATUS_NO_USER:
+ case QGR_STATUS_BAD_PASSWORD:
+ case QGR_STATUS_USER_EXISTS:
+ case QGR_STATUS_NO_MEMBERSHIP:
+ case QGR_STATUS_TIMEOUT:
+ case QGR_STATUS_ERROR:
+ s_ranked_players[index].grank_status = QGR_STATUS_NEW;
+ break;
+ default:
+ break;
+ }
+}
+
+/*
+================
+SV_RankUserSpectate
+================
+*/
+void SV_RankUserSpectate( int index )
+{
+ if( !s_rankings_active )
+ {
+ return;
+ }
+
+ assert( s_ranked_players != NULL );
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+
+ // GRANK_FIXME - check current status?
+ s_ranked_players[index].grank_status = QGR_STATUS_SPECTATOR;
+}
+
+/*
+================
+SV_RankUserCreate
+================
+*/
+void SV_RankUserCreate( int index, char* username, char* password,
+ char* email )
+{
+ GR_INIT init;
+ GR_STATUS status;
+
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+ assert( username != NULL );
+ assert( password != NULL );
+ assert( email != NULL );
+ assert( s_ranked_players );
+ assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE );
+
+ Com_DPrintf( "SV_RankUserCreate( %d, %s, \"****\", %s );\n", index,
+ username, email );
+
+ if( !s_rankings_active )
+ {
+ Com_DPrintf( "SV_RankUserCreate: Not ready to create\n" );
+ s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
+ return;
+ }
+
+ if( s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE )
+ {
+ Com_DPrintf( "SV_RankUserCreate: Got Create from active player\n" );
+ return;
+ }
+
+ // get a separate context for the new user
+ init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
+ s_ranked_players[index].context = init.context;
+ s_rankings_contexts++;
+ Com_DPrintf( "SV_RankUserCreate(); s_rankings_contexts=%d\n",s_rankings_contexts );
+ Com_DPrintf( "SV_RankUserCreate(); s_ranked_players[%d].context=%d\n",index,init.context );
+
+ // attempt to create a new account, proceed to SV_RankUserCBF
+ status = GRankUserCreateAsync
+ (
+ s_ranked_players[index].context,
+ username,
+ password,
+ email,
+ SV_RankUserCBF,
+ (void*)&s_ranked_players[index],
+ GR_OPT_END
+ );
+
+ if( status == GR_STATUS_PENDING )
+ {
+ s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
+ s_ranked_players[index].final_status = QGR_STATUS_NEW;
+ }
+ else
+ {
+ SV_RankError( "SV_RankUserCreate: Expected GR_STATUS_PENDING, got %s",
+ SV_RankStatusString( status ) );
+ }
+}
+
+/*
+================
+SV_RankUserLogin
+================
+*/
+void SV_RankUserLogin( int index, char* username, char* password )
+{
+ GR_INIT init;
+ GR_STATUS status;
+
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+ assert( username != NULL );
+ assert( password != NULL );
+ assert( s_ranked_players );
+ assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE );
+
+ Com_DPrintf( "SV_RankUserLogin( %d, %s, \"****\" );\n", index, username );
+
+ if( !s_rankings_active )
+ {
+ Com_DPrintf( "SV_RankUserLogin: Not ready for login\n" );
+ s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
+ return;
+ }
+
+ if( s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE )
+ {
+ Com_DPrintf( "SV_RankUserLogin: Got Login from active player\n" );
+ return;
+ }
+
+ // get a separate context for the new user
+ init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
+ s_ranked_players[index].context = init.context;
+ s_rankings_contexts++;
+ Com_DPrintf( "SV_RankUserLogin(); s_rankings_contexts=%d\n",s_rankings_contexts );
+ Com_DPrintf( "SV_RankUserLogin(); s_ranked_players[%d].context=%d\n",index,init.context );
+
+ // login user, proceed to SV_RankUserCBF
+ status = GRankUserLoginAsync
+ (
+ s_ranked_players[index].context,
+ username,
+ password,
+ SV_RankUserCBF,
+ (void*)&s_ranked_players[index],
+ GR_OPT_END
+ );
+
+ if( status == GR_STATUS_PENDING )
+ {
+ s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
+ s_ranked_players[index].final_status = QGR_STATUS_NEW;
+ }
+ else
+ {
+ SV_RankError( "SV_RankUserLogin: Expected GR_STATUS_PENDING, got %s",
+ SV_RankStatusString( status ) );
+ }
+}
+
+/*
+===================
+SV_RankUserValidate
+===================
+*/
+qboolean SV_RankUserValidate( int index, const char* player_id, const char* key, int token_len, int rank, char* name )
+{
+ GR_INIT init;
+ GR_STATUS status;
+ qboolean rVal;
+ ranked_player_t* ranked_player;
+ int i;
+
+ assert( s_ranked_players );
+ assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE );
+
+ rVal = qfalse;
+
+ if( !s_rankings_active )
+ {
+ Com_DPrintf( "SV_RankUserValidate: Not ready to validate\n" );
+ s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
+ return rVal;
+ }
+
+ ranked_player = &(s_ranked_players[index]);
+
+ if ( (player_id != NULL) && (key != NULL))
+ {
+ // the real player_id and key is set when SV_RankJoinGameCBF
+ // is called we do this so that SV_RankUserValidate
+ // can be shared by both server side login and client side login
+
+ // for client side logined in players
+ // server is creating GR_OPT_PLAYERCONTEXT
+ init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
+ ranked_player->context = init.context;
+ s_rankings_contexts++;
+ Com_DPrintf( "SV_RankUserValidate(); s_rankings_contexts=%d\n",s_rankings_contexts );
+ Com_DPrintf( "SV_RankUserValidate(); s_ranked_players[%d].context=%d\n",index,init.context );
+
+ // uudecode player id and player token
+ ranked_player->player_id = SV_RankDecodePlayerID(player_id);
+ Com_DPrintf( "SV_RankUserValidate(); ranked_player->player_id =%u\n", (uint32_t)ranked_player->player_id );
+ SV_RankDecodePlayerKey(key, ranked_player->token);
+
+ // save name and check for duplicates
+ Q_strncpyz( ranked_player->name, name, sizeof(ranked_player->name) );
+ for( i = 0; i < sv_maxclients->value; i++ )
+ {
+ if( (i != index) && (s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE) &&
+ (strcmp( s_ranked_players[i].name, name ) == 0) )
+ {
+ Com_DPrintf( "SV_RankUserValidate: Duplicate login\n" );
+ ranked_player->grank_status = QGR_STATUS_NO_USER;
+ ranked_player->final_status = QGR_STATUS_NEW;
+ ranked_player->grank = 0;
+ return qfalse;
+ }
+ }
+
+ // then validate
+ status = GRankPlayerValidate(
+ s_server_context,
+ ranked_player->player_id,
+ ranked_player->token,
+ token_len,
+ GR_OPT_PLAYERCONTEXT,
+ ranked_player->context,
+ GR_OPT_END);
+ }
+ else
+ {
+ // make server side login (bots) happy
+ status = GR_STATUS_OK;
+ }
+
+ if (status == GR_STATUS_OK)
+ {
+ ranked_player->grank_status = QGR_STATUS_ACTIVE;
+ ranked_player->final_status = QGR_STATUS_NEW;
+ ranked_player->grank = rank;
+ rVal = qtrue;
+ }
+ else if (status == GR_STATUS_INVALIDUSER)
+ {
+ ranked_player->grank_status = QGR_STATUS_INVALIDUSER;
+ ranked_player->final_status = QGR_STATUS_NEW;
+ ranked_player->grank = 0;
+ rVal = qfalse;
+ }
+ else
+ {
+ SV_RankError( "SV_RankUserValidate: Unexpected status %s",
+ SV_RankStatusString( status ) );
+ s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
+ ranked_player->grank = 0;
+ }
+
+ return rVal;
+}
+
+/*
+================
+SV_RankUserLogout
+================
+*/
+void SV_RankUserLogout( int index )
+{
+ GR_STATUS status;
+ GR_STATUS cleanup_status;
+
+ if( !s_rankings_active )
+ {
+ return;
+ }
+
+ assert( index >= 0 );
+ assert( index < sv_maxclients->value );
+ assert( s_ranked_players );
+
+ if( s_ranked_players[index].context == 0 ) {
+ return;
+ }
+
+ Com_DPrintf( "SV_RankUserLogout( %d );\n", index );
+
+ // masqueraded player may not be active yet, if they fail validation,
+ // but still they have a context needs to be cleaned
+ // what matters is the s_ranked_players[index].context
+
+ // send reports, proceed to SV_RankSendReportsCBF
+ status = GRankSendReportsAsync
+ (
+ s_ranked_players[index].context,
+ 0,
+ SV_RankSendReportsCBF,
+ (void*)&s_ranked_players[index],
+ GR_OPT_END
+ );
+
+ if( status == GR_STATUS_PENDING )
+ {
+ s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
+ s_ranked_players[index].final_status = QGR_STATUS_NEW;
+ }
+ else
+ {
+ SV_RankError( "SV_RankUserLogout: Expected GR_STATUS_PENDING, got %s",
+ SV_RankStatusString( status ) );
+
+ cleanup_status = GRankCleanupAsync
+ (
+ s_ranked_players[index].context,
+ 0,
+ SV_RankCleanupCBF,
+ (void*)&s_ranked_players[index],
+ GR_OPT_END
+ );
+
+ if( cleanup_status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankUserLogout: Expected "
+ "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
+ SV_RankStatusString( cleanup_status ) );
+ SV_RankCloseContext( &(s_ranked_players[index]) );
+ }
+ }
+}
+
+/*
+================
+SV_RankReportInt
+================
+*/
+void SV_RankReportInt( int index1, int index2, int key, int value,
+ qboolean accum )
+{
+ GR_STATUS status;
+ GR_CONTEXT context;
+ uint64_t match;
+ uint64_t user1;
+ uint64_t user2;
+ int opt_accum;
+
+ if( !s_rankings_active )
+ {
+ return;
+ }
+
+ assert( index1 >= -1 );
+ assert( index1 < sv_maxclients->value );
+ assert( index2 >= -1 );
+ assert( index2 < sv_maxclients->value );
+ assert( s_ranked_players );
+
+// Com_DPrintf( "SV_RankReportInt( %d, %d, %d, %d, %d );\n", index1, index2,
+// key, value, accum );
+
+ // get context, match, and player_id for player index1
+ if( index1 == -1 )
+ {
+ context = s_server_context;
+ match = s_server_match;
+ user1 = 0;
+ }
+ else
+ {
+ if( s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE )
+ {
+ Com_DPrintf( "SV_RankReportInt: Expecting QGR_STATUS_ACTIVE"
+ " Got Unexpected status %d for player %d\n",
+ s_ranked_players[index1].grank_status, index1 );
+ return;
+ }
+
+ context = s_ranked_players[index1].context;
+ match = s_ranked_players[index1].match;
+ user1 = s_ranked_players[index1].player_id;
+ }
+
+ // get player_id for player index2
+ if( index2 == -1 )
+ {
+ user2 = 0;
+ }
+ else
+ {
+ if( s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE )
+ {
+ Com_DPrintf( "SV_RankReportInt: Expecting QGR_STATUS_ACTIVE"
+ " Got Unexpected status %d for player %d\n",
+ s_ranked_players[index2].grank_status, index2 );
+ return;
+ }
+
+ user2 = s_ranked_players[index2].player_id;
+ }
+
+ opt_accum = accum ? GR_OPT_ACCUM : GR_OPT_END;
+
+ status = GRankReportInt
+ (
+ context,
+ match,
+ user1,
+ user2,
+ key,
+ value,
+ opt_accum,
+ GR_OPT_END
+ );
+
+ if( status != GR_STATUS_OK )
+ {
+ SV_RankError( "SV_RankReportInt: Unexpected status %s",
+ SV_RankStatusString( status ) );
+ }
+
+ if( user2 != 0 )
+ {
+ context = s_ranked_players[index2].context;
+ match = s_ranked_players[index2].match;
+
+ status = GRankReportInt
+ (
+ context,
+ match,
+ user1,
+ user2,
+ key,
+ value,
+ opt_accum,
+ GR_OPT_END
+ );
+
+ if( status != GR_STATUS_OK )
+ {
+ SV_RankError( "SV_RankReportInt: Unexpected status %s",
+ SV_RankStatusString( status ) );
+ }
+ }
+}
+
+/*
+================
+SV_RankReportStr
+================
+*/
+void SV_RankReportStr( int index1, int index2, int key, char* value )
+{
+ GR_STATUS status;
+ GR_CONTEXT context;
+ uint64_t match;
+ uint64_t user1;
+ uint64_t user2;
+
+ if( !s_rankings_active )
+ {
+ return;
+ }
+
+ assert( index1 >= -1 );
+ assert( index1 < sv_maxclients->value );
+ assert( index2 >= -1 );
+ assert( index2 < sv_maxclients->value );
+ assert( s_ranked_players );
+
+// Com_DPrintf( "SV_RankReportStr( %d, %d, %d, \"%s\" );\n", index1, index2,
+// key, value );
+
+ // get context, match, and player_id for player index1
+ if( index1 == -1 )
+ {
+ context = s_server_context;
+ match = s_server_match;
+ user1 = 0;
+ }
+ else
+ {
+ if( s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE )
+ {
+ Com_DPrintf( "SV_RankReportStr: Unexpected status %d\n",
+ s_ranked_players[index1].grank_status );
+ return;
+ }
+
+ context = s_ranked_players[index1].context;
+ match = s_ranked_players[index1].match;
+ user1 = s_ranked_players[index1].player_id;
+ }
+
+ // get player_id for player index2
+ if( index2 == -1 )
+ {
+ user2 = 0;
+ }
+ else
+ {
+ if( s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE )
+ {
+ Com_DPrintf( "SV_RankReportStr: Unexpected status %d\n",
+ s_ranked_players[index2].grank_status );
+ return;
+ }
+
+ user2 = s_ranked_players[index2].player_id;
+ }
+
+ status = GRankReportStr
+ (
+ context,
+ match,
+ user1,
+ user2,
+ key,
+ value,
+ GR_OPT_END
+ );
+
+ if( status != GR_STATUS_OK )
+ {
+ SV_RankError( "SV_RankReportStr: Unexpected status %s",
+ SV_RankStatusString( status ) );
+ }
+
+ if( user2 != 0 )
+ {
+ context = s_ranked_players[index2].context;
+ match = s_ranked_players[index2].match;
+
+ status = GRankReportStr
+ (
+ context,
+ match,
+ user1,
+ user2,
+ key,
+ value,
+ GR_OPT_END
+ );
+
+ if( status != GR_STATUS_OK )
+ {
+ SV_RankError( "SV_RankReportInt: Unexpected status %s",
+ SV_RankStatusString( status ) );
+ }
+ }
+}
+
+/*
+================
+SV_RankQuit
+================
+*/
+void SV_RankQuit( void )
+{
+ int i;
+ int j = 0;
+ // yuck
+
+ while( s_rankings_contexts > 1 )
+ {
+ assert(s_ranked_players);
+ if( s_ranked_players != NULL )
+ {
+ for( i = 0; i < sv_maxclients->value; i++ )
+ {
+ // check for players that weren't yet active in SV_RankEnd
+ if( s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE )
+ {
+ SV_RankUserLogout( i );
+ Com_DPrintf( "SV_RankQuit: SV_RankUserLogout %d\n",i );
+ }
+ else
+ {
+ if( s_ranked_players[i].context )
+ {
+ GR_STATUS cleanup_status;
+ cleanup_status = GRankCleanupAsync
+ (
+ s_ranked_players[i].context,
+ 0,
+ SV_RankCleanupCBF,
+ (void*)&(s_ranked_players[i]),
+ GR_OPT_END
+ );
+
+ if( cleanup_status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankQuit: Expected "
+ "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
+ SV_RankStatusString( cleanup_status ) );
+ }
+ }
+ }
+ }
+ }
+ SV_RankPoll();
+
+ // should've finished by now
+ assert( (j++) < 68 );
+ }
+}
+
+/*
+==============================================================================
+
+Private Functions
+
+==============================================================================
+*/
+
+/*
+=================
+SV_RankNewGameCBF
+=================
+*/
+static void SV_RankNewGameCBF( GR_NEWGAME* gr_newgame, void* cbf_arg )
+{
+ GR_MATCH match;
+ int i;
+
+ assert( gr_newgame != NULL );
+ assert( cbf_arg == NULL );
+
+ Com_DPrintf( "SV_RankNewGameCBF( %08X, %08X );\n", gr_newgame, cbf_arg );
+
+ if( gr_newgame->status == GR_STATUS_OK )
+ {
+ char info[MAX_INFO_STRING];
+ char gameid[sizeof(s_ranked_players[i].game_id) * 4 / 3 + 2];
+
+ // save game id
+ s_rankings_game_id = gr_newgame->game_id;
+
+ // encode gameid
+ memset(gameid,0,sizeof(gameid));
+ SV_RankEncodeGameID(s_rankings_game_id,gameid,sizeof(gameid));
+
+ // set CS_GRANK rankingsGameID to pass to client
+ memset(info,0,sizeof(info));
+ Info_SetValueForKey( info, "rankingsGameKey", s_rankings_game_key );
+ Info_SetValueForKey( info, "rankingsGameID", gameid );
+ SV_SetConfigstring( CS_GRANK, info );
+
+ // initialize client status
+ for( i = 0; i < sv_maxclients->value; i++ )
+ s_ranked_players[i].grank_status = QGR_STATUS_NEW;
+
+ // start new match
+ match = GRankStartMatch( s_server_context );
+ s_server_match = match.match;
+
+ // ready to go
+ s_rankings_active = qtrue;
+ Cvar_Set( "sv_rankingsActive", "1" );
+
+ }
+ else if( gr_newgame->status == GR_STATUS_BADLEAGUE )
+ {
+ SV_RankError( "SV_RankNewGameCBF: Invalid League name\n" );
+ }
+ else
+ {
+ //GRank handle new game failure
+ // force SV_RankEnd() to run
+ //SV_RankEnd();
+ SV_RankError( "SV_RankNewGameCBF: Unexpected status %s",
+ SV_RankStatusString( gr_newgame->status ) );
+ }
+}
+
+/*
+================
+SV_RankUserCBF
+================
+*/
+static void SV_RankUserCBF( GR_LOGIN* gr_login, void* cbf_arg )
+{
+ ranked_player_t* ranked_player;
+ GR_STATUS join_status;
+ GR_STATUS cleanup_status;
+
+ assert( gr_login != NULL );
+ assert( cbf_arg != NULL );
+
+ Com_DPrintf( "SV_RankUserCBF( %08X, %08X );\n", gr_login, cbf_arg );
+
+ ranked_player = (ranked_player_t*)cbf_arg;
+ assert(ranked_player);
+ assert( ranked_player->context );
+
+ switch( gr_login->status )
+ {
+ case GR_STATUS_OK:
+ // attempt to join the game, proceed to SV_RankJoinGameCBF
+ join_status = GRankJoinGameAsync
+ (
+ ranked_player->context,
+ s_rankings_game_id,
+ SV_RankJoinGameCBF,
+ cbf_arg,
+ GR_OPT_END
+ );
+
+ if( join_status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankUserCBF: Expected GR_STATUS_PENDING "
+ "from GRankJoinGameAsync, got %s",
+ SV_RankStatusString( join_status ) );
+ }
+ break;
+ case GR_STATUS_NOUSER:
+ Com_DPrintf( "SV_RankUserCBF: Got status %s\n",
+ SV_RankStatusString( gr_login->status ) );
+ ranked_player->final_status = QGR_STATUS_NO_USER;
+ break;
+ case GR_STATUS_BADPASSWORD:
+ Com_DPrintf( "SV_RankUserCBF: Got status %s\n",
+ SV_RankStatusString( gr_login->status ) );
+ ranked_player->final_status = QGR_STATUS_BAD_PASSWORD;
+ break;
+ case GR_STATUS_TIMEOUT:
+ Com_DPrintf( "SV_RankUserCBF: Got status %s\n",
+ SV_RankStatusString( gr_login->status ) );
+ ranked_player->final_status = QGR_STATUS_TIMEOUT;
+ break;
+ default:
+ Com_DPrintf( "SV_RankUserCBF: Unexpected status %s\n",
+ SV_RankStatusString( gr_login->status ) );
+ ranked_player->final_status = QGR_STATUS_ERROR;
+ break;
+ }
+
+ if( ranked_player->final_status != QGR_STATUS_NEW )
+ {
+ // login or create failed, so clean up before the next attempt
+ cleanup_status = GRankCleanupAsync
+ (
+ ranked_player->context,
+ 0,
+ SV_RankCleanupCBF,
+ (void*)ranked_player,
+ GR_OPT_END
+ );
+
+ if( cleanup_status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankUserCBF: Expected GR_STATUS_PENDING "
+ "from GRankCleanupAsync, got %s",
+ SV_RankStatusString( cleanup_status ) );
+ SV_RankCloseContext( ranked_player );
+ }
+ }
+}
+
+/*
+================
+SV_RankJoinGameCBF
+================
+*/
+static void SV_RankJoinGameCBF( GR_JOINGAME* gr_joingame, void* cbf_arg )
+{
+ ranked_player_t* ranked_player;
+ GR_MATCH match;
+ GR_STATUS cleanup_status;
+
+ assert( gr_joingame != NULL );
+ assert( cbf_arg != NULL );
+
+ Com_DPrintf( "SV_RankJoinGameCBF( %08X, %08X );\n", gr_joingame, cbf_arg );
+
+ ranked_player = (ranked_player_t*)cbf_arg;
+
+ assert( ranked_player );
+ assert( ranked_player->context != 0 );
+
+ if( gr_joingame->status == GR_STATUS_OK )
+ {
+ int i;
+ // save user id
+ ranked_player->player_id = gr_joingame->player_id;
+ memcpy(ranked_player->token,gr_joingame->token,
+ sizeof(GR_PLAYER_TOKEN)) ;
+ match = GRankStartMatch( ranked_player->context );
+ ranked_player->match = match.match;
+ ranked_player->grank = gr_joingame->rank;
+
+ // find the index and call SV_RankUserValidate
+ for (i=0;i<sv_maxclients->value;i++)
+ if ( ranked_player == &s_ranked_players[i] )
+ SV_RankUserValidate(i,NULL,NULL,0, gr_joingame->rank,ranked_player->name);
+ }
+ else
+ {
+ //GRand handle join game failure
+ SV_RankError( "SV_RankJoinGameCBF: Unexpected status %s",
+ SV_RankStatusString( gr_joingame->status ) );
+
+ cleanup_status = GRankCleanupAsync
+ (
+ ranked_player->context,
+ 0,
+ SV_RankCleanupCBF,
+ cbf_arg,
+ GR_OPT_END
+ );
+
+ if( cleanup_status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankJoinGameCBF: Expected "
+ "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
+ SV_RankStatusString( cleanup_status ) );
+ SV_RankCloseContext( ranked_player );
+ }
+ }
+}
+
+/*
+================
+SV_RankSendReportsCBF
+================
+*/
+static void SV_RankSendReportsCBF( GR_STATUS* status, void* cbf_arg )
+{
+ ranked_player_t* ranked_player;
+ GR_CONTEXT context;
+ GR_STATUS cleanup_status;
+
+ assert( status != NULL );
+ // NULL cbf_arg means server is sending match reports
+
+ Com_DPrintf( "SV_RankSendReportsCBF( %08X, %08X );\n", status, cbf_arg );
+
+ ranked_player = (ranked_player_t*)cbf_arg;
+ if( ranked_player == NULL )
+ {
+ Com_DPrintf( "SV_RankSendReportsCBF: server\n" );
+ context = s_server_context;
+ }
+ else
+ {
+ Com_DPrintf( "SV_RankSendReportsCBF: player\n" );
+ context = ranked_player->context;
+ }
+
+ //assert( context != 0 );
+ if( *status != GR_STATUS_OK )
+ {
+ SV_RankError( "SV_RankSendReportsCBF: Unexpected status %s",
+ SV_RankStatusString( *status ) );
+ }
+
+ if( context == 0 )
+ {
+ Com_DPrintf( "SV_RankSendReportsCBF: WARNING: context == 0" );
+ SV_RankCloseContext( ranked_player );
+ }
+ else
+ {
+ cleanup_status = GRankCleanupAsync
+ (
+ context,
+ 0,
+ SV_RankCleanupCBF,
+ cbf_arg,
+ GR_OPT_END
+ );
+
+ if( cleanup_status != GR_STATUS_PENDING )
+ {
+ SV_RankError( "SV_RankSendReportsCBF: Expected "
+ "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
+ SV_RankStatusString( cleanup_status ) );
+ SV_RankCloseContext( ranked_player );
+ }
+ }
+}
+
+/*
+================
+SV_RankCleanupCBF
+================
+*/
+static void SV_RankCleanupCBF( GR_STATUS* status, void* cbf_arg )
+{
+ ranked_player_t* ranked_player;
+ ranked_player = (ranked_player_t*)cbf_arg;
+
+ assert( status != NULL );
+ // NULL cbf_arg means server is cleaning up
+
+ Com_DPrintf( "SV_RankCleanupCBF( %08X, %08X );\n", status, cbf_arg );
+
+ if( *status != GR_STATUS_OK )
+ {
+ SV_RankError( "SV_RankCleanupCBF: Unexpected status %s",
+ SV_RankStatusString( *status ) );
+ }
+
+ SV_RankCloseContext( ranked_player );
+}
+
+/*
+================
+SV_RankCloseContext
+================
+*/
+static void SV_RankCloseContext( ranked_player_t* ranked_player )
+{
+ if( ranked_player == NULL )
+ {
+ // server cleanup
+ if( s_server_context == 0 )
+ {
+ return;
+ }
+ s_server_context = 0;
+ s_server_match = 0;
+ }
+ else
+ {
+ // player cleanup
+ if( s_ranked_players == NULL )
+ {
+ return;
+ }
+ if( ranked_player->context == 0 )
+ {
+ return;
+ }
+ ranked_player->context = 0;
+ ranked_player->match = 0;
+ ranked_player->player_id = 0;
+ memset( ranked_player->token, 0, sizeof(GR_PLAYER_TOKEN) );
+ ranked_player->grank_status = ranked_player->final_status;
+ ranked_player->final_status = QGR_STATUS_NEW;
+ ranked_player->name[0] = '\0';
+ }
+
+ assert( s_rankings_contexts > 0 );
+ s_rankings_contexts--;
+ Com_DPrintf( "SV_RankCloseContext: s_rankings_contexts = %d\n",
+ s_rankings_contexts );
+
+ if( s_rankings_contexts == 0 )
+ {
+ GRankLogLevel( GRLOG_OFF );
+
+ if( s_ranked_players != NULL )
+ {
+ Z_Free( s_ranked_players );
+ s_ranked_players = NULL;
+ }
+
+ s_rankings_active = qfalse;
+ Cvar_Set( "sv_rankingsActive", "0" );
+ }
+}
+
+/*
+================
+SV_RankAsciiEncode
+
+Encodes src_len bytes of binary data from the src buffer as ASCII text,
+using 6 bits per character. The result string is null-terminated and
+stored in the dest buffer.
+
+The dest buffer must be at least (src_len * 4) / 3 + 2 bytes in length.
+
+Returns the length of the result string, not including the null.
+================
+*/
+static int SV_RankAsciiEncode( char* dest, const unsigned char* src,
+ int src_len )
+{
+ unsigned char bin[3];
+ unsigned char txt[4];
+ int dest_len = 0;
+ int i;
+ int j;
+ int num_chars;
+
+ assert( dest != NULL );
+ assert( src != NULL );
+
+ for( i = 0; i < src_len; i += 3 )
+ {
+ // read three bytes of input
+ for( j = 0; j < 3; j++ )
+ {
+ bin[j] = (i + j < src_len) ? src[i + j] : 0;
+ }
+
+ // get four 6-bit values from three bytes
+ txt[0] = bin[0] >> 2;
+ txt[1] = ((bin[0] << 4) | (bin[1] >> 4)) & 63;
+ txt[2] = ((bin[1] << 2) | (bin[2] >> 6)) & 63;
+ txt[3] = bin[2] & 63;
+
+ // store ASCII encoding of 6-bit values
+ num_chars = (i + 2 < src_len) ? 4 : ((src_len - i) * 4) / 3 + 1;
+ for( j = 0; j < num_chars; j++ )
+ {
+ dest[dest_len++] = s_ascii_encoding[txt[j]];
+ }
+ }
+
+ dest[dest_len] = '\0';
+
+ return dest_len;
+}
+
+/*
+================
+SV_RankAsciiDecode
+
+Decodes src_len characters of ASCII text from the src buffer, stores
+the binary result in the dest buffer.
+
+The dest buffer must be at least (src_len * 3) / 4 bytes in length.
+
+Returns the length of the binary result, or zero for invalid input.
+================
+*/
+static int SV_RankAsciiDecode( unsigned char* dest, const char* src,
+ int src_len )
+{
+ static unsigned char s_inverse_encoding[256];
+ static char s_init = 0;
+
+ unsigned char bin[3];
+ unsigned char txt[4];
+ int dest_len = 0;
+ int i;
+ int j;
+ int num_bytes;
+
+ assert( dest != NULL );
+ assert( src != NULL );
+
+ if( !s_init )
+ {
+ // initialize lookup table for decoding
+ memset( s_inverse_encoding, 255, sizeof(s_inverse_encoding) );
+ for( i = 0; i < 64; i++ )
+ {
+ s_inverse_encoding[s_ascii_encoding[i]] = i;
+ }
+ s_init = 1;
+ }
+
+ for( i = 0; i < src_len; i += 4 )
+ {
+ // read four characters of input, decode them to 6-bit values
+ for( j = 0; j < 4; j++ )
+ {
+ txt[j] = (i + j < src_len) ? s_inverse_encoding[src[i + j]] : 0;
+ if (txt[j] == 255)
+ {
+ return 0; // invalid input character
+ }
+ }
+
+ // get three bytes from four 6-bit values
+ bin[0] = (txt[0] << 2) | (txt[1] >> 4);
+ bin[1] = (txt[1] << 4) | (txt[2] >> 2);
+ bin[2] = (txt[2] << 6) | txt[3];
+
+ // store binary data
+ num_bytes = (i + 3 < src_len) ? 3 : ((src_len - i) * 3) / 4;
+ for( j = 0; j < num_bytes; j++ )
+ {
+ dest[dest_len++] = bin[j];
+ }
+ }
+
+ return dest_len;
+}
+
+/*
+================
+SV_RankEncodeGameID
+================
+*/
+static void SV_RankEncodeGameID( uint64_t game_id, char* result,
+ int len )
+{
+ assert( result != NULL );
+
+ if( len < ( ( sizeof(game_id) * 4) / 3 + 2) )
+ {
+ Com_DPrintf( "SV_RankEncodeGameID: result buffer too small\n" );
+ result[0] = '\0';
+ }
+ else
+ {
+ qint64 gameid = LittleLong64(*(qint64*)&game_id);
+ SV_RankAsciiEncode( result, (unsigned char*)&gameid,
+ sizeof(qint64) );
+ }
+}
+
+/*
+================
+SV_RankDecodePlayerID
+================
+*/
+static uint64_t SV_RankDecodePlayerID( const char* string )
+{
+ unsigned char buffer[9];
+ int len;
+ qint64 player_id;
+
+ assert( string != NULL );
+
+ len = strlen (string) ;
+ Com_DPrintf( "SV_RankDecodePlayerID: string length %d\n",len );
+ SV_RankAsciiDecode( buffer, string, len );
+ player_id = LittleLong64(*(qint64*)buffer);
+ return *(uint64_t*)&player_id;
+}
+
+/*
+================
+SV_RankDecodePlayerKey
+================
+*/
+static void SV_RankDecodePlayerKey( const char* string, GR_PLAYER_TOKEN key )
+{
+ unsigned char buffer[1400];
+ int len;
+ assert( string != NULL );
+
+ len = strlen (string) ;
+ Com_DPrintf( "SV_RankDecodePlayerKey: string length %d\n",len );
+
+ memset(key,0,sizeof(GR_PLAYER_TOKEN));
+ memset(buffer,0,sizeof(buffer));
+ memcpy( key, buffer, SV_RankAsciiDecode( buffer, string, len ) );
+}
+
+/*
+================
+SV_RankStatusString
+================
+*/
+static char* SV_RankStatusString( GR_STATUS status )
+{
+ switch( status )
+ {
+ case GR_STATUS_OK: return "GR_STATUS_OK";
+ case GR_STATUS_ERROR: return "GR_STATUS_ERROR";
+ case GR_STATUS_BADPARAMS: return "GR_STATUS_BADPARAMS";
+ case GR_STATUS_NETWORK: return "GR_STATUS_NETWORK";
+ case GR_STATUS_NOUSER: return "GR_STATUS_NOUSER";
+ case GR_STATUS_BADPASSWORD: return "GR_STATUS_BADPASSWORD";
+ case GR_STATUS_BADGAME: return "GR_STATUS_BADGAME";
+ case GR_STATUS_PENDING: return "GR_STATUS_PENDING";
+ case GR_STATUS_BADDOMAIN: return "GR_STATUS_BADDOMAIN";
+ case GR_STATUS_DOMAINLOCK: return "GR_STATUS_DOMAINLOCK";
+ case GR_STATUS_TIMEOUT: return "GR_STATUS_TIMEOUT";
+ case GR_STATUS_INVALIDUSER: return "GR_STATUS_INVALIDUSER";
+ case GR_STATUS_INVALIDCONTEXT: return "GR_STATUS_INVALIDCONTEXT";
+ default: return "(UNKNOWN)";
+ }
+}
+
+/*
+================
+SV_RankError
+================
+*/
+static void SV_RankError( const char* fmt, ... )
+{
+ va_list arg_ptr;
+ char text[1024];
+
+ va_start( arg_ptr, fmt );
+ Q_vsnprintf(text, sizeof(text), fmt, arg_ptr );
+ va_end( arg_ptr );
+
+ Com_DPrintf( "****************************************\n" );
+ Com_DPrintf( "SV_RankError: %s\n", text );
+ Com_DPrintf( "****************************************\n" );
+
+ s_rankings_active = qfalse;
+ Cvar_Set( "sv_rankingsActive", "0" );
+ // FIXME - attempt clean shutdown?
+}
+
diff --git a/code/server/sv_snapshot.c b/code/server/sv_snapshot.c
new file mode 100644
index 0000000..d84a189
--- /dev/null
+++ b/code/server/sv_snapshot.c
@@ -0,0 +1,699 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "server.h"
+
+
+/*
+=============================================================================
+
+Delta encode a client frame onto the network channel
+
+A normal server packet will look like:
+
+4 sequence number (high bit set if an oversize fragment)
+<optional reliable commands>
+1 svc_snapshot
+4 last client reliable command
+4 serverTime
+1 lastframe for delta compression
+1 snapFlags
+1 areaBytes
+<areabytes>
+<playerstate>
+<packetentities>
+
+=============================================================================
+*/
+
+/*
+=============
+SV_EmitPacketEntities
+
+Writes a delta update of an entityState_t list to the message.
+=============
+*/
+static void SV_EmitPacketEntities( clientSnapshot_t *from, clientSnapshot_t *to, msg_t *msg ) {
+ entityState_t *oldent, *newent;
+ int oldindex, newindex;
+ int oldnum, newnum;
+ int from_num_entities;
+
+ // generate the delta update
+ if ( !from ) {
+ from_num_entities = 0;
+ } else {
+ from_num_entities = from->num_entities;
+ }
+
+ newent = NULL;
+ oldent = NULL;
+ newindex = 0;
+ oldindex = 0;
+ while ( newindex < to->num_entities || oldindex < from_num_entities ) {
+ if ( newindex >= to->num_entities ) {
+ newnum = 9999;
+ } else {
+ newent = &svs.snapshotEntities[(to->first_entity+newindex) % svs.numSnapshotEntities];
+ newnum = newent->number;
+ }
+
+ if ( oldindex >= from_num_entities ) {
+ oldnum = 9999;
+ } else {
+ oldent = &svs.snapshotEntities[(from->first_entity+oldindex) % svs.numSnapshotEntities];
+ oldnum = oldent->number;
+ }
+
+ if ( newnum == oldnum ) {
+ // delta update from old position
+ // because the force parm is qfalse, this will not result
+ // in any bytes being emited if the entity has not changed at all
+ MSG_WriteDeltaEntity (msg, oldent, newent, qfalse );
+ oldindex++;
+ newindex++;
+ continue;
+ }
+
+ if ( newnum < oldnum ) {
+ // this is a new entity, send it from the baseline
+ MSG_WriteDeltaEntity (msg, &sv.svEntities[newnum].baseline, newent, qtrue );
+ newindex++;
+ continue;
+ }
+
+ if ( newnum > oldnum ) {
+ // the old entity isn't present in the new message
+ MSG_WriteDeltaEntity (msg, oldent, NULL, qtrue );
+ oldindex++;
+ continue;
+ }
+ }
+
+ MSG_WriteBits( msg, (MAX_GENTITIES-1), GENTITYNUM_BITS ); // end of packetentities
+}
+
+
+
+/*
+==================
+SV_WriteSnapshotToClient
+==================
+*/
+static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) {
+ clientSnapshot_t *frame, *oldframe;
+ int lastframe;
+ int i;
+ int snapFlags;
+
+ // this is the snapshot we are creating
+ frame = &client->frames[ client->netchan.outgoingSequence & PACKET_MASK ];
+
+ // try to use a previous frame as the source for delta compressing the snapshot
+ if ( client->deltaMessage <= 0 || client->state != CS_ACTIVE ) {
+ // client is asking for a retransmit
+ oldframe = NULL;
+ lastframe = 0;
+ } else if ( client->netchan.outgoingSequence - client->deltaMessage
+ >= (PACKET_BACKUP - 3) ) {
+ // client hasn't gotten a good message through in a long time
+ Com_DPrintf ("%s: Delta request from out of date packet.\n", client->name);
+ oldframe = NULL;
+ lastframe = 0;
+ } else {
+ // we have a valid snapshot to delta from
+ oldframe = &client->frames[ client->deltaMessage & PACKET_MASK ];
+ lastframe = client->netchan.outgoingSequence - client->deltaMessage;
+
+ // the snapshot's entities may still have rolled off the buffer, though
+ if ( oldframe->first_entity <= svs.nextSnapshotEntities - svs.numSnapshotEntities ) {
+ Com_DPrintf ("%s: Delta request from out of date entities.\n", client->name);
+ oldframe = NULL;
+ lastframe = 0;
+ }
+ }
+
+ MSG_WriteByte (msg, svc_snapshot);
+
+ // NOTE, MRE: now sent at the start of every message from server to client
+ // let the client know which reliable clientCommands we have received
+ //MSG_WriteLong( msg, client->lastClientCommand );
+
+ // send over the current server time so the client can drift
+ // its view of time to try to match
+ if( client->oldServerTime ) {
+ // The server has not yet got an acknowledgement of the
+ // new gamestate from this client, so continue to send it
+ // a time as if the server has not restarted. Note from
+ // the client's perspective this time is strictly speaking
+ // incorrect, but since it'll be busy loading a map at
+ // the time it doesn't really matter.
+ MSG_WriteLong (msg, sv.time + client->oldServerTime);
+ } else {
+ MSG_WriteLong (msg, sv.time);
+ }
+
+ // what we are delta'ing from
+ MSG_WriteByte (msg, lastframe);
+
+ snapFlags = svs.snapFlagServerBit;
+ if ( client->rateDelayed ) {
+ snapFlags |= SNAPFLAG_RATE_DELAYED;
+ }
+ if ( client->state != CS_ACTIVE ) {
+ snapFlags |= SNAPFLAG_NOT_ACTIVE;
+ }
+
+ MSG_WriteByte (msg, snapFlags);
+
+ // send over the areabits
+ MSG_WriteByte (msg, frame->areabytes);
+ MSG_WriteData (msg, frame->areabits, frame->areabytes);
+
+ // delta encode the playerstate
+ if ( oldframe ) {
+ MSG_WriteDeltaPlayerstate( msg, &oldframe->ps, &frame->ps );
+ } else {
+ MSG_WriteDeltaPlayerstate( msg, NULL, &frame->ps );
+ }
+
+ // delta encode the entities
+ SV_EmitPacketEntities (oldframe, frame, msg);
+
+ // padding for rate debugging
+ if ( sv_padPackets->integer ) {
+ for ( i = 0 ; i < sv_padPackets->integer ; i++ ) {
+ MSG_WriteByte (msg, svc_nop);
+ }
+ }
+}
+
+
+/*
+==================
+SV_UpdateServerCommandsToClient
+
+(re)send all server commands the client hasn't acknowledged yet
+==================
+*/
+void SV_UpdateServerCommandsToClient( client_t *client, msg_t *msg ) {
+ int i;
+
+ // write any unacknowledged serverCommands
+ for ( i = client->reliableAcknowledge + 1 ; i <= client->reliableSequence ; i++ ) {
+ MSG_WriteByte( msg, svc_serverCommand );
+ MSG_WriteLong( msg, i );
+ MSG_WriteString( msg, client->reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] );
+ }
+ client->reliableSent = client->reliableSequence;
+}
+
+/*
+=============================================================================
+
+Build a client snapshot structure
+
+=============================================================================
+*/
+
+#define MAX_SNAPSHOT_ENTITIES 1024
+typedef struct {
+ int numSnapshotEntities;
+ int snapshotEntities[MAX_SNAPSHOT_ENTITIES];
+} snapshotEntityNumbers_t;
+
+/*
+=======================
+SV_QsortEntityNumbers
+=======================
+*/
+static int QDECL SV_QsortEntityNumbers( const void *a, const void *b ) {
+ int *ea, *eb;
+
+ ea = (int *)a;
+ eb = (int *)b;
+
+ if ( *ea == *eb ) {
+ Com_Error( ERR_DROP, "SV_QsortEntityStates: duplicated entity" );
+ }
+
+ if ( *ea < *eb ) {
+ return -1;
+ }
+
+ return 1;
+}
+
+
+/*
+===============
+SV_AddEntToSnapshot
+===============
+*/
+static void SV_AddEntToSnapshot( svEntity_t *svEnt, sharedEntity_t *gEnt, snapshotEntityNumbers_t *eNums ) {
+ // if we have already added this entity to this snapshot, don't add again
+ if ( svEnt->snapshotCounter == sv.snapshotCounter ) {
+ return;
+ }
+ svEnt->snapshotCounter = sv.snapshotCounter;
+
+ // if we are full, silently discard entities
+ if ( eNums->numSnapshotEntities == MAX_SNAPSHOT_ENTITIES ) {
+ return;
+ }
+
+ eNums->snapshotEntities[ eNums->numSnapshotEntities ] = gEnt->s.number;
+ eNums->numSnapshotEntities++;
+}
+
+/*
+===============
+SV_AddEntitiesVisibleFromPoint
+===============
+*/
+static void SV_AddEntitiesVisibleFromPoint( vec3_t origin, clientSnapshot_t *frame,
+ snapshotEntityNumbers_t *eNums, qboolean portal ) {
+ int e, i;
+ sharedEntity_t *ent;
+ svEntity_t *svEnt;
+ int l;
+ int clientarea, clientcluster;
+ int leafnum;
+ byte *clientpvs;
+ byte *bitvector;
+
+ // during an error shutdown message we may need to transmit
+ // the shutdown message after the server has shutdown, so
+ // specfically check for it
+ if ( !sv.state ) {
+ return;
+ }
+
+ leafnum = CM_PointLeafnum (origin);
+ clientarea = CM_LeafArea (leafnum);
+ clientcluster = CM_LeafCluster (leafnum);
+
+ // calculate the visible areas
+ frame->areabytes = CM_WriteAreaBits( frame->areabits, clientarea );
+
+ clientpvs = CM_ClusterPVS (clientcluster);
+
+ for ( e = 0 ; e < sv.num_entities ; e++ ) {
+ ent = SV_GentityNum(e);
+
+ // never send entities that aren't linked in
+ if ( !ent->r.linked ) {
+ continue;
+ }
+
+ if (ent->s.number != e) {
+ Com_DPrintf ("FIXING ENT->S.NUMBER!!!\n");
+ ent->s.number = e;
+ }
+
+ // entities can be flagged to explicitly not be sent to the client
+ if ( ent->r.svFlags & SVF_NOCLIENT ) {
+ continue;
+ }
+
+ // entities can be flagged to be sent to only one client
+ if ( ent->r.svFlags & SVF_SINGLECLIENT ) {
+ if ( ent->r.singleClient != frame->ps.clientNum ) {
+ continue;
+ }
+ }
+ // entities can be flagged to be sent to everyone but one client
+ if ( ent->r.svFlags & SVF_NOTSINGLECLIENT ) {
+ if ( ent->r.singleClient == frame->ps.clientNum ) {
+ continue;
+ }
+ }
+ // entities can be flagged to be sent to a given mask of clients
+ if ( ent->r.svFlags & SVF_CLIENTMASK ) {
+ if (frame->ps.clientNum >= 32)
+ Com_Error( ERR_DROP, "SVF_CLIENTMASK: cientNum > 32\n" );
+ if (~ent->r.singleClient & (1 << frame->ps.clientNum))
+ continue;
+ }
+
+ svEnt = SV_SvEntityForGentity( ent );
+
+ // don't double add an entity through portals
+ if ( svEnt->snapshotCounter == sv.snapshotCounter ) {
+ continue;
+ }
+
+ // broadcast entities are always sent
+ if ( ent->r.svFlags & SVF_BROADCAST ) {
+ SV_AddEntToSnapshot( svEnt, ent, eNums );
+ continue;
+ }
+
+ // ignore if not touching a PV leaf
+ // check area
+ if ( !CM_AreasConnected( clientarea, svEnt->areanum ) ) {
+ // doors can legally straddle two areas, so
+ // we may need to check another one
+ if ( !CM_AreasConnected( clientarea, svEnt->areanum2 ) ) {
+ continue; // blocked by a door
+ }
+ }
+
+ bitvector = clientpvs;
+
+ // check individual leafs
+ if ( !svEnt->numClusters ) {
+ continue;
+ }
+ l = 0;
+ for ( i=0 ; i < svEnt->numClusters ; i++ ) {
+ l = svEnt->clusternums[i];
+ if ( bitvector[l >> 3] & (1 << (l&7) ) ) {
+ break;
+ }
+ }
+
+ // if we haven't found it to be visible,
+ // check overflow clusters that coudln't be stored
+ if ( i == svEnt->numClusters ) {
+ if ( svEnt->lastCluster ) {
+ for ( ; l <= svEnt->lastCluster ; l++ ) {
+ if ( bitvector[l >> 3] & (1 << (l&7) ) ) {
+ break;
+ }
+ }
+ if ( l == svEnt->lastCluster ) {
+ continue; // not visible
+ }
+ } else {
+ continue;
+ }
+ }
+
+ // add it
+ SV_AddEntToSnapshot( svEnt, ent, eNums );
+
+ // if its a portal entity, add everything visible from its camera position
+ if ( ent->r.svFlags & SVF_PORTAL ) {
+ if ( ent->s.generic1 ) {
+ vec3_t dir;
+ VectorSubtract(ent->s.origin, origin, dir);
+ if ( VectorLengthSquared(dir) > (float) ent->s.generic1 * ent->s.generic1 ) {
+ continue;
+ }
+ }
+ SV_AddEntitiesVisibleFromPoint( ent->s.origin2, frame, eNums, qtrue );
+ }
+
+ }
+}
+
+/*
+=============
+SV_BuildClientSnapshot
+
+Decides which entities are going to be visible to the client, and
+copies off the playerstate and areabits.
+
+This properly handles multiple recursive portals, but the render
+currently doesn't.
+
+For viewing through other player's eyes, clent can be something other than client->gentity
+=============
+*/
+static void SV_BuildClientSnapshot( client_t *client ) {
+ vec3_t org;
+ clientSnapshot_t *frame;
+ snapshotEntityNumbers_t entityNumbers;
+ int i;
+ sharedEntity_t *ent;
+ entityState_t *state;
+ svEntity_t *svEnt;
+ sharedEntity_t *clent;
+ int clientNum;
+ playerState_t *ps;
+
+ // bump the counter used to prevent double adding
+ sv.snapshotCounter++;
+
+ // this is the frame we are creating
+ frame = &client->frames[ client->netchan.outgoingSequence & PACKET_MASK ];
+
+ // clear everything in this snapshot
+ entityNumbers.numSnapshotEntities = 0;
+ Com_Memset( frame->areabits, 0, sizeof( frame->areabits ) );
+
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=62
+ frame->num_entities = 0;
+
+ clent = client->gentity;
+ if ( !clent || client->state == CS_ZOMBIE ) {
+ return;
+ }
+
+ // grab the current playerState_t
+ ps = SV_GameClientNum( client - svs.clients );
+ frame->ps = *ps;
+
+ // never send client's own entity, because it can
+ // be regenerated from the playerstate
+ clientNum = frame->ps.clientNum;
+ if ( clientNum < 0 || clientNum >= MAX_GENTITIES ) {
+ Com_Error( ERR_DROP, "SV_SvEntityForGentity: bad gEnt" );
+ }
+ svEnt = &sv.svEntities[ clientNum ];
+
+ svEnt->snapshotCounter = sv.snapshotCounter;
+
+ // find the client's viewpoint
+ VectorCopy( ps->origin, org );
+ org[2] += ps->viewheight;
+
+ // add all the entities directly visible to the eye, which
+ // may include portal entities that merge other viewpoints
+ SV_AddEntitiesVisibleFromPoint( org, frame, &entityNumbers, qfalse );
+
+ // if there were portals visible, there may be out of order entities
+ // in the list which will need to be resorted for the delta compression
+ // to work correctly. This also catches the error condition
+ // of an entity being included twice.
+ qsort( entityNumbers.snapshotEntities, entityNumbers.numSnapshotEntities,
+ sizeof( entityNumbers.snapshotEntities[0] ), SV_QsortEntityNumbers );
+
+ // now that all viewpoint's areabits have been OR'd together, invert
+ // all of them to make it a mask vector, which is what the renderer wants
+ for ( i = 0 ; i < MAX_MAP_AREA_BYTES/4 ; i++ ) {
+ ((int *)frame->areabits)[i] = ((int *)frame->areabits)[i] ^ -1;
+ }
+
+ // copy the entity states out
+ frame->num_entities = 0;
+ frame->first_entity = svs.nextSnapshotEntities;
+ for ( i = 0 ; i < entityNumbers.numSnapshotEntities ; i++ ) {
+ ent = SV_GentityNum(entityNumbers.snapshotEntities[i]);
+ state = &svs.snapshotEntities[svs.nextSnapshotEntities % svs.numSnapshotEntities];
+ *state = ent->s;
+ svs.nextSnapshotEntities++;
+ // this should never hit, map should always be restarted first in SV_Frame
+ if ( svs.nextSnapshotEntities >= 0x7FFFFFFE ) {
+ Com_Error(ERR_FATAL, "svs.nextSnapshotEntities wrapped");
+ }
+ frame->num_entities++;
+ }
+}
+
+
+/*
+====================
+SV_RateMsec
+
+Return the number of msec a given size message is supposed
+to take to clear, based on the current rate
+====================
+*/
+#define HEADER_RATE_BYTES 48 // include our header, IP header, and some overhead
+static int SV_RateMsec( client_t *client, int messageSize ) {
+ int rate;
+ int rateMsec;
+
+ // individual messages will never be larger than fragment size
+ if ( messageSize > 1500 ) {
+ messageSize = 1500;
+ }
+ rate = client->rate;
+ if ( sv_maxRate->integer ) {
+ if ( sv_maxRate->integer < 1000 ) {
+ Cvar_Set( "sv_MaxRate", "1000" );
+ }
+ if ( sv_maxRate->integer < rate ) {
+ rate = sv_maxRate->integer;
+ }
+ }
+ if ( sv_minRate->integer ) {
+ if ( sv_minRate->integer < 1000 )
+ Cvar_Set( "sv_minRate", "1000" );
+ if ( sv_minRate->integer > rate )
+ rate = sv_minRate->integer;
+ }
+
+ rateMsec = ( messageSize + HEADER_RATE_BYTES ) * 1000 / rate * com_timescale->value;
+
+ return rateMsec;
+}
+
+/*
+=======================
+SV_SendMessageToClient
+
+Called by SV_SendClientSnapshot and SV_SendClientGameState
+=======================
+*/
+void SV_SendMessageToClient( msg_t *msg, client_t *client ) {
+ int rateMsec;
+
+ // record information about the message
+ client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageSize = msg->cursize;
+ client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageSent = svs.time;
+ client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageAcked = -1;
+
+ // send the datagram
+ SV_Netchan_Transmit( client, msg ); //msg->cursize, msg->data );
+
+ // set nextSnapshotTime based on rate and requested number of updates
+
+ // local clients get snapshots every server frame
+ // TTimo - https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=491
+ // added sv_lanForceRate check
+ if ( client->netchan.remoteAddress.type == NA_LOOPBACK || (sv_lanForceRate->integer && Sys_IsLANAddress (client->netchan.remoteAddress)) ) {
+ client->nextSnapshotTime = svs.time + (1000.0 / sv_fps->integer * com_timescale->value);
+ return;
+ }
+
+ // normal rate / snapshotMsec calculation
+ rateMsec = SV_RateMsec(client, msg->cursize);
+
+ if ( rateMsec < client->snapshotMsec * com_timescale->value) {
+ // never send more packets than this, no matter what the rate is at
+ rateMsec = client->snapshotMsec * com_timescale->value;
+ client->rateDelayed = qfalse;
+ } else {
+ client->rateDelayed = qtrue;
+ }
+
+ client->nextSnapshotTime = svs.time + rateMsec * com_timescale->value;
+
+ // don't pile up empty snapshots while connecting
+ if ( client->state != CS_ACTIVE ) {
+ // a gigantic connection message may have already put the nextSnapshotTime
+ // more than a second away, so don't shorten it
+ // do shorten if client is downloading
+ if (!*client->downloadName && client->nextSnapshotTime < svs.time + 1000 * com_timescale->value)
+ client->nextSnapshotTime = svs.time + 1000 * com_timescale->value;
+ }
+}
+
+
+/*
+=======================
+SV_SendClientSnapshot
+
+Also called by SV_FinalMessage
+
+=======================
+*/
+void SV_SendClientSnapshot( client_t *client ) {
+ byte msg_buf[MAX_MSGLEN];
+ msg_t msg;
+
+ // build the snapshot
+ SV_BuildClientSnapshot( client );
+
+ // bots need to have their snapshots build, but
+ // the query them directly without needing to be sent
+ if ( client->gentity && client->gentity->r.svFlags & SVF_BOT ) {
+ return;
+ }
+
+ MSG_Init (&msg, msg_buf, sizeof(msg_buf));
+ msg.allowoverflow = qtrue;
+
+ // NOTE, MRE: all server->client messages now acknowledge
+ // let the client know which reliable clientCommands we have received
+ MSG_WriteLong( &msg, client->lastClientCommand );
+
+ // (re)send any reliable server commands
+ SV_UpdateServerCommandsToClient( client, &msg );
+
+ // send over all the relevant entityState_t
+ // and the playerState_t
+ SV_WriteSnapshotToClient( client, &msg );
+
+ // Add any download data if the client is downloading
+ SV_WriteDownloadToClient( client, &msg );
+
+#ifdef USE_VOIP
+ SV_WriteVoipToClient( client, &msg );
+#endif
+
+ // check for overflow
+ if ( msg.overflowed ) {
+ Com_Printf ("WARNING: msg overflowed for %s\n", client->name);
+ MSG_Clear (&msg);
+ }
+
+ SV_SendMessageToClient( &msg, client );
+}
+
+
+/*
+=======================
+SV_SendClientMessages
+=======================
+*/
+void SV_SendClientMessages( void ) {
+ int i;
+ client_t *c;
+
+ // send a message to each connected client
+ for (i=0, c = svs.clients ; i < sv_maxclients->integer ; i++, c++) {
+ if (!c->state) {
+ continue; // not connected
+ }
+
+ if ( svs.time < c->nextSnapshotTime ) {
+ continue; // not time yet
+ }
+
+ // send additional message fragments if the last message
+ // was too large to send at once
+ if ( c->netchan.unsentFragments ) {
+ c->nextSnapshotTime = svs.time +
+ SV_RateMsec( c, c->netchan.unsentLength - c->netchan.unsentFragmentStart );
+ SV_Netchan_TransmitNextFragment( c );
+ continue;
+ }
+
+ // generate and send a new message
+ SV_SendClientSnapshot( c );
+ }
+}
+
diff --git a/code/server/sv_world.c b/code/server/sv_world.c
new file mode 100644
index 0000000..b2cec81
--- /dev/null
+++ b/code/server/sv_world.c
@@ -0,0 +1,691 @@
+/*
+===========================================================================
+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 Quake III Arena source code; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+// world.c -- world query functions
+
+#include "server.h"
+
+/*
+================
+SV_ClipHandleForEntity
+
+Returns a headnode that can be used for testing or clipping to a
+given entity. If the entity is a bsp model, the headnode will
+be returned, otherwise a custom box tree will be constructed.
+================
+*/
+clipHandle_t SV_ClipHandleForEntity( const sharedEntity_t *ent ) {
+ if ( ent->r.bmodel ) {
+ // explicit hulls in the BSP model
+ return CM_InlineModel( ent->s.modelindex );
+ }
+ if ( ent->r.svFlags & SVF_CAPSULE ) {
+ // create a temp capsule from bounding box sizes
+ return CM_TempBoxModel( ent->r.mins, ent->r.maxs, qtrue );
+ }
+
+ // create a temp tree from bounding box sizes
+ return CM_TempBoxModel( ent->r.mins, ent->r.maxs, qfalse );
+}
+
+
+
+/*
+===============================================================================
+
+ENTITY CHECKING
+
+To avoid linearly searching through lists of entities during environment testing,
+the world is carved up with an evenly spaced, axially aligned bsp tree. Entities
+are kept in chains either at the final leafs, or at the first node that splits
+them, which prevents having to deal with multiple fragments of a single entity.
+
+===============================================================================
+*/
+
+typedef struct worldSector_s {
+ int axis; // -1 = leaf node
+ float dist;
+ struct worldSector_s *children[2];
+ svEntity_t *entities;
+} worldSector_t;
+
+#define AREA_DEPTH 4
+#define AREA_NODES 64
+
+worldSector_t sv_worldSectors[AREA_NODES];
+int sv_numworldSectors;
+
+
+/*
+===============
+SV_SectorList_f
+===============
+*/
+void SV_SectorList_f( void ) {
+ int i, c;
+ worldSector_t *sec;
+ svEntity_t *ent;
+
+ for ( i = 0 ; i < AREA_NODES ; i++ ) {
+ sec = &sv_worldSectors[i];
+
+ c = 0;
+ for ( ent = sec->entities ; ent ; ent = ent->nextEntityInWorldSector ) {
+ c++;
+ }
+ Com_Printf( "sector %i: %i entities\n", i, c );
+ }
+}
+
+/*
+===============
+SV_CreateworldSector
+
+Builds a uniformly subdivided tree for the given world size
+===============
+*/
+static worldSector_t *SV_CreateworldSector( int depth, vec3_t mins, vec3_t maxs ) {
+ worldSector_t *anode;
+ vec3_t size;
+ vec3_t mins1, maxs1, mins2, maxs2;
+
+ anode = &sv_worldSectors[sv_numworldSectors];
+ sv_numworldSectors++;
+
+ if (depth == AREA_DEPTH) {
+ anode->axis = -1;
+ anode->children[0] = anode->children[1] = NULL;
+ return anode;
+ }
+
+ VectorSubtract (maxs, mins, size);
+ if (size[0] > size[1]) {
+ anode->axis = 0;
+ } else {
+ anode->axis = 1;
+ }
+
+ anode->dist = 0.5 * (maxs[anode->axis] + mins[anode->axis]);
+ VectorCopy (mins, mins1);
+ VectorCopy (mins, mins2);
+ VectorCopy (maxs, maxs1);
+ VectorCopy (maxs, maxs2);
+
+ maxs1[anode->axis] = mins2[anode->axis] = anode->dist;
+
+ anode->children[0] = SV_CreateworldSector (depth+1, mins2, maxs2);
+ anode->children[1] = SV_CreateworldSector (depth+1, mins1, maxs1);
+
+ return anode;
+}
+
+/*
+===============
+SV_ClearWorld
+
+===============
+*/
+void SV_ClearWorld( void ) {
+ clipHandle_t h;
+ vec3_t mins, maxs;
+
+ Com_Memset( sv_worldSectors, 0, sizeof(sv_worldSectors) );
+ sv_numworldSectors = 0;
+
+ // get world map bounds
+ h = CM_InlineModel( 0 );
+ CM_ModelBounds( h, mins, maxs );
+ SV_CreateworldSector( 0, mins, maxs );
+}
+
+
+/*
+===============
+SV_UnlinkEntity
+
+===============
+*/
+void SV_UnlinkEntity( sharedEntity_t *gEnt ) {
+ svEntity_t *ent;
+ svEntity_t *scan;
+ worldSector_t *ws;
+
+ ent = SV_SvEntityForGentity( gEnt );
+
+ gEnt->r.linked = qfalse;
+
+ ws = ent->worldSector;
+ if ( !ws ) {
+ return; // not linked in anywhere
+ }
+ ent->worldSector = NULL;
+
+ if ( ws->entities == ent ) {
+ ws->entities = ent->nextEntityInWorldSector;
+ return;
+ }
+
+ for ( scan = ws->entities ; scan ; scan = scan->nextEntityInWorldSector ) {
+ if ( scan->nextEntityInWorldSector == ent ) {
+ scan->nextEntityInWorldSector = ent->nextEntityInWorldSector;
+ return;
+ }
+ }
+
+ Com_Printf( "WARNING: SV_UnlinkEntity: not found in worldSector\n" );
+}
+
+
+/*
+===============
+SV_LinkEntity
+
+===============
+*/
+#define MAX_TOTAL_ENT_LEAFS 128
+void SV_LinkEntity( sharedEntity_t *gEnt ) {
+ worldSector_t *node;
+ int leafs[MAX_TOTAL_ENT_LEAFS];
+ int cluster;
+ int num_leafs;
+ int i, j, k;
+ int area;
+ int lastLeaf;
+ float *origin, *angles;
+ svEntity_t *ent;
+
+ ent = SV_SvEntityForGentity( gEnt );
+
+ if ( ent->worldSector ) {
+ SV_UnlinkEntity( gEnt ); // unlink from old position
+ }
+
+ // encode the size into the entityState_t for client prediction
+ if ( gEnt->r.bmodel ) {
+ gEnt->s.solid = SOLID_BMODEL; // a solid_box will never create this value
+ } else if ( gEnt->r.contents & ( CONTENTS_SOLID | CONTENTS_BODY ) ) {
+ // assume that x/y are equal and symetric
+ i = gEnt->r.maxs[0];
+ if (i<1)
+ i = 1;
+ if (i>255)
+ i = 255;
+
+ // z is not symetric
+ j = (-gEnt->r.mins[2]);
+ if (j<1)
+ j = 1;
+ if (j>255)
+ j = 255;
+
+ // and z maxs can be negative...
+ k = (gEnt->r.maxs[2]+32);
+ if (k<1)
+ k = 1;
+ if (k>255)
+ k = 255;
+
+ gEnt->s.solid = (k<<16) | (j<<8) | i;
+ } else {
+ gEnt->s.solid = 0;
+ }
+
+ // get the position
+ origin = gEnt->r.currentOrigin;
+ angles = gEnt->r.currentAngles;
+
+ // set the abs box
+ if ( gEnt->r.bmodel && (angles[0] || angles[1] || angles[2]) ) {
+ // expand for rotation
+ float max;
+ int i;
+
+ max = RadiusFromBounds( gEnt->r.mins, gEnt->r.maxs );
+ for (i=0 ; i<3 ; i++) {
+ gEnt->r.absmin[i] = origin[i] - max;
+ gEnt->r.absmax[i] = origin[i] + max;
+ }
+ } else {
+ // normal
+ VectorAdd (origin, gEnt->r.mins, gEnt->r.absmin);
+ VectorAdd (origin, gEnt->r.maxs, gEnt->r.absmax);
+ }
+
+ // because movement is clipped an epsilon away from an actual edge,
+ // we must fully check even when bounding boxes don't quite touch
+ gEnt->r.absmin[0] -= 1;
+ gEnt->r.absmin[1] -= 1;
+ gEnt->r.absmin[2] -= 1;
+ gEnt->r.absmax[0] += 1;
+ gEnt->r.absmax[1] += 1;
+ gEnt->r.absmax[2] += 1;
+
+ // link to PVS leafs
+ ent->numClusters = 0;
+ ent->lastCluster = 0;
+ ent->areanum = -1;
+ ent->areanum2 = -1;
+
+ //get all leafs, including solids
+ num_leafs = CM_BoxLeafnums( gEnt->r.absmin, gEnt->r.absmax,
+ leafs, MAX_TOTAL_ENT_LEAFS, &lastLeaf );
+
+ // if none of the leafs were inside the map, the
+ // entity is outside the world and can be considered unlinked
+ if ( !num_leafs ) {
+ return;
+ }
+
+ // set areas, even from clusters that don't fit in the entity array
+ for (i=0 ; i<num_leafs ; i++) {
+ area = CM_LeafArea (leafs[i]);
+ if (area != -1) {
+ // doors may legally straggle two areas,
+ // but nothing should evern need more than that
+ if (ent->areanum != -1 && ent->areanum != area) {
+ if (ent->areanum2 != -1 && ent->areanum2 != area && sv.state == SS_LOADING) {
+ Com_DPrintf ("Object %i touching 3 areas at %f %f %f\n",
+ gEnt->s.number,
+ gEnt->r.absmin[0], gEnt->r.absmin[1], gEnt->r.absmin[2]);
+ }
+ ent->areanum2 = area;
+ } else {
+ ent->areanum = area;
+ }
+ }
+ }
+
+ // store as many explicit clusters as we can
+ ent->numClusters = 0;
+ for (i=0 ; i < num_leafs ; i++) {
+ cluster = CM_LeafCluster( leafs[i] );
+ if ( cluster != -1 ) {
+ ent->clusternums[ent->numClusters++] = cluster;
+ if ( ent->numClusters == MAX_ENT_CLUSTERS ) {
+ break;
+ }
+ }
+ }
+
+ // store off a last cluster if we need to
+ if ( i != num_leafs ) {
+ ent->lastCluster = CM_LeafCluster( lastLeaf );
+ }
+
+ gEnt->r.linkcount++;
+
+ // find the first world sector node that the ent's box crosses
+ node = sv_worldSectors;
+ while (1)
+ {
+ if (node->axis == -1)
+ break;
+ if ( gEnt->r.absmin[node->axis] > node->dist)
+ node = node->children[0];
+ else if ( gEnt->r.absmax[node->axis] < node->dist)
+ node = node->children[1];
+ else
+ break; // crosses the node
+ }
+
+ // link it in
+ ent->worldSector = node;
+ ent->nextEntityInWorldSector = node->entities;
+ node->entities = ent;
+
+ gEnt->r.linked = qtrue;
+}
+
+/*
+============================================================================
+
+AREA QUERY
+
+Fills in a list of all entities who's absmin / absmax intersects the given
+bounds. This does NOT mean that they actually touch in the case of bmodels.
+============================================================================
+*/
+
+typedef struct {
+ const float *mins;
+ const float *maxs;
+ int *list;
+ int count, maxcount;
+} areaParms_t;
+
+
+/*
+====================
+SV_AreaEntities_r
+
+====================
+*/
+static void SV_AreaEntities_r( worldSector_t *node, areaParms_t *ap ) {
+ svEntity_t *check, *next;
+ sharedEntity_t *gcheck;
+ int count;
+
+ count = 0;
+
+ for ( check = node->entities ; check ; check = next ) {
+ next = check->nextEntityInWorldSector;
+
+ gcheck = SV_GEntityForSvEntity( check );
+
+ if ( gcheck->r.absmin[0] > ap->maxs[0]
+ || gcheck->r.absmin[1] > ap->maxs[1]
+ || gcheck->r.absmin[2] > ap->maxs[2]
+ || gcheck->r.absmax[0] < ap->mins[0]
+ || gcheck->r.absmax[1] < ap->mins[1]
+ || gcheck->r.absmax[2] < ap->mins[2]) {
+ continue;
+ }
+
+ if ( ap->count == ap->maxcount ) {
+ Com_Printf ("SV_AreaEntities: MAXCOUNT\n");
+ return;
+ }
+
+ ap->list[ap->count] = check - sv.svEntities;
+ ap->count++;
+ }
+
+ if (node->axis == -1) {
+ return; // terminal node
+ }
+
+ // recurse down both sides
+ if ( ap->maxs[node->axis] > node->dist ) {
+ SV_AreaEntities_r ( node->children[0], ap );
+ }
+ if ( ap->mins[node->axis] < node->dist ) {
+ SV_AreaEntities_r ( node->children[1], ap );
+ }
+}
+
+/*
+================
+SV_AreaEntities
+================
+*/
+int SV_AreaEntities( const vec3_t mins, const vec3_t maxs, int *entityList, int maxcount ) {
+ areaParms_t ap;
+
+ ap.mins = mins;
+ ap.maxs = maxs;
+ ap.list = entityList;
+ ap.count = 0;
+ ap.maxcount = maxcount;
+
+ SV_AreaEntities_r( sv_worldSectors, &ap );
+
+ return ap.count;
+}
+
+
+
+//===========================================================================
+
+
+typedef struct {
+ vec3_t boxmins, boxmaxs;// enclose the test object along entire move
+ const float *mins;
+ const float *maxs; // size of the moving object
+ const float *start;
+ vec3_t end;
+ trace_t trace;
+ int passEntityNum;
+ int contentmask;
+ int capsule;
+} moveclip_t;
+
+
+/*
+====================
+SV_ClipToEntity
+
+====================
+*/
+void SV_ClipToEntity( trace_t *trace, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int entityNum, int contentmask, int capsule ) {
+ sharedEntity_t *touch;
+ clipHandle_t clipHandle;
+ float *origin, *angles;
+
+ touch = SV_GentityNum( entityNum );
+
+ Com_Memset(trace, 0, sizeof(trace_t));
+
+ // if it doesn't have any brushes of a type we
+ // are looking for, ignore it
+ if ( ! ( contentmask & touch->r.contents ) ) {
+ trace->fraction = 1.0;
+ return;
+ }
+
+ // might intersect, so do an exact clip
+ clipHandle = SV_ClipHandleForEntity (touch);
+
+ origin = touch->r.currentOrigin;
+ angles = touch->r.currentAngles;
+
+ if ( !touch->r.bmodel ) {
+ angles = vec3_origin; // boxes don't rotate
+ }
+
+ CM_TransformedBoxTrace ( trace, (float *)start, (float *)end,
+ (float *)mins, (float *)maxs, clipHandle, contentmask,
+ origin, angles, capsule);
+
+ if ( trace->fraction < 1 ) {
+ trace->entityNum = touch->s.number;
+ }
+}
+
+
+/*
+====================
+SV_ClipMoveToEntities
+
+====================
+*/
+static void SV_ClipMoveToEntities( moveclip_t *clip ) {
+ int i, num;
+ int touchlist[MAX_GENTITIES];
+ sharedEntity_t *touch;
+ int passOwnerNum;
+ trace_t trace;
+ clipHandle_t clipHandle;
+ float *origin, *angles;
+
+ num = SV_AreaEntities( clip->boxmins, clip->boxmaxs, touchlist, MAX_GENTITIES);
+
+ if ( clip->passEntityNum != ENTITYNUM_NONE ) {
+ passOwnerNum = ( SV_GentityNum( clip->passEntityNum ) )->r.ownerNum;
+ if ( passOwnerNum == ENTITYNUM_NONE ) {
+ passOwnerNum = -1;
+ }
+ } else {
+ passOwnerNum = -1;
+ }
+
+ for ( i=0 ; i<num ; i++ ) {
+ if ( clip->trace.allsolid ) {
+ return;
+ }
+ touch = SV_GentityNum( touchlist[i] );
+
+ // see if we should ignore this entity
+ if ( clip->passEntityNum != ENTITYNUM_NONE ) {
+ if ( touchlist[i] == clip->passEntityNum ) {
+ continue; // don't clip against the pass entity
+ }
+ if ( touch->r.ownerNum == clip->passEntityNum ) {
+ continue; // don't clip against own missiles
+ }
+ if ( touch->r.ownerNum == passOwnerNum ) {
+ continue; // don't clip against other missiles from our owner
+ }
+ }
+
+ // if it doesn't have any brushes of a type we
+ // are looking for, ignore it
+ if ( ! ( clip->contentmask & touch->r.contents ) ) {
+ continue;
+ }
+
+ // might intersect, so do an exact clip
+ clipHandle = SV_ClipHandleForEntity (touch);
+
+ origin = touch->r.currentOrigin;
+ angles = touch->r.currentAngles;
+
+
+ if ( !touch->r.bmodel ) {
+ angles = vec3_origin; // boxes don't rotate
+ }
+
+ CM_TransformedBoxTrace ( &trace, (float *)clip->start, (float *)clip->end,
+ (float *)clip->mins, (float *)clip->maxs, clipHandle, clip->contentmask,
+ origin, angles, clip->capsule);
+
+ if ( trace.allsolid ) {
+ clip->trace.allsolid = qtrue;
+ trace.entityNum = touch->s.number;
+ } else if ( trace.startsolid ) {
+ clip->trace.startsolid = qtrue;
+ trace.entityNum = touch->s.number;
+ }
+
+ if ( trace.fraction < clip->trace.fraction ) {
+ qboolean oldStart;
+
+ // make sure we keep a startsolid from a previous trace
+ oldStart = clip->trace.startsolid;
+
+ trace.entityNum = touch->s.number;
+ clip->trace = trace;
+ clip->trace.startsolid |= oldStart;
+ }
+ }
+}
+
+
+/*
+==================
+SV_Trace
+
+Moves the given mins/maxs volume through the world from start to end.
+passEntityNum and entities owned by passEntityNum are explicitly not checked.
+==================
+*/
+void SV_Trace( trace_t *results, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask, int capsule ) {
+ moveclip_t clip;
+ int i;
+
+ if ( !mins ) {
+ mins = vec3_origin;
+ }
+ if ( !maxs ) {
+ maxs = vec3_origin;
+ }
+
+ Com_Memset ( &clip, 0, sizeof ( moveclip_t ) );
+
+ // clip to world
+ CM_BoxTrace( &clip.trace, start, end, mins, maxs, 0, contentmask, capsule );
+ clip.trace.entityNum = clip.trace.fraction != 1.0 ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
+ if ( clip.trace.fraction == 0 ) {
+ *results = clip.trace;
+ return; // blocked immediately by the world
+ }
+
+ clip.contentmask = contentmask;
+ clip.start = start;
+// VectorCopy( clip.trace.endpos, clip.end );
+ VectorCopy( end, clip.end );
+ clip.mins = mins;
+ clip.maxs = maxs;
+ clip.passEntityNum = passEntityNum;
+ clip.capsule = capsule;
+
+ // create the bounding box of the entire move
+ // we can limit it to the part of the move not
+ // already clipped off by the world, which can be
+ // a significant savings for line of sight and shot traces
+ for ( i=0 ; i<3 ; i++ ) {
+ if ( end[i] > start[i] ) {
+ clip.boxmins[i] = clip.start[i] + clip.mins[i] - 1;
+ clip.boxmaxs[i] = clip.end[i] + clip.maxs[i] + 1;
+ } else {
+ clip.boxmins[i] = clip.end[i] + clip.mins[i] - 1;
+ clip.boxmaxs[i] = clip.start[i] + clip.maxs[i] + 1;
+ }
+ }
+
+ // clip to other solid entities
+ SV_ClipMoveToEntities ( &clip );
+
+ *results = clip.trace;
+}
+
+
+
+/*
+=============
+SV_PointContents
+=============
+*/
+int SV_PointContents( const vec3_t p, int passEntityNum ) {
+ int touch[MAX_GENTITIES];
+ sharedEntity_t *hit;
+ int i, num;
+ int contents, c2;
+ clipHandle_t clipHandle;
+ float *angles;
+
+ // get base contents from world
+ contents = CM_PointContents( p, 0 );
+
+ // or in contents from all the other entities
+ num = SV_AreaEntities( p, p, touch, MAX_GENTITIES );
+
+ for ( i=0 ; i<num ; i++ ) {
+ if ( touch[i] == passEntityNum ) {
+ continue;
+ }
+ hit = SV_GentityNum( touch[i] );
+ // might intersect, so do an exact clip
+ clipHandle = SV_ClipHandleForEntity( hit );
+ angles = hit->s.angles;
+ if ( !hit->r.bmodel ) {
+ angles = vec3_origin; // boxes don't rotate
+ }
+
+ c2 = CM_TransformedPointContents (p, clipHandle, hit->s.origin, hit->s.angles);
+
+ contents |= c2;
+ }
+
+ return contents;
+}
+
+