summaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorIronClawTrem <louie.nutman@gmail.com>2020-02-16 03:40:06 +0000
committerIronClawTrem <louie.nutman@gmail.com>2020-02-16 03:40:06 +0000
commit425decdf7e9284d15aa726e3ae96b9942fb0e3ea (patch)
tree6c0dd7edfefff1be7b9e75fe0b3a0a85fe1595f3 /src/server
parentccb0b2e4d6674a7a00c9bf491f08fc73b6898c54 (diff)
create tremded branch
Diffstat (limited to 'src/server')
-rw-r--r--src/server/CMakeLists.txt114
-rw-r--r--src/server/server.h529
-rw-r--r--src/server/sv_admin.cpp0
-rw-r--r--src/server/sv_admin.h61
-rw-r--r--src/server/sv_ccmds.cpp441
-rw-r--r--src/server/sv_client.cpp1949
-rw-r--r--src/server/sv_game.cpp602
-rw-r--r--src/server/sv_init.cpp1004
-rw-r--r--src/server/sv_main.cpp1551
-rw-r--r--src/server/sv_net_chan.cpp259
-rw-r--r--src/server/sv_snapshot.cpp749
-rw-r--r--src/server/sv_world.cpp745
12 files changed, 8004 insertions, 0 deletions
diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt
new file mode 100644
index 0000000..98f3a23
--- /dev/null
+++ b/src/server/CMakeLists.txt
@@ -0,0 +1,114 @@
+
+#
+## .dMMMb dMMMMMP dMMMMb dMP dMP dMMMMMP dMMMMb
+## dMP" VP dMP dMP.dMP dMP dMP dMP dMP.dMP
+## VMMMb dMMMP dMMMMK" dMP dMP dMMMP dMMMMK"
+## dP .dMP dMP dMP"AMF YMvAP" dMP dMP"AMF
+## VMMMP" dMMMMMP dMP dMP VP" dMMMMMP dMP dMP
+#
+
+add_definitions(
+ -DDEDICATED
+ -DUSE_LOCAL_HEADERS
+ -DPRODUCT_VERSION="1.2.0 pre-release"
+ -DUSE_VOIP
+ -DNDEBUG
+ )
+
+set(EXTERNAL_DIR ${CMAKE_SOURCE_DIR}/external)
+set(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..)
+if(APPLE)
+set(APPLE_SOURCES ${PARENT_DIR}/sys/sys_osx.mm)
+endif(APPLE)
+
+add_executable(
+ tremded
+ #
+ server.h
+ #
+ sv_ccmds.cpp
+ sv_client.cpp
+ sv_game.cpp
+ sv_init.cpp
+ sv_main.cpp
+ sv_net_chan.cpp
+ sv_snapshot.cpp
+ sv_world.cpp
+ #
+ ${PARENT_DIR}/qcommon/cm_load.cpp
+ ${PARENT_DIR}/qcommon/cm_patch.cpp
+ ${PARENT_DIR}/qcommon/cm_polylib.cpp
+ ${PARENT_DIR}/qcommon/cm_test.cpp
+ ${PARENT_DIR}/qcommon/cm_trace.cpp
+ ${PARENT_DIR}/qcommon/cmd.cpp
+ ${PARENT_DIR}/qcommon/common.cpp
+ ${PARENT_DIR}/qcommon/crypto.cpp
+ ${PARENT_DIR}/qcommon/cvar.cpp
+ ${PARENT_DIR}/qcommon/files.cpp
+ ${PARENT_DIR}/qcommon/huffman.cpp
+ ${PARENT_DIR}/qcommon/huffman.h
+ ${PARENT_DIR}/qcommon/ioapi.cpp
+ ${PARENT_DIR}/qcommon/md4.cpp
+ ${PARENT_DIR}/qcommon/msg.h
+ ${PARENT_DIR}/qcommon/msg.cpp
+ ${PARENT_DIR}/qcommon/net.h
+ ${PARENT_DIR}/qcommon/net_chan.cpp
+ ${PARENT_DIR}/qcommon/net_ip.cpp
+ ${PARENT_DIR}/qcommon/parse.cpp
+ ${PARENT_DIR}/qcommon/q3_lauxlib.cpp
+ ${PARENT_DIR}/qcommon/q_math.c
+ ${PARENT_DIR}/qcommon/q_shared.c
+ ${PARENT_DIR}/qcommon/unzip.cpp
+ ${PARENT_DIR}/qcommon/vm.cpp
+ ${PARENT_DIR}/qcommon/vm_interpreted.cpp
+ ${PARENT_DIR}/qcommon/vm_x86.cpp
+ #
+ ${PARENT_DIR}/null/null_client.cpp
+ ${PARENT_DIR}/null/null_input.cpp
+ ${PARENT_DIR}/null/null_snddma.cpp
+ #
+ ${PARENT_DIR}/asm/snapvector.c
+ #
+ ${PARENT_DIR}/sys/con_log.cpp
+ ${PARENT_DIR}/sys/con_tty.cpp
+ ${PARENT_DIR}/sys/sys_main.cpp
+ ${PARENT_DIR}/sys/sys_unix.cpp
+ ${PARENT_DIR}/sys/sys_shared.h
+ ${APPLE_SOURCES}
+ #
+ ${EXTERNAL_DIR}/zlib/adler32.c
+ ${EXTERNAL_DIR}/zlib/crc32.c
+ ${EXTERNAL_DIR}/zlib/inffast.c
+ ${EXTERNAL_DIR}/zlib/inflate.c
+ ${EXTERNAL_DIR}/zlib/inftrees.c
+ ${EXTERNAL_DIR}/zlib/zutil.c
+ )
+
+if(APPLE)
+ # FIXME Prefixed with "lua" to prevent cmake from doing "-l-framework Cocoa"
+ set(FRAMEWORKS "-framework Cocoa -framework Security -framework OpenAL -framework IOKit")
+else(APPLE)
+ if(UNIX)
+ set(SYSLIBS dl rt)
+ endif(UNIX)
+endif(APPLE)
+
+target_link_libraries(
+ tremded
+ #
+ lua
+ script_api
+ nettle
+ zlib
+ ${FRAMEWORKS}
+ ${SYSLIBS}
+ )
+
+include_directories(
+ ${PARENT_DIR}/script
+ ${EXTERNAL_DIR}/lua-5.3.3/include
+ ${EXTERNAL_DIR}/sol
+ ${EXTERNAL_DIR}/script/rapidjson
+ ${EXTERNAL_DIR}/nettle-3.3
+ ${EXTERNAL_DIR}/zlib
+ )
diff --git a/src/server/server.h b/src/server/server.h
new file mode 100644
index 0000000..a07b66c
--- /dev/null
+++ b/src/server/server.h
@@ -0,0 +1,529 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+// server.h
+
+#ifndef SERVER_H
+#define SERVER_H 1
+
+#include "game/g_public.h"
+#include "qcommon/cmd.h"
+#include "qcommon/crypto.h"
+#include "qcommon/cvar.h"
+#include "qcommon/files.h"
+#include "qcommon/huffman.h"
+#include "qcommon/msg.h"
+#include "qcommon/net.h"
+#include "qcommon/q_shared.h"
+#include "qcommon/qcommon.h"
+#include "qcommon/vm.h"
+#include "sys/sys_shared.h"
+
+//=============================================================================
+
+#define PERS_SCORE 0 // !!! MUST NOT CHANGE, SERVER AND GAME BOTH REFERENCE !!!
+#define CS_WARMUP 5 // !!! MUST NOT CHANGE, SERVER AND GAME BOTH REFERENCE !!!
+
+// server attack protection
+#define SVP_IOQ3 0x0001 ///< 1 - ioQuake3 way
+#define SVP_OWOLF 0x0002 ///< 2 - OpenWolf way
+#define SVP_CONSOLE 0x0004 ///< 4 - console print
+
+#define MAX_ENT_CLUSTERS 16
+
+#ifdef USE_VOIP
+#define VOIP_QUEUE_LENGTH 64
+struct voipServerPacket_t {
+ int generation;
+ int sequence;
+ int frames;
+ int len;
+ int sender;
+ int flags;
+ byte data[4000];
+};
+#endif // USE_VOIP
+
+struct svEntity_t {
+ struct worldSector_t *worldSector;
+ svEntity_t *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
+};
+
+enum serverState_t {
+ SS_DEAD, // no map loaded
+ SS_LOADING, // spawning level entities
+ SS_GAME // actively running
+};
+
+struct configString_t {
+ char *s;
+ bool restricted; // if true, don't send to clientList
+ clientList_t clientList;
+};
+
+struct server_t {
+ serverState_t state;
+ bool 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
+ configString_t 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;
+
+ vm_t *gvm; // game virtual machine
+};
+
+struct clientSnapshot_t {
+ 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
+};
+
+enum clientState_t {
+ 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
+};
+
+struct netchan_buffer_t {
+ msg_t msg;
+ byte msgBuffer[MAX_MSGLEN];
+ netchan_buffer_t *next;
+};
+
+struct client_t {
+ clientState_t state;
+ char userinfo[MAX_INFO_STRING]; // name, etc
+ char userinfobuffer[MAX_INFO_STRING]; ///< used for buffering of user info
+
+ 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];
+ bool 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 nextReliableUserTime; // svs.time when another userinfo change will be allowed
+ int lastPacketTime; // svs.time when packet was last received
+ int lastConnectTime; // svs.time when connection started
+ int lastSnapshotTime; // svs.time of last sent snapshot
+ bool 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;
+ bool 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;
+
+ char fingerprint[SHA256_DIGEST_SIZE * 2 + 1];
+
+#ifdef USE_VOIP
+ bool hasVoip;
+ bool muteAllVoip;
+ bool ignoreVoipFromClient[MAX_CLIENTS];
+ voipServerPacket_t *voipPacket[VOIP_QUEUE_LENGTH];
+ size_t queuedVoipPackets;
+ int queuedVoipIndex;
+#endif
+
+ int oldServerTime;
+ bool csUpdated[MAX_CONFIGSTRINGS];
+};
+
+//=============================================================================
+#define STATFRAMES 200 ///< 5 seconds - assumed we run 40 fps
+
+/**
+ * @struct svstats_t
+ * @brief
+ */
+struct svstats_t {
+ double active;
+ double idle;
+ int count;
+
+ double latched_active;
+ double latched_idle;
+
+ float cpu;
+ float avg;
+};
+
+// 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 2048
+// Allow a certain amount of challenges to have the same IP address
+// to make it a bit harder to DOS one single IP address from connecting
+// while not allowing a single ip to grab all challenge resources
+#define MAX_CHALLENGES_MULTI (MAX_CHALLENGES / 2)
+
+#define AUTHORIZE_TIMEOUT 5000
+
+struct challenge_t {
+ netadr_t adr;
+ int challenge;
+ char challenge2[33];
+ 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
+ bool wasrefused;
+ bool connected;
+};
+
+/**
+ * @struct receipt_t
+ * @brief
+ */
+struct receipt_t {
+ netadr_t adr;
+ int time;
+};
+
+/**
+ * @def MAX_INFO_RECEIPTS
+ * @brief the maximum number of getstatus+getinfo responses that we send in
+ * a two second time period.
+ */
+#define MAX_INFO_RECEIPTS 48
+
+/**
+ * @struct tempBan_s
+ * @typedef tempBan_t
+ * @brief
+ */
+struct tempBan_t {
+ netadr_t adr;
+ int endtime;
+};
+
+#define MAX_TEMPBAN_ADDRESSES MAX_CLIENTS
+
+#define SERVER_PERFORMANCECOUNTER_FRAMES 600
+#define SERVER_PERFORMANCECOUNTER_SAMPLES 6
+
+// this structure will be cleared only when the game dll changes
+struct serverStatic_t {
+ bool 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_SNAPSHOT_ENTITIES
+ int nextSnapshotEntities; // next snapshotEntities to use
+ entityState_t *snapshotEntities; // [numSnapshotEntities]
+ int nextHeartbeatTime;
+ challenge_t challenges[MAX_CHALLENGES]; // to prevent invalid IPs from connecting
+ receipt_t infoReceipts[MAX_INFO_RECEIPTS];
+ netadr_t redirectAddress; // for rcon return messages
+
+ netadr_t authorizeAddress; // for rcon return messages
+
+ int sampleTimes[SERVER_PERFORMANCECOUNTER_SAMPLES];
+ int currentSampleIndex;
+ int totalFrameTime;
+ int currentFrameIndex;
+ int serverLoad;
+ svstats_t stats;
+};
+
+//=============================================================================
+
+extern serverStatic_t svs; // persistant server info across maps
+extern server_t sv; // cleared each map
+
+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_masters[3][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_dlRate;
+extern cvar_t *sv_minPing;
+extern cvar_t *sv_maxPing;
+extern cvar_t *sv_pure;
+extern cvar_t *sv_lanForceRate;
+extern cvar_t *sv_banFile;
+
+extern cvar_t *sv_protect;
+extern cvar_t *sv_protectLog;
+
+#ifdef USE_VOIP
+extern cvar_t *sv_voip;
+extern cvar_t *sv_voipProtocol;
+#endif
+
+extern cvar_t *sv_rsaAuth;
+
+extern cvar_t *sv_schachtmeisterPort;
+
+//===========================================================
+
+//
+// sv_main.c
+//
+struct leakyBucket_t {
+ netadrtype_t type;
+
+ union {
+ byte _4[4];
+ byte _6[16];
+ } ipv;
+
+ int lastTime;
+ signed char burst;
+
+ long hash;
+
+ leakyBucket_t *prev, *next;
+};
+
+extern leakyBucket_t outboundLeakyBucket;
+
+bool SVC_RateLimit(leakyBucket_t *bucket, int burst, int period);
+bool SVC_RateLimitAddress(netadr_t from, int burst, int period);
+
+void SV_FinalMessage(const char *message);
+void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+
+void SV_AddOperatorCommands(void);
+void SV_RemoveOperatorCommands(void);
+
+void SV_MasterShutdown(void);
+int SV_RateMsec(client_t *client);
+
+//
+// sv_init.c
+//
+void SV_SetConfigstring(int index, const char *val);
+void SV_GetConfigstring(int index, char *buffer, int bufferSize);
+void SV_SetConfigstringRestrictions(int index, const clientList_t *clientList);
+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);
+void SV_WriteAttackLog(const char *log);
+
+#ifdef NDEBUG
+#define SV_WriteAttackLogD(x)
+#else
+#define SV_WriteAttackLogD(x) SV_WriteAttackLog(x)
+#endif
+
+//
+// sv_client.c
+//
+void SV_GetChallenge(netadr_t from);
+
+void SV_DirectConnect(netadr_t from);
+
+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_FreeClient(client_t *client);
+void SV_DropClient(client_t *drop, const char *reason);
+
+void SV_ExecuteClientCommand(client_t *cl, const char *s, bool clientOK);
+void SV_ClientThink(client_t *cl, usercmd_t *cmd);
+
+int SV_WriteDownloadToClient(client_t *cl, msg_t *msg);
+int SV_SendDownloadMessages(void);
+int SV_SendQueuedMessages(void);
+
+//
+// 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);
+bool SV_inPVS(const vec3_t p1, const vec3_t p2);
+
+//============================================================
+//
+// 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->r.absmin and ent->r.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, traceType_t type);
+// 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, traceType_t type);
+// clip to a specific entity
+
+//
+// sv_net_chan.c
+//
+void SV_Netchan_Transmit(client_t *client, msg_t *msg);
+int SV_Netchan_TransmitNextFragment(client_t *client);
+bool SV_Netchan_Process(client_t *client, msg_t *msg);
+void SV_Netchan_FreeQueue(client_t *client);
+
+#endif
diff --git a/src/server/sv_admin.cpp b/src/server/sv_admin.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/server/sv_admin.cpp
diff --git a/src/server/sv_admin.h b/src/server/sv_admin.h
new file mode 100644
index 0000000..b261457
--- /dev/null
+++ b/src/server/sv_admin.h
@@ -0,0 +1,61 @@
+//
+// This file is part of Tremulous.
+// Copyright © 2017 Victor Roemer (blowfish) <victor@badsec.org>
+// Copyright (C) 2015-2019 GrangerHub
+//
+// This program 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 3 of the License, or
+// (at your option) any later version.
+//
+// This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
+//
+
+#pragma once
+
+#include <iostream>
+
+using std::string;
+
+typedef char fingerprint_t[64];
+typedef char guid_t[33];
+typedef char name_t[MAX_NAME_LENGTH];
+typedef char err_t[MAX_STRING_CHARS];
+
+struct AdminFlag {
+ unsigned id;
+ name_t name;
+};
+
+struct AdminLevel {
+ name_t name;
+ admin_flags_t flags;
+ unsigned level;
+};
+
+struct Admin {
+ bool flag(const name_t flagname)
+ { }
+
+ bool deny(const name_t flagname)
+ { }
+
+ guid_t guid;
+ name_t name;
+
+private:
+ admin_flags_t flags;
+ admin_flags_t denied;
+ unsigned level;
+};
+
+class AdminMgr {
+ bool add(Admin&);
+ bool remove(Admin&);
+}
diff --git a/src/server/sv_ccmds.cpp b/src/server/sv_ccmds.cpp
new file mode 100644
index 0000000..5c8902d
--- /dev/null
+++ b/src/server/sv_ccmds.cpp
@@ -0,0 +1,441 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+
+#include <arpa/inet.h>
+#include "server.h"
+
+/*
+===============================================================================
+
+OPERATOR CONSOLE ONLY COMMANDS
+
+These commands can only be entered from stdin or by a remote operator datagram
+===============================================================================
+*/
+
+/*
+==================
+SV_Map_f
+
+Restart the server on a different map
+==================
+*/
+static void SV_Map_f( void ) {
+ const char *cmd;
+ const char *map;
+ bool cheat;
+ char expanded[MAX_QPATH];
+ char mapname[MAX_QPATH];
+ int a;
+ int i;
+
+ 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;
+ }
+
+ cmd = Cmd_Argv(0);
+ if ( !Q_stricmp( cmd, "devmap" ) ) {
+ cheat = true;
+ } else {
+ cheat = false;
+ }
+
+ // save the map name here cause on a map restart we reload the autogen.cfg
+ // and thus nuke the arguments of the map command
+ Q_strncpyz(mapname, map, sizeof(mapname));
+
+ // start up the map
+ SV_SpawnServer( mapname );
+
+ // 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" );
+ }
+
+ // This forces the local master server IP address cache
+ // to be updated on sending the next heartbeat
+ for( a = 0; a < 3; ++a )
+ for( i = 0; i < MAX_MASTER_SERVERS; i++ )
+ sv_masters[ a ][ i ]->modified = true;
+}
+
+/*
+================
+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;
+ 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 = 0;
+ }
+ 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 ) {
+ 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 );
+ 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 = true;
+
+ SV_RestartGameProgs();
+
+ // run a few frames to allow everything to settle
+ for (i = 0; i < 3; i++)
+ {
+ VM_Call (sv.gvm, GAME_RUN_FRAME, sv.time);
+ sv.time += 100;
+ svs.time += 100;
+ }
+
+ sv.state = SS_GAME;
+ sv.restarting = false;
+
+ // 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;
+ }
+
+ // add the map_restart command
+ SV_AddServerCommand( client, "map_restart\n" );
+
+ // connect the client again, without the firstTime flag
+ denied = (char*)VM_ExplicitArgPtr( sv.gvm, VM_Call( sv.gvm, GAME_CLIENT_CONNECT, i, false ) );
+ 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;
+ }
+
+ if(client->state == CS_ACTIVE)
+ SV_ClientEnterWorld(client, &client->lastUsercmd);
+ else
+ {
+ // If we don't reset client->lastUsercmd and are restarting during map load,
+ // the client will hang because we'll use the last Usercmd from the previous map,
+ // which is wrong obviously.
+ SV_ClientEnterWorld(client, NULL);
+ }
+ }
+
+ // run another frame to allow things to look at all the players
+ VM_Call (sv.gvm, GAME_RUN_FRAME, sv.time);
+ sv.time += 100;
+ svs.time += 100;
+}
+
+
+//===============================================================
+
+/**
+ * @brief SV_Status_f
+ */
+static void SV_Status_f(void) {
+ int i;
+ client_t *cl;
+ playerState_t *ps;
+ const char *s;
+ int ping;
+ unsigned int maxNameLength;
+
+ // make sure server is running
+ if (!com_sv_running->integer) {
+ Com_Printf("Server is not running.\n");
+ return;
+ }
+
+ Com_Printf("cpu server utilization: %i %%\n"
+ "avg response time : %i ms\n"
+ "server time : %i\n"
+ "internal time : %i\n"
+ "map : %s\n\n"
+ "num score ping name lastmsg address qport rate lastConnectTime\n"
+ "--- ----- ---- ----------------------------------- ------- --------------------- ----- ----- ---------------\n",
+ ( int ) svs.stats.cpu,
+ ( int ) svs.stats.avg,
+ svs.time,
+ Sys_Milliseconds(),
+ sv_mapname->string);
+
+ for (i = 0, cl = svs.clients ; i < sv_maxclients->integer ; i++, cl++) {
+ 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);
+ }
+
+ s = NET_AdrToString(cl->netchan.remoteAddress);
+
+ // extend the name length by couting extra color characters to keep well formated output
+ maxNameLength = sizeof(cl->name) + (strlen(cl->name) - Q_PrintStrlen(cl->name)) + 1;
+
+ Com_Printf("%-*s %7i %-21s %5i %5i %i\n", maxNameLength, rc(cl->name), svs.time - cl->lastPacketTime, s, cl->netchan.qport, cl->rate, svs.time - cl->lastConnectTime);
+ }
+
+ Com_Printf("\n");
+}
+
+
+/*
+==================
+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 ) {
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ Com_Printf ("Server info settings:\n");
+ Info_Print ( Cvar_InfoString( CVAR_SERVERINFO ) );
+}
+
+
+/*
+===========
+SV_Systeminfo_f
+
+Examine the systeminfo string
+===========
+*/
+static void SV_Systeminfo_f( void ) {
+ // make sure server is running
+ if ( !com_sv_running->integer ) {
+ Com_Printf( "Server is not running.\n" );
+ return;
+ }
+
+ Com_Printf ("System info settings:\n");
+ Info_Print ( Cvar_InfoString_Big( CVAR_SYSTEMINFO ) );
+}
+
+
+/*
+=================
+SV_KillServer
+=================
+*/
+static void SV_KillServer_f( void ) {
+ SV_Shutdown( "killserver" );
+}
+
+static void SV_SMQ_f( void ) {
+ static qboolean schmResolved = qfalse;
+ static netadr_t schmAddress;
+
+ if ( !schmResolved ) {
+ schmResolved = qtrue;
+ NET_StringToAdr( "127.0.0.1", &schmAddress, NA_IP );
+ schmAddress.port = 1337;
+ }
+
+ if ( sv_schachtmeisterPort->modified &&
+ sv_schachtmeisterPort->integer >= 1 && sv_schachtmeisterPort->integer <= 65535 )
+ {
+ schmAddress.port = htons(sv_schachtmeisterPort->integer);
+ }
+
+ if ( Cmd_Argc() >= 3 && !Q_stricmp( Cmd_Argv( 1 ), "ipa" ) ) { // compatibility with out-of-date crapware conceived in the future
+ NET_OutOfBandPrint( NS_SERVER, schmAddress, "sm2query %s", Cmd_ArgsFrom( 2 ) );
+ Com_Printf( "^3query [^7sm2query %s^3]\n", Cmd_ArgsFrom( 2 ) ); // DELME
+ } else {
+ char args[ MAX_STRING_CHARS ];
+ char *p;
+ int s, i;
+
+ p = args;
+ s = sizeof( args );
+
+ for ( i = 1; i < Cmd_Argc(); ++i )
+ {
+ int l;
+ Com_sprintf( p, s, " \"%s\"", Cmd_Argv( i ) );
+ l = strlen( p );
+ s -= l;
+ p += l;
+ }
+
+ NET_OutOfBandPrint( NS_SERVER, schmAddress, "sm2query%s", args );
+ Com_Printf( "^3query [^7sm2query%s^3]\n", args ); // DELME
+ }
+}
+
+//===========================================================
+
+/*
+==================
+SV_CompleteMapName
+==================
+*/
+static void SV_CompleteMapName( char *args, int argNum ) {
+ if( argNum == 2 ) {
+ Field_CompleteFilename( "maps", "bsp", true, false );
+ }
+}
+
+/*
+==================
+SV_AddOperatorCommands
+==================
+*/
+void SV_AddOperatorCommands( void ) {
+ static bool initialized = false;
+
+ if ( initialized ) {
+ return;
+ }
+ initialized = true;
+
+ Cmd_AddCommand ("heartbeat", SV_Heartbeat_f);
+ Cmd_AddCommand ("status", SV_Status_f);
+ Cmd_AddCommand ("serverinfo", SV_Serverinfo_f);
+ Cmd_AddCommand ("systeminfo", SV_Systeminfo_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 );
+ Cmd_AddCommand ("devmap", SV_Map_f);
+ Cmd_SetCommandCompletionFunc( "devmap", SV_CompleteMapName );
+ Cmd_AddCommand ("killserver", SV_KillServer_f);
+ Cmd_AddCommand ("smq", SV_SMQ_f);
+}
+
+/*
+==================
+SV_RemoveOperatorCommands
+==================
+*/
+void SV_RemoveOperatorCommands( void ) {
+#if 0
+ // removing these won't let the server start again
+ Cmd_RemoveCommand ("heartbeat");
+ Cmd_RemoveCommand ("serverinfo");
+ Cmd_RemoveCommand ("systeminfo");
+ Cmd_RemoveCommand ("map_restart");
+ Cmd_RemoveCommand ("sectorlist");
+#endif
+}
diff --git a/src/server/sv_client.cpp b/src/server/sv_client.cpp
new file mode 100644
index 0000000..0a54a32
--- /dev/null
+++ b/src/server/sv_client.cpp
@@ -0,0 +1,1949 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+// sv_client.c -- server code for dealing with clients
+
+#include "server.h"
+
+static void SV_CloseDownload( client_t *cl );
+
+/*
+=================
+SV_RSA_VerifySignature
+
+Verifies the signature of data and on success it returns the sha256
+fingerprint of the public key. pubkey, signature, and fingerprint
+are all base 16 encoded strings.
+
+Returns true on success
+=================
+*/
+static bool SV_RSA_VerifySignature( const char *pubkey, const char *signature, const char *data, char *fingerprint )
+{
+ struct rsa_public_key public_key;
+ struct sha256_ctx sha256_hash;
+ uint8_t buf[RSA_STRING_LENGTH];
+ int err;
+ mpz_t n;
+
+ if ( !*pubkey || !*signature )
+ return false;
+
+ // load public key
+ rsa_public_key_init( &public_key );
+ mpz_set_ui( public_key.e, RSA_PUBLIC_EXPONENT );
+ err = mpz_set_str( public_key.n, pubkey, 16 );
+ if ( err ) {
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ err = !rsa_public_key_prepare( &public_key );
+ if ( err ) {
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ // load signature
+ mpz_init( n );
+ err = mpz_set_str( n, signature, 16 );
+ if ( err ) {
+ mpz_clear( n );
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ // hash data
+ sha256_init( &sha256_hash );
+ sha256_update( &sha256_hash, strlen(data), (uint8_t *) data);
+
+ if ( !rsa_sha256_verify( &public_key, &sha256_hash, n ) ) {
+ mpz_clear( n );
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ // VERIFIED, save the sha256 fingerprint of the key
+ nettle_mpz_get_str_256( sizeof(buf), buf, public_key.n );
+
+ sha256_update( &sha256_hash, sizeof(buf), buf );
+ sha256_digest( &sha256_hash, SHA256_DIGEST_SIZE, buf );
+
+ nettle_mpz_set_str_256_u( n, SHA256_DIGEST_SIZE, buf );
+ mpz_get_str( fingerprint, 16, n );
+
+ mpz_clear( n );
+ rsa_public_key_clear( &public_key );
+ return true;
+}
+
+/*
+=================
+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;
+ int oldestClientTime;
+ int clientChallenge;
+ challenge_t *challenge;
+ bool wasfound = false;
+ byte buf[16];
+ mpz_t n;
+
+ if (sv_protect->integer & SVP_IOQ3)
+ if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
+ {
+ Com_DPrintf( "SV_GetChallenge: rate limit from %s exceeded, dropping request\n",
+ // Prevent using getchallenge as an amplifier
+ NET_AdrToString( from ) );
+ if (SVC_RateLimitAddress(from, 10, 1000))
+ return;
+ {
+ }
+ SV_WriteAttackLog(va("SV_GetChallenge: rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString(from)));
+ return;
+ }
+
+
+ // Allow getchallenge to be DoSed relatively easily, but prevent
+ // Allow getchallenge to be DoSed relatively easily, but prevent
+ // excess outbound bandwidth usage when being flooded inbound
+ // excess outbound bandwidth usage when being flooded inbound
+ if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) {
+ if (SVC_RateLimit(&outboundLeakyBucket, 10, 100))
+ Com_DPrintf( "SV_GetChallenge: rate limit exceeded, dropping request\n" );
+ {
+ return;
+ SV_WriteAttackLog("SV_GetChallenge: rate limit exceeded, dropping request\n");
+ return;
+ }
+ }
+ }
+
+ oldest = 0;
+ oldestClientTime = oldestTime = 0x7fffffff;
+
+ // see if we already have a challenge for this ip
+ challenge = &svs.challenges[0];
+ clientChallenge = atoi(Cmd_Argv(1));
+
+ for(i = 0 ; i < MAX_CHALLENGES ; i++, challenge++)
+ {
+ if(!challenge->connected && NET_CompareAdr(from, challenge->adr))
+ {
+ wasfound = true;
+
+ if(challenge->time < oldestClientTime)
+ oldestClientTime = challenge->time;
+ }
+
+ if(wasfound && i >= MAX_CHALLENGES_MULTI)
+ {
+ i = MAX_CHALLENGES;
+ 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 = clientChallenge;
+ challenge->adr = from;
+ challenge->firstTime = svs.time;
+ challenge->connected = false;
+
+ if ( sv_rsaAuth->integer ) {
+ Sys_CryptoRandomBytes( buf, sizeof(buf) );
+ nettle_mpz_init_set_str_256_u( n, sizeof(buf), buf );
+ mpz_get_str( challenge->challenge2, 16, n );
+ mpz_clear( n );
+ }
+ }
+
+ // always generate a new challenge number, so the client cannot circumvent sv_maxping
+ challenge->challenge = ( (rand() << 16) ^ rand() ) ^ svs.time;
+ challenge->wasrefused = false;
+ challenge->time = svs.time;
+ challenge->pingTime = svs.time;
+
+ if ( sv_rsaAuth->integer ) {
+ NET_OutOfBandPrint( NS_SERVER, challenge->adr, "challengeResponse %d %d %d %s",
+ challenge->challenge, clientChallenge, PROTOCOL_VERSION, challenge->challenge2 );
+ }
+ else {
+ NET_OutOfBandPrint( NS_SERVER, challenge->adr, "challengeResponse %d %d %d",
+ challenge->challenge, clientChallenge, PROTOCOL_VERSION );
+ }
+}
+
+/*
+==================
+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;
+ const char *ip;
+ char *challenge2;
+ bool challenge2Verified = false;
+
+ Com_DPrintf ("SVC_DirectConnect ()\n");
+
+ // Prevent using connect as an amplifier
+ if (sv_protect->integer & SVP_IOQ3)
+ {
+ if(SVC_RateLimitAddress(from, 10, 1000))
+ {
+ SV_WriteAttackLog(va("Bad direct connect - rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString(from)));
+ return;
+ }
+ }
+
+ Q_strncpyz( userinfo, Cmd_Argv(1), sizeof(userinfo) );
+
+ version = atoi( Info_ValueForKey( userinfo, "protocol" ) );
+ if ( version != PROTOCOL_VERSION && version != 70 && version != 69 ) {
+ NET_OutOfBandPrint(NS_SERVER, from, "print\nServer uses either protocol version %i, 70 or 69 "
+ "(yours is %i).\n", PROTOCOL_VERSION, 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;
+ }
+
+ if ( sv_rsaAuth->integer )
+ {
+ challenge2 = Info_ValueForKey( userinfo, "challenge2" );
+ if ( !Q_stricmp( challenge2, svs.challenges[i].challenge2 ) )
+ {
+ challenge2Verified = true;
+ }
+ }
+
+ 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 = true;
+ 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 = true;
+ return;
+ }
+ }
+
+ Com_Printf("Client %i connecting with %i challenge ping\n", i, ping);
+ challengeptr->connected = true;
+ }
+
+ // ignore any fingerprint set by the client
+ char fingerprint[SHA256_DIGEST_SIZE * 2 + 1];
+ Info_RemoveKey(userinfo, "fingerprint");
+ fingerprint[0] = '\0';
+
+ if ( sv_rsaAuth->integer && (NET_IsLocalAddress(from) || challenge2Verified) )
+ {
+ if ( SV_RSA_VerifySignature(Cmd_Argv(2), Cmd_Argv(3), Cmd_Argv(1), fingerprint) )
+ {
+ if( strlen(fingerprint) + strlen(userinfo) + 13 >= MAX_INFO_STRING )
+ {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nUserinfo string length exceeded.\n" );
+ return;
+ }
+ Info_SetValueForKey( userinfo, "fingerprint", fingerprint );
+ Com_DPrintf( "Public key fingerprint: %s\n", fingerprint );
+ }
+ }
+
+ newcl = &temp;
+ ::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( sv.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 ( *password && !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 ) ) {
+ Com_Error( ERR_FATAL, "server is full on local connect" );
+ 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;
+
+ Cvar_Set( va( "sv_clAltProto%i", clientNum ), ( version == 69 ? "2" : version == 70 ? "1" : "0" ) );
+
+ // save the challenge
+ newcl->challenge = challenge;
+
+ // save the address
+ Netchan_Setup((version == 69 ? 2 : version == 70 ? 1 : 0), NS_SERVER, &newcl->netchan, from, qport, challenge);
+ // init the netchan queue
+ newcl->netchan_end_queue = &newcl->netchan_start_queue;
+
+ // save the fingerprint
+ Q_strncpyz( newcl->fingerprint, fingerprint, sizeof(newcl->fingerprint) );
+
+ // 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( sv.gvm, GAME_CLIENT_CONNECT, clientNum, true ); // firstTime = true
+ if ( denied ) {
+ // we can't just use VM_ArgPtr, because that is only valid inside a VM_Call
+ char *str = (char*)VM_ExplicitArgPtr( sv.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 %d", challenge);
+
+ Com_DPrintf( "Going from CS_FREE to CS_CONNECTED for %s\n", newcl->name );
+
+ newcl->state = CS_CONNECTED;
+ newcl->lastSnapshotTime = 0;
+ 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_FreeClient
+
+Destructor for data allocated in a client structure
+=====================
+*/
+void SV_FreeClient(client_t *client)
+{
+#ifdef USE_VOIP
+ int index;
+
+ for(index = client->queuedVoipIndex; index < client->queuedVoipPackets; index++)
+ {
+ index %= ARRAY_LEN(client->voipPacket);
+
+ Z_Free(client->voipPacket[index]);
+ }
+
+ client->queuedVoipPackets = 0;
+#endif
+
+ SV_Netchan_FreeQueue(client);
+ SV_CloseDownload(client);
+}
+
+/*
+=====================
+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;
+
+ if ( drop->state == CS_ZOMBIE ) {
+ return; // already dropped
+ }
+
+ // 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 ) ) {
+ ::memset(challenge, 0, sizeof(*challenge));
+ break;
+ }
+ }
+
+ // Free all allocated data on the client structure
+ SV_FreeClient(drop);
+
+ // tell everyone why they got dropped
+ SV_SendServerCommand( NULL, "print \"%s" S_COLOR_WHITE " %s\n\"", drop->name, reason );
+
+ // call the prog function for removing a client
+ // this will remove the body, among other things
+ VM_Call( sv.gvm, GAME_CLIENT_DISCONNECT, drop - svs.clients );
+
+ // add the disconnect command
+ SV_SendServerCommand( drop, "disconnect \"%s\"", reason);
+
+ // nuke user info
+ SV_SetUserinfo( drop - svs.clients, "" );
+
+ 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();
+ }
+}
+
+extern char alternateInfos[2][2][BIG_INFO_STRING];
+
+/*
+================
+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];
+ const char *configstring;
+
+ 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 = false;
+
+ // 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 ( start <= CS_SYSTEMINFO && client->netchan.alternateProtocol != 0 ) {
+ configstring = alternateInfos[start][ client->netchan.alternateProtocol - 1 ];
+ } else {
+ configstring = sv.configstrings[start].s;
+ }
+
+ if (configstring[0]) {
+ MSG_WriteByte( &msg, svc_configstring );
+ MSG_WriteShort( &msg, start );
+ MSG_WriteBigString( &msg, configstring );
+ }
+ }
+
+ // write the baselines
+ ::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( client->netchan.alternateProtocol, &msg, &nullstate, base, true );
+ }
+
+ 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->lastSnapshotTime = 0; // generate a snapshot immediately
+
+ if(cmd)
+ memcpy(&client->lastUsercmd, cmd, sizeof(client->lastUsercmd));
+ else
+ memset(&client->lastUsercmd, '\0', sizeof(client->lastUsercmd));
+
+ // call the game begin function
+ VM_Call( sv.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 ) {
+ if ( cl->state == CS_ACTIVE )
+ return;
+
+ 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, return number of download blocks added
+==================
+*/
+int SV_WriteDownloadToClient(client_t *cl, msg_t *msg)
+{
+ int curindex;
+ int unreferenced = 1;
+ char errorMessage[1024];
+ char pakbuf[MAX_QPATH], *pakptr;
+ int numRefPaks;
+
+ if (!*cl->downloadName)
+ return 0; // Nothing being downloaded
+
+ if(!cl->download)
+ {
+ // Chop off filename extension.
+ Com_sprintf(pakbuf, sizeof(pakbuf), "%s", cl->downloadName);
+ pakptr = strrchr(pakbuf, '.');
+
+ if(pakptr)
+ {
+ *pakptr = '\0';
+
+ // Check for pk3 filename extension
+ if(!Q_stricmp(pakptr + 1, "pk3"))
+ {
+ const char *referencedPaks = FS_ReferencedPakNames( cl->netchan.alternateProtocol == 2 );
+
+ // 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;
+ break;
+ }
+ }
+ }
+ }
+
+ cl->download = 0;
+
+ // We open the file here
+ if ( !(sv_allowDownload->integer & DLF_ENABLE) ||
+ (sv_allowDownload->integer & DLF_NO_UDP) ||
+ 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 ( !(sv_allowDownload->integer & DLF_ENABLE) ||
+ (sv_allowDownload->integer & DLF_NO_UDP) ) {
+
+ Com_Printf("clientDownload: %d : \"%s\" download disabled\n", (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 1;
+ }
+
+ 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 = false;
+ }
+
+ // 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] = (unsigned char*)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 = true; // We have added the EOF block
+ }
+
+ if (cl->downloadClientBlock == cl->downloadCurrentBlock)
+ return 0; // Nothing to transmit
+
+ // Write out the next section of the file, if we have already reached our window,
+ // automatically start retransmitting
+ if (cl->downloadXmitBlock == cl->downloadCurrentBlock)
+ {
+ // We have transmitted the complete window, should we start resending?
+ if (svs.time - cl->downloadSendTime > 1000)
+ cl->downloadXmitBlock = cl->downloadClientBlock;
+ else
+ return 0;
+ }
+
+ // 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;
+
+ return 1;
+}
+
+/*
+==================
+SV_SendQueuedMessages
+
+Send one round of fragments, or queued messages to all clients that have data pending.
+Return the shortest time interval for sending next packet to client
+==================
+*/
+
+int SV_SendQueuedMessages(void)
+{
+ int i, retval = -1, nextFragT;
+ client_t *cl;
+
+ for(i=0; i < sv_maxclients->integer; i++)
+ {
+ cl = &svs.clients[i];
+
+ if(cl->state)
+ {
+ nextFragT = SV_RateMsec(cl);
+
+ if(!nextFragT)
+ nextFragT = SV_Netchan_TransmitNextFragment(cl);
+
+ if(nextFragT >= 0 && (retval == -1 || retval > nextFragT))
+ retval = nextFragT;
+ }
+ }
+
+ return retval;
+}
+
+
+/*
+==================
+SV_SendDownloadMessages
+
+Send one round of download messages to all clients
+==================
+*/
+
+int SV_SendDownloadMessages(void)
+{
+ int i, numDLs = 0, retval;
+ client_t *cl;
+ msg_t msg;
+ byte msgBuffer[MAX_MSGLEN];
+
+ for(i=0; i < sv_maxclients->integer; i++)
+ {
+ cl = &svs.clients[i];
+
+ if(cl->state && *cl->downloadName)
+ {
+ MSG_Init(&msg, msgBuffer, sizeof(msgBuffer));
+ MSG_WriteLong(&msg, cl->lastClientCommand);
+
+ retval = SV_WriteDownloadToClient(cl, &msg);
+
+ if(retval)
+ {
+ MSG_WriteByte(&msg, svc_EOF);
+ SV_Netchan_Transmit(cl, &msg);
+ numDLs += retval;
+ }
+ }
+ }
+
+ return numDLs;
+}
+
+/*
+=================
+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;
+ bool bGood = true;
+
+ // 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 ) {
+
+ nChkSum1 = nChkSum2 = 0;
+ // we run the game, so determine which cgame and ui the client "should" be running
+ bGood = (FS_FileIsInPAK_A((cl->netchan.alternateProtocol == 2), "vm/cgame.qvm", &nChkSum1) == 1);
+ if (bGood)
+ bGood = (FS_FileIsInPAK_A((cl->netchan.alternateProtocol == 2), "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 = false;
+ }
+ 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 = false;
+ break;
+ }
+ // verify first to be the cgame checksum
+ pArg = Cmd_Argv(nCurArg++);
+ if (!pArg || *pArg == '@' || atoi(pArg) != nChkSum1 ) {
+ bGood = false;
+ break;
+ }
+ // verify the second to be the ui checksum
+ pArg = Cmd_Argv(nCurArg++);
+ if (!pArg || *pArg == '@' || atoi(pArg) != nChkSum2 ) {
+ bGood = false;
+ break;
+ }
+ // should be sitting at the delimeter now
+ pArg = Cmd_Argv(nCurArg++);
+ if (*pArg != '@') {
+ bGood = false;
+ 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 = false;
+ break;
+ }
+ }
+ if (bGood == false)
+ break;
+ }
+ if (bGood == false)
+ break;
+
+ // get the pure checksums of the pk3 files loaded by the server
+ pPaks = FS_LoadedPakPureChecksums(cl->netchan.alternateProtocol == 2);
+ 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 = false;
+ break;
+ }
+ }
+ if ( bGood == false ) {
+ 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 = false;
+ break;
+ }
+
+ // break out
+ break;
+ }
+
+ cl->gotCP = true;
+
+ if (bGood) {
+ cl->pureAuthentic = 1;
+ }
+ else {
+ cl->pureAuthentic = 0;
+ cl->lastSnapshotTime = 0;
+ cl->state = CS_ACTIVE;
+ SV_SendClientSnapshot( cl );
+ SV_SendServerCommand( cl, "disconnect \"Unpure Client. "
+ "You may need to enable in-game downloads "
+ "to connect to this server (set "
+ "cl_allowDownload 1)\"" );
+ 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 = false;
+}
+
+/*
+=================
+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;
+ const 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;
+
+ i = 1000 / i;
+ }
+ else
+ i = 50;
+
+ if(i != cl->snapshotMsec)
+ {
+ // Reset last sent snapshot so we avoid desync between server frame time and snapshot send time
+ cl->lastSnapshotTime = 0;
+ cl->snapshotMsec = i;
+ }
+
+#ifdef USE_VOIP
+ val = Info_ValueForKey(cl->userinfo, "cl_voipProtocol");
+ cl->hasVoip = !Q_stricmp( val, "opus" );
+#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 );
+
+ val = Info_ValueForKey( cl->userinfo, "fingerprint" );
+ if( val[0] )
+ len = strlen(cl->fingerprint) - strlen(val) + strlen(cl->userinfo);
+ else
+ len = strlen(cl->fingerprint) + 13 + strlen(cl->userinfo);
+
+ if( len >= MAX_INFO_STRING )
+ SV_DropClient( cl, "userinfo string length exceeded" );
+ else
+ Info_SetValueForKey( cl->userinfo, "fingerprint", cl->fingerprint );
+}
+
+
+/*
+==================
+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( sv.gvm, GAME_CLIENT_USERINFO_CHANGED, cl - svs.clients );
+}
+
+
+#ifdef USE_VOIP
+static void SV_UpdateVoipIgnore(client_t *cl, const char *idstr, bool 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), true);
+ } else if (strcmp(cmd, "unignore") == 0) {
+ SV_UpdateVoipIgnore(cl, Cmd_Argv(2), false);
+ } else if (strcmp(cmd, "muteall") == 0) {
+ cl->muteAllVoip = true;
+ } else if (strcmp(cmd, "unmuteall") == 0) {
+ cl->muteAllVoip = false;
+ }
+}
+#endif
+
+
+typedef struct {
+ const 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, bool clientOK ) {
+ ucmd_t *u;
+ bool bProcessed = false;
+
+ 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 = true;
+ break;
+ }
+ }
+
+ if (clientOK) {
+ // pass unknown strings to the game
+ if (!u->name && sv.state == SS_GAME && (cl->state == CS_ACTIVE || cl->state == CS_PRIMED)) {
+ VM_Call( sv.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 bool SV_ClientCommand( client_t *cl, msg_t *msg ) {
+ int seq;
+ const char *s;
+ bool clientOk = true;
+
+ seq = MSG_ReadLong( msg );
+ s = MSG_ReadString( msg );
+
+ // see if we have already executed it
+ if ( cl->lastClientCommand >= seq ) {
+ return true;
+ }
+
+ 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 false;
+ }
+
+ // 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 it's
+ // normal to spam a lot of commands when downloading
+#if 0 // flood protection in game for trem
+ 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 = false;
+ }
+#endif
+
+ // 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 true; // 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( sv.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, bool 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->netchan.alternateProtocol, cl->reliableCommands[ cl->reliableAcknowledge & (MAX_RELIABLE_COMMANDS-1) ], 32);
+
+ ::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
+/*
+==================
+SV_ShouldIgnoreVoipSender
+
+Blocking of voip packets based on source client
+==================
+*/
+
+static bool SV_ShouldIgnoreVoipSender(const client_t *cl)
+{
+ if (!sv_voip->integer)
+ return true; // VoIP disabled on this server.
+ else if (!cl->hasVoip) // client doesn't have VoIP support?!
+ return true;
+
+ // !!! FIXME: implement player blacklist.
+
+ return false; // don't ignore.
+}
+
+static void SV_UserVoip(client_t *cl, msg_t *msg, bool ignoreData)
+{
+ int sender, generation, sequence, frames;
+ uint8_t recips[(MAX_CLIENTS + 7) / 8];
+ int recip1 = 0, recip2 = 0, recip3 = 0; // silence warning
+ int flags = 0;
+ byte encoded[sizeof(cl->voipPacket[0]->data)];
+ client_t *client = NULL;
+ voipServerPacket_t *packet = NULL;
+ int i;
+
+ sender = cl - svs.clients;
+ generation = MSG_ReadByte(msg);
+ sequence = MSG_ReadLong(msg);
+ frames = MSG_ReadByte(msg);
+ if (cl->netchan.alternateProtocol == 0) {
+ MSG_ReadData(msg, recips, sizeof(recips));
+ flags = MSG_ReadByte(msg);
+ } else {
+ recip1 = MSG_ReadLong(msg);
+ recip2 = MSG_ReadLong(msg);
+ recip3 = MSG_ReadLong(msg);
+ }
+ size_t packetsize = MSG_ReadShort(msg);
+
+ if (msg->readcount > msg->cursize)
+ return; // short/invalid packet, bail.
+
+ if (packetsize > sizeof(encoded)) { // overlarge packet?
+ size_t bytesleft = packetsize;
+ while (bytesleft) {
+ size_t 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 (ignoreData || SV_ShouldIgnoreVoipSender(cl))
+ return; // Blacklisted, disabled, etc.
+
+ // !!! FIXME: see if we read past end of msg...
+
+ // !!! FIXME: reject if not opus data.
+ // !!! FIXME: decide if this is bogus data?
+
+ // 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 unsupported protocol
+ 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.
+
+ if (cl->netchan.alternateProtocol == 0) {
+ if(Com_IsVoipTarget(recips, sizeof(recips), i))
+ flags |= VOIP_DIRECT;
+ else
+ flags &= ~VOIP_DIRECT;
+ } else {
+ if (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 && (recip3 & (1 << (i - 62))) == 0)
+ continue; // not addressed to this player.
+
+ flags |= VOIP_DIRECT;
+ }
+
+ if (!(flags & (VOIP_SPATIAL | VOIP_DIRECT)))
+ continue; // not addressed to this player.
+
+ // Transmit this packet to the client.
+ if (client->queuedVoipPackets >= ARRAY_LEN(client->voipPacket)) {
+ Com_Printf("Too many VoIP packets queued for client #%d\n", i);
+ continue; // no room for another packet right now.
+ }
+
+ packet = (voipServerPacket_t*)Z_Malloc(sizeof(*packet));
+ packet->sender = sender;
+ packet->frames = frames;
+ packet->len = packetsize;
+ packet->generation = generation;
+ packet->sequence = sequence;
+ packet->flags = flags;
+ memcpy(packet->data, encoded, packetsize);
+
+ client->voipPacket[(client->queuedVoipIndex + client->queuedVoipPackets) % ARRAY_LEN(client->voipPacket)] = packet;
+ 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 );
+
+ if ( cl->netchan.alternateProtocol != 0 ) {
+ // 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_voipSpeex ) {
+ MSG_ReadByte( msg ); // throw the clc_extension byte away.
+ c = MSG_ReadByte( msg ); // something legacy servers can't do!
+ if ( c == clc_voipSpeex + 1 ) {
+ c = clc_voipSpeex;
+ }
+ // 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 == svc_voipSpeex ) {
+ c = svc_voipSpeex + 1;
+ } else if ( c == svc_voipSpeex + 1 ) {
+ c = svc_voipSpeex;
+ }
+ }
+
+ 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 );
+
+ // skip legacy speex voip data
+ if ( c == clc_voipSpeex ) {
+#ifdef USE_VOIP
+ SV_UserVoip( cl, msg, true );
+ c = MSG_ReadByte( msg );
+#endif
+ }
+
+ // read optional voip data
+ if ( c == clc_voipOpus ) {
+#ifdef USE_VOIP
+ SV_UserVoip( cl, msg, false );
+ c = MSG_ReadByte( msg );
+#endif
+ }
+
+ // read the usercmd_t
+ if ( c == clc_move ) {
+ SV_UserMove( cl, msg, true );
+ } else if ( c == clc_moveNoDelta ) {
+ SV_UserMove( cl, msg, false );
+ } 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/src/server/sv_game.cpp b/src/server/sv_game.cpp
new file mode 100644
index 0000000..23e5212
--- /dev/null
+++ b/src/server/sv_game.cpp
@@ -0,0 +1,602 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+// sv_game.c -- interface to the game dll
+
+#include "server.h"
+
+// 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
+=================
+*/
+bool 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 false;
+
+ if (!CM_AreasConnected (area1, area2))
+ return false; // a door blocks sight
+
+ return true;
+}
+
+
+/*
+=================
+SV_inPVSIgnorePortals
+
+Does NOT check portalareas
+=================
+*/
+bool 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 false;
+
+ return true;
+}
+
+
+/*
+========================
+SV_AdjustAreaPortalState
+========================
+*/
+void SV_AdjustAreaPortalState( sharedEntity_t *ent, bool open ) {
+ svEntity_t *svEnt;
+
+ svEnt = SV_SvEntityForGentity( ent );
+ if ( svEnt->areanum2 == -1 ) {
+ return;
+ }
+ CM_AdjustAreaPortalState( svEnt->areanum, svEnt->areanum2, open );
+}
+
+
+/*
+==================
+SV_EntityContact
+==================
+*/
+bool SV_EntityContact( vec3_t mins, vec3_t maxs, const sharedEntity_t *gEnt, traceType_t type ) {
+ 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, type );
+
+ 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( (vmCvar_t*)VMA(1), (const char*)VMA(2), (const char*)VMA(3), args[4] );
+ return 0;
+ case G_CVAR_UPDATE:
+ Cvar_Update( (vmCvar_t*)VMA(1) );
+ return 0;
+ case G_CVAR_SET:
+ Cvar_SetSafe( (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( (const char*)VMA(1), (char*)VMA(2), args[3] );
+ return 0;
+ case G_ARGC:
+ return Cmd_Argc();
+ case G_ARGV:
+ Cmd_ArgvBuffer( args[1], (char*)VMA(2), args[3] );
+ return 0;
+ case G_SEND_CONSOLE_COMMAND:
+ Cbuf_ExecuteText( args[1], (const char*)VMA(2) );
+ return 0;
+
+ case G_FS_FOPEN_FILE:
+ return FS_FOpenFileByMode( (const char*)VMA(1), (fileHandle_t*)VMA(2), (FS_Mode)args[3] );
+ case G_FS_READ:
+ FS_Read( 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( (const char*)VMA(1), (const char*)VMA(2), (char*)VMA(3), args[4] );
+ case G_FS_GETFILTEREDFILES:
+ return FS_GetFilteredFiles( (const char*)VMA(1), (const char*)VMA(2), (char*)VMA(3), (char*)VMA(4), args[5] );
+ case G_FS_SEEK:
+ return FS_Seek( args[1], args[2], (FS_Origin)args[3] );
+ case G_LOCATE_GAME_DATA:
+ SV_LocateGameData( (sharedEntity_t*)VMA(1), args[2], args[3], (playerState_t*)VMA(4), args[5] );
+ return 0;
+ case G_DROP_CLIENT:
+ SV_GameDropClient( args[1], (const char*)VMA(2) );
+ return 0;
+ case G_SEND_SERVER_COMMAND:
+ SV_GameSendServerCommand( args[1], (const char*)VMA(2) );
+ return 0;
+ case G_LINKENTITY:
+ SV_LinkEntity( (sharedEntity_t*)VMA(1) );
+ return 0;
+ case G_UNLINKENTITY:
+ SV_UnlinkEntity( (sharedEntity_t*)VMA(1) );
+ return 0;
+ case G_ENTITIES_IN_BOX:
+ return SV_AreaEntities( (const vec_t*)VMA(1), (const vec_t*)VMA(2), (int*)VMA(3), args[4] );
+ case G_ENTITY_CONTACT:
+ return SV_EntityContact( (vec_t*)VMA(1), (vec_t*)VMA(2), (const sharedEntity_t*)VMA(3), TT_AABB );
+ case G_ENTITY_CONTACTCAPSULE:
+ return SV_EntityContact( (vec_t*)VMA(1), (vec_t*)VMA(2), (const sharedEntity_t*)VMA(3), TT_CAPSULE );
+ case G_TRACE:
+ SV_Trace( (trace_t*)VMA(1), (const vec_t*)VMA(2), (vec_t*)VMA(3), (vec_t*)VMA(4), (const vec_t*)VMA(5), args[6], args[7], TT_AABB );
+ return 0;
+ case G_TRACECAPSULE:
+ SV_Trace( (trace_t*)VMA(1), (const vec_t*)VMA(2), (vec_t*)VMA(3), (vec_t*)VMA(4), (const vec_t*)VMA(5), args[6], args[7], TT_CAPSULE );
+ return 0;
+ case G_POINT_CONTENTS:
+ return SV_PointContents( (const vec_t*)VMA(1), args[2] );
+ case G_SET_BRUSH_MODEL:
+ SV_SetBrushModel( (sharedEntity_t*)VMA(1), (const char*)VMA(2) );
+ return 0;
+ case G_IN_PVS:
+ return SV_inPVS( (const vec_t*)VMA(1), (const vec_t*)VMA(2) );
+ case G_IN_PVS_IGNORE_PORTALS:
+ return SV_inPVSIgnorePortals( (const vec_t*)VMA(1), (const vec_t*)VMA(2) );
+
+ case G_SET_CONFIGSTRING:
+ SV_SetConfigstring( args[1], (const char*)VMA(2) );
+ return 0;
+ case G_GET_CONFIGSTRING:
+ SV_GetConfigstring( args[1], (char*)VMA(2), args[3] );
+ return 0;
+ case G_SET_CONFIGSTRING_RESTRICTIONS:
+ SV_SetConfigstringRestrictions( args[1], (clientList_t*)VMA(2) );
+ return 0;
+ case G_SET_USERINFO:
+ SV_SetUserinfo( args[1], (const char*)VMA(2) );
+ return 0;
+ case G_GET_USERINFO:
+ SV_GetUserinfo( args[1], (char*)VMA(2), args[3] );
+ return 0;
+ case G_GET_SERVERINFO:
+ SV_GetServerinfo( (char*)VMA(1), args[2] );
+ return 0;
+ case G_ADJUST_AREA_PORTAL_STATE:
+ SV_AdjustAreaPortalState( (sharedEntity_t*)VMA(1), (bool)args[2] );
+ return 0;
+ case G_AREAS_CONNECTED:
+ return CM_AreasConnected( args[1], args[2] );
+
+ case G_GET_USERCMD:
+ SV_GetUsercmd( args[1], (usercmd_t*)VMA(2) );
+ return 0;
+ case G_GET_ENTITY_TOKEN:
+ {
+ const char *s;
+
+ s = COM_Parse( &sv.entityParsePoint );
+ Q_strncpyz( (char*)VMA(1), s, args[2] );
+ if ( !sv.entityParsePoint && !s[0] ) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ case G_REAL_TIME:
+ return Com_RealTime( (qtime_t*)VMA(1) );
+ case G_SNAPVECTOR:
+ Q_SnapVector( (vec_t*)VMA(1) );
+ return 0;
+
+ case G_SEND_GAMESTAT:
+ return 0;
+
+ //====================================
+
+ case G_PARSE_ADD_GLOBAL_DEFINE:
+ return Parse_AddGlobalDefine( (char*)VMA(1) );
+ case G_PARSE_LOAD_SOURCE:
+ return Parse_LoadSourceHandle( (const char*)VMA(1) );
+ case G_PARSE_FREE_SOURCE:
+ return Parse_FreeSourceHandle( args[1] );
+ case G_PARSE_READ_TOKEN:
+ return Parse_ReadTokenHandle( args[1], (pc_token_t*)VMA(2) );
+ case G_PARSE_SOURCE_FILE_AND_LINE:
+ return Parse_SourceFileAndLine( args[1], (char*)VMA(2), (int*)VMA(3) );
+
+ case G_ADDCOMMAND:
+ Cmd_AddCommand( (const char*)VMA(1), NULL );
+ return 0;
+ case G_REMOVECOMMAND:
+ Cmd_RemoveCommand( (const char*)VMA(1) );
+ return 0;
+
+ case TRAP_MEMSET:
+ ::memset( VMA(1), args[2], args[3] );
+ return 0;
+
+ case TRAP_MEMCPY:
+ ::memcpy( VMA(1), VMA(2), args[3] );
+ return 0;
+
+ case TRAP_STRNCPY:
+ ::strncpy( (char*)VMA(1), (const char*)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:
+ {
+ // XXX C++ is made this annoying
+ float (&in1)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(1));
+ float (&in2)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(2));
+ float (&in3)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(3));
+ MatrixMultiply( in1, in2, in3 );
+ return 0;
+ }
+
+ case TRAP_ANGLEVECTORS:
+ AngleVectors( (const vec_t*)VMA(1), (vec_t*)VMA(2), (vec_t*)VMA(3), (vec_t*)VMA(4) );
+ return 0;
+
+ case TRAP_PERPENDICULARVECTOR:
+ PerpendicularVector( (vec_t*)VMA(1), (const vec_t*)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 0;
+}
+
+/*
+===============
+SV_ShutdownGameProgs
+
+Called every time a map changes
+===============
+*/
+void SV_ShutdownGameProgs( void ) {
+ if ( !sv.gvm ) {
+ return;
+ }
+ VM_Call( sv.gvm, GAME_SHUTDOWN, false );
+ VM_Free( sv.gvm );
+ sv.gvm = NULL;
+}
+
+/*
+==================
+SV_InitGameVM
+
+Called for both a full init and a restart
+==================
+*/
+static void SV_InitGameVM( bool 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 (sv.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 ( !sv.gvm ) {
+ return;
+ }
+ VM_Call( sv.gvm, GAME_SHUTDOWN, true );
+
+ // do a restart instead of a free
+ sv.gvm = VM_Restart(sv.gvm, true);
+ if ( !sv.gvm ) {
+ Com_Error( ERR_FATAL, "VM_Restart on game failed" );
+ }
+
+ SV_InitGameVM( true );
+}
+
+
+/*
+===============
+SV_InitGameProgs
+
+Called on a normal map change, not on a map_restart
+===============
+*/
+void SV_InitGameProgs( void ) {
+ // load the dll or bytecode
+ sv.gvm = VM_Create( "game", SV_GameSystemCalls, (vmInterpret_t)Cvar_VariableValue( "vm_game" ) );
+ if ( !sv.gvm ) {
+ Com_Error( ERR_FATAL, "VM_Create on game failed" );
+ }
+
+ SV_InitGameVM( false );
+}
+
+
+/*
+====================
+SV_GameCommand
+
+See if the current console command is claimed by the game
+====================
+*/
+bool SV_GameCommand( void ) {
+ if ( sv.state != SS_GAME ) {
+ return false;
+ }
+
+ return (bool)VM_Call( sv.gvm, GAME_CONSOLE_COMMAND );
+}
diff --git a/src/server/sv_init.cpp b/src/server/sv_init.cpp
new file mode 100644
index 0000000..8c7729e
--- /dev/null
+++ b/src/server/sv_init.cpp
@@ -0,0 +1,1004 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+
+#include "server.h"
+
+#include "qcommon/cvar.h"
+
+// Attack log file is started when server is init (!= sv_running 1!)
+// we even log attacks when the server is waiting for rcon and doesn't run a map
+int attHandle = 0; // server attack log file handle
+
+char alternateInfos[2][2][BIG_INFO_STRING];
+
+/*
+===============
+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 i)
+{
+ const char *configstring;
+ int maxChunkSize = MAX_STRING_CHARS - 24;
+ int len;
+
+ if (sv.configstrings[i].restricted &&
+ Com_ClientListContains(&sv.configstrings[i].clientList, client - svs.clients))
+ {
+ // Send a blank config string for this client if it's listed
+ SV_SendServerCommand(client, "cs %i \"\"\n", i);
+ return;
+ }
+
+ if (i <= CS_SYSTEMINFO && client->netchan.alternateProtocol != 0)
+ {
+ configstring = alternateInfos[i][client->netchan.alternateProtocol - 1];
+ }
+ else
+ {
+ configstring = sv.configstrings[i].s;
+ }
+
+ len = strlen(configstring);
+
+ if (len >= maxChunkSize)
+ {
+ int sent = 0;
+ int remaining = len;
+ const 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, &configstring[sent], maxChunkSize);
+
+ SV_SendServerCommand(client, "%s %i \"%s\"\n", cmd, i, buf);
+
+ sent += (maxChunkSize - 1);
+ remaining -= (maxChunkSize - 1);
+ }
+ }
+ else
+ {
+ // standard cs, just send it
+ SV_SendServerCommand(client, "cs %i \"%s\"\n", i, configstring);
+ }
+}
+
+/*
+===============
+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)
+{
+ for (int i = 0; i < MAX_CONFIGSTRINGS; i++)
+ {
+ // if the CS hasn't changed since we went to CS_PRIMED, ignore
+ if (!client->csUpdated[i]) continue;
+
+ // do not always send server info to all clients
+ if (i == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO))
+ {
+ continue;
+ }
+
+ SV_SendConfigstring(client, i);
+ client->csUpdated[i] = false;
+ }
+}
+
+/*
+===============
+SV_SetConfigstring
+
+===============
+*/
+void SV_SetConfigstring(int idx, const char *val)
+{
+ bool modified[3] = {false, false, false};
+ int i;
+ client_t *client;
+
+ if (idx < 0 || idx >= MAX_CONFIGSTRINGS)
+ {
+ Com_Error(ERR_DROP, "SV_SetConfigstring: bad idx %i", idx);
+ }
+
+ if (!val)
+ {
+ val = "";
+ }
+
+ if (idx <= CS_SYSTEMINFO)
+ {
+ for (i = 1; i < 3; ++i)
+ {
+ char info[BIG_INFO_STRING];
+
+ Q_strncpyz(info, val, sizeof(info));
+ if (idx == CS_SERVERINFO)
+ {
+ Info_SetValueForKey_Big(info, "protocol", (i == 1 ? "70" : "69"));
+ }
+ else if (i == 2)
+ {
+ Info_SetValueForKey_Big(info, "sv_paks", Cvar_VariableString("sv_alternatePaks"));
+ Info_SetValueForKey_Big(info, "sv_pakNames", Cvar_VariableString("sv_alternatePakNames"));
+ Info_SetValueForKey_Big(info, "sv_referencedPaks", Cvar_VariableString("sv_referencedAlternatePaks"));
+ Info_SetValueForKey_Big(
+ info, "sv_referencedPakNames", Cvar_VariableString("sv_referencedAlternatePakNames"));
+ Info_SetValueForKey_Big(info, "cl_allowDownload", "1, you should set it yourself");
+ if (!(sv_allowDownload->integer & DLF_NO_REDIRECT))
+ {
+ Info_SetValueForKey_Big(info, "sv_wwwBaseURL", Cvar_VariableString("sv_dlUrl"));
+ Info_SetValueForKey_Big(
+ info, "sv_wwwDownload", Cvar_VariableString("1, you should set it yourself"));
+ }
+ }
+
+ if (strcmp(info, alternateInfos[idx][i - 1]))
+ {
+ modified[i] = true;
+ strcpy(alternateInfos[idx][i - 1], info);
+ }
+ }
+
+ if (strcmp(val, sv.configstrings[idx].s))
+ {
+ modified[0] = true;
+ Z_Free(sv.configstrings[idx].s);
+ sv.configstrings[idx].s = CopyString(val);
+ }
+
+ if (!modified[0] && !modified[1] && !modified[2])
+ {
+ return;
+ }
+ }
+ else
+ {
+ // don't bother broadcasting an update if no change
+ if (!strcmp(val, sv.configstrings[idx].s))
+ {
+ return;
+ }
+
+ // change the string in sv
+ Z_Free(sv.configstrings[idx].s);
+ sv.configstrings[idx].s = 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 (idx <= CS_SYSTEMINFO && !modified[client->netchan.alternateProtocol])
+ {
+ continue;
+ }
+
+ if (client->state < CS_ACTIVE)
+ {
+ if (client->state == CS_PRIMED) client->csUpdated[idx] = true;
+ continue;
+ }
+ // do not always send server info to all clients
+ if (idx == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO))
+ {
+ continue;
+ }
+
+ SV_SendConfigstring(client, idx);
+ }
+ }
+}
+
+/*
+===============
+SV_GetConfigstring
+
+===============
+*/
+void SV_GetConfigstring(int idx, char *buffer, int bufferSize)
+{
+ if (bufferSize < 1)
+ {
+ Com_Error(ERR_DROP, "SV_GetConfigstring: bufferSize == %i", bufferSize);
+ }
+ if (idx < 0 || idx >= MAX_CONFIGSTRINGS)
+ {
+ Com_Error(ERR_DROP, "SV_GetConfigstring: bad idx %i", idx);
+ }
+ if (!sv.configstrings[idx].s)
+ {
+ buffer[0] = 0;
+ return;
+ }
+
+ Q_strncpyz(buffer, sv.configstrings[idx].s, bufferSize);
+}
+
+/*
+===============
+SV_SetConfigstringRestrictions
+===============
+*/
+void SV_SetConfigstringRestrictions(int idx, const clientList_t *clientList)
+{
+ int i;
+ clientList_t oldClientList = sv.configstrings[idx].clientList;
+
+ sv.configstrings[idx].clientList = *clientList;
+ sv.configstrings[idx].restricted = true;
+
+ for (i = 0; i < sv_maxclients->integer; i++)
+ {
+ if (svs.clients[i].state >= CS_CONNECTED)
+ {
+ if (Com_ClientListContains(&oldClientList, i) != Com_ClientListContains(clientList, i))
+ {
+ // A client has left or joined the restricted list, so update
+ SV_SendConfigstring(&svs.clients[i], idx);
+ }
+ }
+ }
+}
+
+/*
+===============
+SV_SetUserinfo
+
+===============
+*/
+void SV_SetUserinfo(int idx, const char *val)
+{
+ if (idx < 0 || idx >= sv_maxclients->integer)
+ {
+ Com_Error(ERR_DROP, "SV_SetUserinfo: bad idx %i", idx);
+ }
+
+ if (!val)
+ {
+ val = "";
+ }
+
+ Q_strncpyz(svs.clients[idx].userinfo, val, sizeof(svs.clients[idx].userinfo));
+ Q_strncpyz(svs.clients[idx].name, Info_ValueForKey(val, "name"), sizeof(svs.clients[idx].name));
+}
+
+/*
+===============
+SV_GetUserinfo
+
+===============
+*/
+void SV_GetUserinfo(int idx, char *buffer, int bufferSize)
+{
+ if (bufferSize < 1)
+ {
+ Com_Error(ERR_DROP, "SV_GetUserinfo: bufferSize == %i", bufferSize);
+ }
+ if (idx < 0 || idx >= sv_maxclients->integer)
+ {
+ Com_Error(ERR_DROP, "SV_GetUserinfo: bad idx %i", idx);
+ }
+ Q_strncpyz(buffer, svs.clients[idx].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 = false;
+
+ 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 = (client_t *)Z_Malloc(sizeof(client_t) * sv_maxclients->integer);
+ if (com_dedicated->integer)
+ {
+ svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * MAX_SNAPSHOT_ENTITIES;
+ }
+ else
+ {
+ // we don't need nearly as many when playing locally
+ svs.numSnapshotEntities = sv_maxclients->integer * 4 * MAX_SNAPSHOT_ENTITIES;
+ }
+ svs.initialized = true;
+
+ // 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 = (client_t *)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
+ {
+ ::memset(&oldClients[i], 0, sizeof(client_t));
+ }
+ }
+
+ // free old clients arrays
+ Z_Free(svs.clients);
+
+ // allocate new clients
+ svs.clients = (client_t *)Z_Malloc(sv_maxclients->integer * sizeof(client_t));
+ ::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 * MAX_SNAPSHOT_ENTITIES;
+ }
+ else
+ {
+ // we don't need nearly as many when playing locally
+ svs.numSnapshotEntities = sv_maxclients->integer * 4 * MAX_SNAPSHOT_ENTITIES;
+ }
+}
+
+/*
+================
+SV_ClearServer
+================
+*/
+static void SV_ClearServer(void)
+{
+ int i;
+
+ for (i = 0; i < MAX_CONFIGSTRINGS; i++)
+ {
+ if (i <= CS_SYSTEMINFO)
+ {
+ alternateInfos[i][0][0] = alternateInfos[i][1][0] = '\0';
+ }
+ if (sv.configstrings[i].s)
+ {
+ Z_Free(sv.configstrings[i].s);
+ }
+ }
+ ::memset(&sv, 0, sizeof(sv));
+}
+
+/*
+================
+SV_TouchCGame
+
+Touch the cgame.qvm 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, false);
+ 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)
+{
+ int i;
+ int checksum;
+ 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(false);
+
+ // clear the whole hunk because we're (re)loading the server
+ Hunk_Clear();
+
+ // clear collision map data
+ CM_ClearMap();
+
+ // init client structures and svs.numSnapshotEntities
+ if (!Cvar_VariableValue("sv_running"))
+ {
+ SV_Startup();
+ }
+ else
+ {
+ // check for maxclients change
+ if (sv_maxclients->modified)
+ {
+ SV_ChangeMaxClients();
+ }
+ }
+
+ // clear pak references
+ FS_ClearPakReferences(0);
+
+ // allocate the snapshot entities on the hunk
+ svs.snapshotEntities = (entityState_t *)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;
+
+ 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++)
+ {
+ if (i <= CS_SYSTEMINFO)
+ {
+ alternateInfos[i][0][0] = alternateInfos[i][1][0] = '\0';
+ }
+ sv.configstrings[i].s = CopyString("");
+ sv.configstrings[i].restricted = false;
+ ::memset(&sv.configstrings[i].clientList, 0, sizeof(clientList_t));
+ }
+
+ // 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);
+
+ // advertise GPP-compatible extensions
+ Cvar_Set("sv_gppExtension", "1");
+
+ CM_LoadMap(va("maps/%s.bsp", server), false, &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();
+
+ // run a few frames to allow everything to settle
+ for (i = 0; i < 3; i++)
+ {
+ VM_Call(sv.gvm, GAME_RUN_FRAME, 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;
+
+ // connect the client again
+ denied =
+ (char *)VM_ExplicitArgPtr(sv.gvm, VM_Call(sv.gvm, GAME_CLIENT_CONNECT, i, false)); // firstTime = false
+ if (denied)
+ {
+ // this generally shouldn't happen, because the client
+ // was connected before the level change
+ SV_DropClient(&svs.clients[i], denied);
+ }
+ else
+ {
+ // when we get the next packet from a connected client,
+ // the new gamestate will be sent
+ svs.clients[i].state = CS_CONNECTED;
+ }
+ }
+ }
+
+ // run another frame to allow things to look at all the players
+ VM_Call(sv.gvm, GAME_RUN_FRAME, 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(false);
+ Cvar_Set("sv_paks", p);
+ p = FS_LoadedPakChecksums(true);
+ Cvar_Set("sv_alternatePaks", p);
+ if (strlen(p) == 0)
+ {
+ Com_Printf("WARNING: sv_pure set but no PK3 files loaded\n");
+ }
+ p = FS_LoadedPakNames(false);
+ Cvar_Set("sv_pakNames", p);
+ p = FS_LoadedPakNames(true);
+ Cvar_Set("sv_alternatePakNames", 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", "");
+ Cvar_Set("sv_alternatePaks", "");
+ Cvar_Set("sv_alternatePakNames", "");
+ }
+ // the server sends these to the clients so they can figure
+ // out which pk3s should be auto-downloaded
+ p = FS_ReferencedPakChecksums(false);
+ Cvar_Set("sv_referencedPaks", p);
+ p = FS_ReferencedPakChecksums(true);
+ Cvar_Set("sv_referencedAlternatePaks", p);
+ p = FS_ReferencedPakNames(false);
+ Cvar_Set("sv_referencedPakNames", p);
+ p = FS_ReferencedPakNames(true);
+ Cvar_Set("sv_referencedAlternatePakNames", 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();
+
+#ifndef DEDICATED
+ if (com_dedicated->integer)
+ {
+ // restart renderer in order to show console for dedicated servers
+ // launched through the regular binary
+ CL_StartHunkUsers(true);
+ }
+#endif
+
+ Com_Printf("-----------------------------------\n");
+}
+
+/**
+ * @brief SV_WriteAttackLog
+ * @param[in] log
+ */
+void SV_WriteAttackLog(const char *log)
+{
+ if (attHandle > 0)
+ {
+ char string[512]; // 512 chars seem enough here
+ qtime_t time;
+
+ Com_RealTime(&time);
+ Com_sprintf(string, sizeof(string), "%i/%i/%i %i:%i:%i %s", 1900 + time.tm_year, time.tm_mday, time.tm_mon + 1, time.tm_hour, time.tm_min, time.tm_sec, log);
+ (void) FS_Write(string, strlen(string), attHandle);
+ }
+
+ if (sv_protect->integer & SVP_CONSOLE)
+ {
+ Com_Printf("%s", log);
+ }
+}
+
+/**
+ * @brief SV_InitAttackLog
+ */
+void SV_InitAttackLog()
+{
+ if (sv_protectLog->string[0] == '\0')
+ {
+ Com_Printf("Not logging server attacks to disk.\n");
+ }
+ else
+ {
+ // in sync so admins can check this at runtime
+ FS_FOpenFileByMode(sv_protectLog->string, &attHandle, FS_APPEND_SYNC);
+
+ if (attHandle <= 0)
+ {
+ Com_Printf("WARNING: Couldn't open server attack logfile %s\n", sv_protectLog->string);
+ }
+ else
+ {
+ Com_Printf("Logging server attacks to %s\n", sv_protectLog->string);
+ SV_WriteAttackLog("-------------------------------------------------------------------------------\n");
+ SV_WriteAttackLog("Start server attack log\n");
+ SV_WriteAttackLog("-------------------------------------------------------------------------------\n");
+ }
+ }
+}
+
+/**
+ * @brief SV_CloseAttackLog
+ */
+void SV_CloseAttackLog()
+{
+ if (attHandle > 0)
+ {
+ SV_WriteAttackLog("-------------------------------------------------------------------------------\n");
+ SV_WriteAttackLog("End server attack log\n");
+ SV_WriteAttackLog("-------------------------------------------------------------------------------\n");
+ Com_Printf("Server attack log closed \n");
+ }
+
+ FS_FCloseFile(attHandle);
+
+ attHandle = 0; // local handle
+}
+
+/*
+===============
+SV_Init
+
+Only called at main exe startup, not for each game
+===============
+*/
+void SV_Init(void)
+{
+ SV_AddOperatorCommands();
+
+ // serverinfo vars
+ Cvar_Get("timelimit", "0", CVAR_SERVERINFO);
+ Cvar_Get("sv_keywords", "", CVAR_SERVERINFO);
+ 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_dlRate = Cvar_Get("sv_dlRate", "100", 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);
+
+ // 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_LATCH);
+ Cvar_CheckRange(sv_voip, 0, 1, true);
+ sv_voipProtocol = Cvar_Get("sv_voipProtocol", sv_voip->integer ? "opus" : "", CVAR_SYSTEMINFO | CVAR_ROM);
+#endif
+ Cvar_Get("sv_paks", "", CVAR_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_pakNames", "", CVAR_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_referencedPaks", "", CVAR_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_referencedPakNames", "", CVAR_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_alternatePaks", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_alternatePakNames", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_referencedAlternatePaks", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM);
+ Cvar_Get("sv_referencedAlternatePakNames", "", CVAR_ALTERNATE_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", "40", CVAR_TEMP);
+ sv_timeout = Cvar_Get("sv_timeout", "200", CVAR_TEMP);
+ sv_zombietime = Cvar_Get("sv_zombietime", "2", CVAR_TEMP);
+
+ sv_allowDownload = Cvar_Get("sv_allowDownload", "0", CVAR_SERVERINFO);
+ Cvar_Get("sv_dlURL", "http://downloads.tremulous.net", CVAR_SERVERINFO | CVAR_ARCHIVE);
+
+ sv_protect = Cvar_Get("sv_protect", "3", CVAR_ARCHIVE);
+ sv_protectLog = Cvar_Get("sv_protectLog", "sv_protect.log", CVAR_ARCHIVE);
+ SV_InitAttackLog();
+
+ for (int a = 0; a < 3; ++a)
+ {
+ sv_masters[a][0] = Cvar_Get(va("sv_%smaster1", (a == 2 ? "alt2" : a == 1 ? "alt1" : "")), MASTER_SERVER_NAME, 0);
+ for (int i = 1; i < MAX_MASTER_SERVERS; i++)
+ sv_masters[a][i] = Cvar_Get(va("sv_%smaster%d", (a == 2 ? "alt2" : a == 1 ? "alt1" : ""), i + 1), "", 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_rsaAuth = Cvar_Get("sv_rsaAuth", "1", CVAR_INIT | CVAR_PROTECTED);
+ sv_schachtmeisterPort = Cvar_Get ("sv_schachtmeisterPort", "1337", CVAR_ARCHIVE);
+}
+
+/*
+==================
+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(const char *message)
+{
+ client_t *cl;
+
+ // send it twice, ignoring rate
+ for (int j = 0; j < 2; j++)
+ {
+ int i;
+ 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->lastSnapshotTime = 0;
+ SV_SendClientSnapshot(cl);
+ }
+ }
+ }
+}
+
+/*
+================
+SV_Shutdown
+
+Called when each game quits,
+before Sys_Quit or Sys_Error
+================
+*/
+void SV_Shutdown(const char *finalmsg)
+{
+ // close attack log
+ SV_CloseAttackLog();
+
+ 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)
+ {
+ for (int i = 0; i < sv_maxclients->integer; i++)
+ SV_FreeClient(&svs.clients[i]);
+
+ Z_Free(svs.clients);
+ }
+ ::memset(&svs, 0, sizeof(svs));
+
+ Cvar_Set("sv_running", "0");
+
+ Com_Printf("---------------------------\n");
+
+ // disconnect any local clients
+ if (sv_killserver->integer != 2) CL_Disconnect(false);
+}
diff --git a/src/server/sv_main.cpp b/src/server/sv_main.cpp
new file mode 100644
index 0000000..f5c3b98
--- /dev/null
+++ b/src/server/sv_main.cpp
@@ -0,0 +1,1551 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+
+#include "server.h"
+
+#include <iostream>
+
+#ifdef USE_VOIP
+cvar_t *sv_voip;
+cvar_t *sv_voipProtocol;
+#endif
+
+serverStatic_t svs; // persistant server info
+server_t sv {}; // local server
+
+cvar_t *sv_fps = NULL; // 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_masters[3][MAX_MASTER_SERVERS]; // master server IP addresses
+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_dlRate;
+cvar_t *sv_minPing;
+cvar_t *sv_maxPing;
+cvar_t *sv_pure;
+cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491)
+cvar_t *sv_banFile;
+
+cvar_t *sv_rsaAuth;
+
+cvar_t *sv_schachtmeisterPort;
+
+// server attack protection
+cvar_t *sv_protect; // 0 - unprotected
+ // 1 - ioquake3 method (default)
+ // 2 - OpenWolf method
+ // 4 - prints attack info to console (when ioquake3 or OPenWolf method is set)
+cvar_t *sv_protectLog; // name of log file
+
+/*
+=============================================================================
+
+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 bool 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 ] ) );
+ return true;
+ }
+ }
+ }
+ return false;
+}
+#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.
+ // Summary: The bug is that messages longer than 1022 are not
+ // allowed downstream and there is a buffer overflow issue
+ // affecting network traffic etc. Therefore, one way to stop
+ // this from happening is stopping the packet here. Ideally,
+ // we should increase the size of the downstream message.
+ if ( strlen ((char *)message) > 1022 ) {
+ SV_WriteAttackLog( va( "SV_SendServerCommand( %ld, %.20s... ) length %ld > 1022, "
+ "dropping to avoid server buffer overflow.\n",
+ cl - svs.clients, message, strlen( (char *)message ) ) );
+ SV_WriteAttackLog( va( "Full message: [%s]\n", message ) );
+ 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
+void SV_MasterHeartbeat(const char *message)
+{
+ static netadr_t adrs[3][MAX_MASTER_SERVERS][2]; // [2] for v4 and v6 address for the same address string.
+ int a;
+ int i;
+ int res;
+ int netenabled;
+ int netAlternateProtocols;
+
+ netenabled = Cvar_VariableIntegerValue("net_enabled");
+ netAlternateProtocols = Cvar_VariableIntegerValue("net_alternateProtocols");
+
+ // "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;
+
+ for (a = 0; a < 3; ++a)
+ {
+ // indent
+ if(a == 0 && (netAlternateProtocols & NET_DISABLEPRIMPROTO))
+ continue;
+ if(a == 1 && !(netAlternateProtocols & NET_ENABLEALT1PROTO))
+ continue;
+ if(a == 2 && !(netAlternateProtocols & NET_ENABLEALT2PROTO))
+ continue;
+
+ // send to group masters
+ for (i = 0; i < MAX_MASTER_SERVERS; i++)
+ {
+ if(!sv_masters[a][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_masters[a][i]->modified || (adrs[a][i][0].type == NA_BAD && adrs[a][i][1].type == NA_BAD))
+ {
+ sv_masters[a][i]->modified = false;
+
+ if(netenabled & NET_ENABLEV4)
+ {
+ Com_Printf("Resolving %s (IPv4)\n", sv_masters[a][i]->string);
+ res = NET_StringToAdr(sv_masters[a][i]->string, &adrs[a][i][0], NA_IP);
+ adrs[a][i][0].alternateProtocol = a;
+
+ if(res == 2)
+ {
+ // if no port was specified, use the default master port
+ adrs[a][i][0].port = BigShort(a == 2 ? ALT2PORT_MASTER : a == 1 ? ALT1PORT_MASTER : PORT_MASTER);
+ }
+
+ if(res)
+ Com_Printf( "%s resolved to %s\n", sv_masters[a][i]->string, NET_AdrToStringwPort(adrs[a][i][0]));
+ else
+ Com_Printf( "%s has no IPv4 address.\n", sv_masters[a][i]->string);
+ }
+
+ if(netenabled & NET_ENABLEV6)
+ {
+ Com_Printf("Resolving %s (IPv6)\n", sv_masters[a][i]->string);
+ res = NET_StringToAdr(sv_masters[a][i]->string, &adrs[a][i][1], NA_IP6);
+ adrs[a][i][1].alternateProtocol = a;
+
+ if(res == 2)
+ {
+ // if no port was specified, use the default master port
+ adrs[a][i][1].port = BigShort(a == 2 ? ALT2PORT_MASTER : a == 1 ? ALT1PORT_MASTER : PORT_MASTER);
+ }
+
+ if(res)
+ Com_Printf( "%s resolved to %s\n", sv_masters[a][i]->string, NET_AdrToStringwPort(adrs[a][i][1]));
+ else
+ Com_Printf( "%s has no IPv6 address.\n", sv_masters[a][i]->string);
+ }
+
+ if(adrs[a][i][0].type == NA_BAD && adrs[a][i][1].type == NA_BAD)
+ {
+ Com_Printf("Couldn't resolve address: %s\n", sv_masters[a][i]->string);
+ Cvar_Set(sv_masters[a][i]->name, "");
+ sv_masters[a][i]->modified = false;
+ continue;
+ }
+ }
+
+
+ Com_Printf ("Sending%s heartbeat to %s\n", (a == 2 ? " alternate-2" : a == 1 ? " alternate-1" : ""), sv_masters[a][i]->string );
+
+ // this command should be changed if the server info / status format
+ // ever incompatably changes
+
+ if(adrs[a][i][0].type != NA_BAD)
+ NET_OutOfBandPrint( NS_SERVER, adrs[a][i][0], "heartbeat %s\n", message);
+ if(adrs[a][i][1].type != NA_BAD)
+ NET_OutOfBandPrint( NS_SERVER, adrs[a][i][1], "heartbeat %s\n", message);
+ }
+ // outdent
+ }
+}
+
+/*
+=================
+SV_MasterShutdown
+
+Informs all masters that this server is going down
+=================
+*/
+void SV_MasterShutdown( void ) {
+ // send a heartbeat right now
+ svs.nextHeartbeatTime = -9999;
+ SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER);
+
+ // send it again to minimize chance of drops
+ svs.nextHeartbeatTime = -9999;
+ SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER);
+
+ // when the master tries to poll the server, it won't respond, so
+ // it will be removed from the list
+}
+
+/*
+==============================================================================
+
+CONNECTIONLESS COMMANDS
+
+==============================================================================
+*/
+
+// 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 ];
+leakyBucket_t outboundLeakyBucket;
+
+/*
+================
+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;
+ 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 ( int 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 ) ||
+ interval < 0 ) ) {
+ if ( bucket->prev != NULL ) {
+ bucket->prev->next = bucket->next;
+ } else {
+ bucketHashes[ bucket->hash ] = bucket->next;
+ }
+
+ if ( bucket->next != NULL ) {
+ bucket->next->prev = bucket->prev;
+ }
+
+ ::memset( bucket, 0, sizeof( leakyBucket_t ) );
+ }
+
+ if ( bucket->type == NA_BAD ) {
+ bucket->type = address.type;
+ switch ( address.type ) {
+ case NA_IP: ::memcpy( bucket->ipv._4, address.ip, 4 ); break;
+ case NA_IP6: ::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
+ // Write the info to the attack log since this is relevant information as the system is malfunctioning
+ SV_WriteAttackLogD(va("SVC_BucketForAddress: Could not allocate a bucket for client from %s\n", NET_AdrToString(address)));
+
+ return NULL;
+}
+
+/*
+================
+SVC_RateLimit
+ *
+ * @param[in,out] bucket
+ * @param[in] burst
+ * @param[in] period
+ * @return
+ *
+ * @note Don't call if sv_protect 1 (SVP_IOQ3) flag is not set!
+================
+*/
+bool 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 || interval < 0 )
+ {
+ bucket->burst = 0;
+ bucket->lastTime = now;
+ }
+ else
+ {
+ bucket->burst -= expired;
+ bucket->lastTime = now - expiredRemainder;
+ }
+
+ if ( bucket->burst < burst )
+ {
+ bucket->burst++;
+ return false;
+ }
+ else
+ {
+ SV_WriteAttackLogD(va("SVC_RateLimit: burst limit exceeded for bucket: %i limit: %i\n", bucket->burst, burst));
+ }
+ }
+
+ return true;
+}
+
+/*
+================
+SVC_RateLimitAddress
+
+Rate limit for a particular address
+================
+*/
+bool 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];
+
+ if (sv_protect->integer & SVP_IOQ3) {
+ // Prevent using getstatus as an amplifier
+ if (SVC_RateLimitAddress(from, 10, 1000)) {
+ SV_WriteAttackLog(va("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(&outboundLeakyBucket, 10, 100)) {
+ SV_WriteAttackLog("SVC_Status: rate limit exceeded, dropping request\n");
+ return;
+ }
+ }
+
+ // A maximum challenge length of 128 should be more than plenty.
+ if (strlen(Cmd_Argv(1)) > 128) {
+ SV_WriteAttackLog(va("SVC_Status: challenge length exceeded from %s, dropping request\n", NET_AdrToString(from)));
+ 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) );
+
+ if ( from.alternateProtocol != 0 )
+ Info_SetValueForKey( infostring, "protocol", from.alternateProtocol == 2 ? "69" : "70" );
+
+ 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;
+ const char *gamedir;
+ char infostring[MAX_INFO_STRING];
+
+ if (sv_protect->integer & SVP_IOQ3) {
+ // Prevent using getinfo as an amplifier
+ if (SVC_RateLimitAddress(from, 10, 1000)) {
+ SV_WriteAttackLog(va("SVC_Info: rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString(from)));
+ return;
+ }
+
+ // Allow getinfo to be DoSed relatively easily, but prevent
+ // excess outbound bandwidth usage when being flooded inbound
+ if (SVC_RateLimit(&outboundLeakyBucket, 10, 100)) {
+ SV_WriteAttackLog("SVC_Info: rate limit exceeded, dropping request\n");
+ 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) {
+ SV_WriteAttackLog(va("SVC_Info: challenge length from %s exceeded, dropping request\n", NET_AdrToString(from)));
+ 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", from.alternateProtocol == 2 ? 69 : from.alternateProtocol == 1 ? 70 : PROTOCOL_VERSION) );
+ Info_SetValueForKey( infostring, "gamename", com_gamename->string );
+ 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, "pure", va("%i", sv_pure->integer ) );
+
+#ifdef USE_VOIP
+ if (sv_voipProtocol->string && *sv_voipProtocol->string) {
+ Info_SetValueForKey( infostring, "voip", sv_voipProtocol->string );
+ }
+#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 );
+}
+
+/**
+ * @brief DRDoS stands for "Distributed Reflected Denial of Service".
+ * See here: http://www.lemuria.org/security/application-drdos.html
+ *
+ * If the address isn't NA_IP, it's automatically denied.
+ *
+ * @return false if we're good.
+ * otherwise true means we need to block.
+ *
+ * @note Don't call this if sv_protect 2 flag is not set!
+ */
+bool SV_CheckDRDoS(netadr_t from) {
+ int i;
+ int globalCount;
+ int specificCount;
+ int timeNow;
+ receipt_t *receipt;
+ netadr_t exactFrom;
+ int oldest;
+ int oldestTime;
+ static int lastGlobalLogTime = 0;
+ static int lastSpecificLogTime = 0;
+
+ // Usually the network is smart enough to not allow incoming UDP packets
+ // with a source address being a spoofed LAN address. Even if that's not
+ // the case, sending packets to other hosts in the LAN is not a big deal.
+ // NA_LOOPBACK qualifies as a LAN address.
+ if (Sys_IsLANAddress(from)) {
+ return false;
+ }
+
+ timeNow = svs.time;
+ exactFrom = from;
+
+ // Time has wrapped
+ if (lastGlobalLogTime > timeNow || lastSpecificLogTime > timeNow) {
+ lastGlobalLogTime = 0;
+ lastSpecificLogTime = 0;
+
+ // just setting time to 1 (cannot be 0 as then globalCount would not be counted)
+ for (i = 0; i < MAX_INFO_RECEIPTS; i++) {
+ if (svs.infoReceipts[i].time) {
+ svs.infoReceipts[i].time = 1; // hack it so we count globalCount correctly
+ }
+ }
+ }
+
+ if (from.type == NA_IP) {
+ from.ip[3] = 0; // xx.xx.xx.0
+ } else {
+ from.ip6[15] = 0;
+ }
+
+ // Count receipts in last 2 seconds.
+ globalCount = 0;
+ specificCount = 0;
+ receipt = &svs.infoReceipts[0];
+ oldest = 0;
+ oldestTime = 0x7fffffff;
+ for (i = 0; i < MAX_INFO_RECEIPTS; i++, receipt++) {
+ if (receipt->time + 2000 > timeNow) {
+ if (receipt->time) {
+ // When the server starts, all receipt times are at zero. Furthermore,
+ // svs.time is close to zero. We check that the receipt time is already
+ // set so that during the first two seconds after server starts, queries
+ // from the master servers don't get ignored. As a consequence a potentially
+ // unlimited number of getinfo+getstatus responses may be sent during the
+ // first frame of a server's life.
+ globalCount++;
+ }
+ if (NET_CompareBaseAdr(from, receipt->adr)) {
+ specificCount++;
+ }
+ }
+ if (receipt->time < oldestTime) {
+ oldestTime = receipt->time;
+ oldest = i;
+ }
+ }
+
+ if (globalCount == MAX_INFO_RECEIPTS) { // All receipts happened in last 2 seconds.
+ if (lastGlobalLogTime + 1000 <= timeNow) { // Limit one log every second.
+ SV_WriteAttackLog("Detected flood of getinfo/getstatus connectionless packets\n");
+ lastGlobalLogTime = timeNow;
+ }
+
+ return true;
+ }
+ if (specificCount >= 3) { // Already sent 3 to this IP in last 2 seconds.
+ if (lastSpecificLogTime + 1000 <= timeNow) { // Limit one log every second.
+ SV_WriteAttackLog(va("Possible DRDoS attack to address %s, ignoring getinfo/getstatus connectionless packet\n",
+ NET_AdrToString(exactFrom)));
+ lastSpecificLogTime = timeNow;
+ }
+
+ return true;
+ }
+
+ receipt = &svs.infoReceipts[oldest];
+ receipt->adr = from;
+ receipt->time = timeNow;
+ return false;
+}
+
+/*
+===============
+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 ) {
+ bool 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 ((sv_protect->integer & SVP_IOQ3) && SVC_RateLimitAddress(from, 10, 1000)) {
+ SV_WriteAttackLog(va("Bad rcon - 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 ((sv_protect->integer & SVP_IOQ3) && SVC_RateLimit(&bucket, 10, 1000)) {
+ SV_WriteAttackLog("Bad rcon - rate limit exceeded, dropping request\n");
+ return;
+ }
+
+ valid = false;
+ Com_Printf ("Bad rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) );
+ } else {
+ valid = true;
+ Com_Printf ("Rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) );
+ SV_WriteAttackLog(va("Rcon from %s: %s\n", NET_AdrToString(from), Cmd_Argv(2)));
+ }
+
+ // start redirecting all print outputs to the packet
+ svs.redirectAddress = from;
+ Com_BeginRedirect (sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect);
+
+ if ( !strlen( sv_rconPassword->string ) ) {
+ Com_Printf ("No rconpassword set on the server.\n");
+ } else if ( !valid ) {
+ Com_Printf ("Bad rconpassword.\n");
+ SV_WriteAttackLog(va("Bad rconpassword from %s\n", NET_AdrToString(from)));
+ } 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 ();
+}
+
+static void SVC_SchachtmeisterResponse( netadr_t from ) {
+
+ int tmp[ 4 ];
+
+ if ( !( from.type == NA_IP && from.ip[0] == 127 ) ) {
+ return;
+ }
+
+ if ( Cmd_Argc() >= 2 && sscanf( Cmd_Argv( 1 ), "%i.%i.%i.%i", &tmp[ 0 ], &tmp[ 1 ], &tmp[ 2 ], &tmp[ 3 ] ) == 4 ) { // compatibility with out-of-date crapware conceived in the future
+ char cmdl[ MAX_STRING_CHARS ];
+ Com_sprintf( cmdl, sizeof( cmdl ), "smr ipa %s", Cmd_ArgsFrom( 1 ) );
+ Cmd_TokenizeString( cmdl );
+ } else {
+ strcpy( Cmd_Argv( 0 ), "smr" );
+ }
+
+ SV_GameCommand();
+}
+
+/*
+=================
+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;
+ const 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")) {
+ if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) {
+ return;
+ }
+
+ SVC_Status( from );
+ } else if (!Q_stricmp(c, "getinfo")) {
+ if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) {
+ return;
+ }
+
+ SVC_Info( from );
+ } else if (!Q_stricmp(c, "getchallenge")) {
+ if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) {
+ return;
+ }
+
+ SV_GetChallenge(from);
+ } else if (!Q_stricmp(c, "connect")) {
+ SV_DirectConnect( from );
+ } else if (!Q_stricmp(c, "rcon")) {
+ SVC_RemoteCommand( from, msg );
+ } else if (!Q_stricmp(c, "disconnect")) {
+ // if a client starts up a local server, we may see some spurious
+ // server disconnect messages when their new server sees our final
+ // sequenced messages to the old client
+ } else if (!Q_stricmp(c, "sm2reply")) {
+ SVC_SchachtmeisterResponse( from );
+ Com_Printf( "^2response [^7%s^2]\n", s );
+ } else {
+ SV_WriteAttackLog(va("bad connectionless packet from %s:\n%s\n" // changed from Com_DPrintf to print in attack log
+ , NET_AdrToString(from), s)); // this was never reported to admins before so they might be confused
+ } // note: if protect log isn't set we do Com_Printf
+}
+
+//============================================================================
+
+/*
+=================
+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;
+ }
+}
+
+
+/*
+===================
+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;
+ }
+
+ 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 bool SV_CheckPaused( void ) {
+ int count;
+ client_t *cl;
+ int i;
+
+ if ( !cl_paused->integer ) {
+ return false;
+ }
+
+ // 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 ) {
+ count++;
+ }
+ }
+
+ if ( count > 1 ) {
+ // don't pause
+ if (sv_paused->integer)
+ Cvar_Set("sv_paused", "0");
+ return false;
+ }
+
+ if (!sv_paused->integer)
+ Cvar_Set("sv_paused", "1");
+ return true;
+}
+
+/*
+==================
+SV_FrameMsec
+Return time in millseconds until processing of the next server frame.
+==================
+*/
+int SV_FrameMsec()
+{
+ if(sv_fps)
+ {
+ int frameMsec;
+
+ frameMsec = 1000.0f / sv_fps->value;
+
+ if(frameMsec < sv.timeResidual)
+ return 0;
+ else
+ return frameMsec - sv.timeResidual;
+ }
+ else
+ return 1;
+}
+
+#define CPU_USAGE_WARNING 70
+#define FRAME_TIME_WARNING 30
+
+/*
+==================
+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;
+ int frameStartTime = 0;
+ static int start, end;
+
+ start = Sys_Milliseconds();
+ svs.stats.idle += ( double )(start - end) / 1000;
+
+ // 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 (com_dedicated->integer)
+ {
+ frameStartTime = Sys_Milliseconds();
+ }
+
+ // 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 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();
+
+ // 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 (sv.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(HEARTBEAT_FOR_MASTER);
+
+ if (com_dedicated->integer)
+ {
+ int frameEndTime = Sys_Milliseconds();
+
+ svs.totalFrameTime += (frameEndTime - frameStartTime);
+
+ // we may send warnings (similar to watchdog) to the game in case the frametime is unacceptable
+ //Com_Printf("FRAMETIME frame: %i total %i\n", frameEndTime - frameStartTime, svs.totalFrameTime);
+
+ svs.currentFrameIndex++;
+
+ //if( svs.currentFrameIndex % 50 == 0 )
+ // Com_Printf( "currentFrameIndex: %i\n", svs.currentFrameIndex );
+
+ if (svs.currentFrameIndex == SERVER_PERFORMANCECOUNTER_FRAMES)
+ {
+ int averageFrameTime = svs.totalFrameTime / SERVER_PERFORMANCECOUNTER_FRAMES;
+
+ svs.sampleTimes[svs.currentSampleIndex % SERVER_PERFORMANCECOUNTER_SAMPLES] = averageFrameTime;
+ svs.currentSampleIndex++;
+
+ if (svs.currentSampleIndex > SERVER_PERFORMANCECOUNTER_SAMPLES)
+ {
+ int totalTime = 0, i;
+
+ for (i = 0; i < SERVER_PERFORMANCECOUNTER_SAMPLES; i++)
+ {
+ totalTime += svs.sampleTimes[i];
+ }
+
+ if (!totalTime)
+ {
+ totalTime = 1;
+ }
+
+ averageFrameTime = totalTime / SERVER_PERFORMANCECOUNTER_SAMPLES;
+
+ svs.serverLoad = (int)((averageFrameTime / (float)frameMsec) * 100);
+ }
+
+ //Com_Printf( "serverload: %i (%i/%i)\n", svs.serverLoad, averageFrameTime, frameMsec );
+
+ svs.totalFrameTime = 0;
+ svs.currentFrameIndex = 0;
+ }
+ }
+ else
+ {
+ svs.serverLoad = -1;
+ }
+
+ // collect timing statistics
+ // - the above 2.60 performance thingy is just inaccurate (30 seconds 'stats')
+ // to give good warning messages and is only done for dedicated
+ end = Sys_Milliseconds();
+ svs.stats.active += (( double )(end - start)) / 1000;
+
+ if (++svs.stats.count == STATFRAMES) // 5 seconds
+ {
+ svs.stats.latched_active = svs.stats.active;
+ svs.stats.latched_idle = svs.stats.idle;
+ svs.stats.active = 0;
+ svs.stats.idle = 0;
+ svs.stats.count = 0;
+
+ svs.stats.cpu = svs.stats.latched_active + svs.stats.latched_idle;
+
+ if (svs.stats.cpu != 0.f)
+ {
+ svs.stats.cpu = 100 * svs.stats.latched_active / svs.stats.cpu;
+ }
+
+ svs.stats.avg = 1000 * svs.stats.latched_active / STATFRAMES;
+
+ // FIXME: add mail, IRC, player info etc for both warnings
+ // TODO: inspect/adjust these values and/or add cvars
+ if (svs.stats.cpu > CPU_USAGE_WARNING)
+ {
+ Com_Printf("^3WARNING: Server CPU has reached a critical usage of %i%%\n", (int) svs.stats.cpu);
+ }
+
+ if (svs.stats.avg > FRAME_TIME_WARNING)
+ {
+ Com_Printf("^3WARNING: Average frame time has reached a critical value of %ims\n", (int) svs.stats.avg);
+ }
+ }
+}
+
+/*
+====================
+SV_RateMsec
+
+Return the number of msec until another message can be sent to
+a client based on its rate settings
+====================
+*/
+
+#define UDPIP_HEADER_SIZE 28
+#define UDPIP6_HEADER_SIZE 48
+
+int SV_RateMsec(client_t *client)
+{
+ int rate, rateMsec;
+ int messageSize;
+
+ messageSize = client->netchan.lastSentSize;
+ 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;
+ }
+
+ if(client->netchan.remoteAddress.type == NA_IP6)
+ messageSize += UDPIP6_HEADER_SIZE;
+ else
+ messageSize += UDPIP_HEADER_SIZE;
+
+ rate = (int)(rate * com_timescale->value);
+ if(rate < 1)
+ rate = 1;
+ rateMsec = messageSize * 1000 / rate;
+ rate = Sys_Milliseconds() - client->netchan.lastSentTime;
+
+ if(rate > rateMsec)
+ return 0;
+ else
+ return rateMsec - rate;
+}
+
+/*
+====================
+SV_SendQueuedPackets
+
+Send download messages and queued packets in the time that we're idle, i.e.
+not computing a server frame or sending client snapshots.
+Return the time in msec until we expect to be called next
+====================
+*/
+
+int SV_SendQueuedPackets()
+{
+ int numBlocks;
+ int dlStart, deltaT, delayT;
+ static int dlNextRound = 0;
+ int timeVal = INT_MAX;
+
+ // Send out fragmented packets now that we're idle
+ delayT = SV_SendQueuedMessages();
+ if(delayT >= 0)
+ timeVal = delayT;
+
+ if(sv_dlRate->integer)
+ {
+ // Rate limiting. This is very imprecise for high
+ // download rates due to millisecond timedelta resolution
+ dlStart = Sys_Milliseconds();
+ deltaT = dlNextRound - dlStart;
+
+ if(deltaT > 0)
+ {
+ if(deltaT < timeVal)
+ timeVal = deltaT + 1;
+ }
+ else
+ {
+ numBlocks = SV_SendDownloadMessages();
+
+ if(numBlocks)
+ {
+ // There are active downloads
+ deltaT = Sys_Milliseconds() - dlStart;
+
+ delayT = 1000 * numBlocks * MAX_DOWNLOAD_BLKSIZE;
+ delayT /= sv_dlRate->integer * 1024;
+
+ if(delayT <= deltaT + 1)
+ {
+ // Sending the last round of download messages
+ // took too long for given rate, don't wait for
+ // next round, but always enforce a 1ms delay
+ // between DL message rounds so we don't hog
+ // all of the bandwidth. This will result in an
+ // effective maximum rate of 1MB/s per user, but the
+ // low download window size limits this anyways.
+ if(timeVal > 2)
+ timeVal = 2;
+
+ dlNextRound = dlStart + deltaT + 1;
+ }
+ else
+ {
+ dlNextRound = dlStart + delayT;
+ delayT -= deltaT;
+
+ if(delayT < timeVal)
+ timeVal = delayT;
+ }
+ }
+ }
+ }
+ else
+ {
+ if(SV_SendDownloadMessages())
+ timeVal = 0;
+ }
+
+ return timeVal;
+}
diff --git a/src/server/sv_net_chan.cpp b/src/server/sv_net_chan.cpp
new file mode 100644
index 0000000..f8d9b7e
--- /dev/null
+++ b/src/server/sv_net_chan.cpp
@@ -0,0 +1,259 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+
+#include "server.h"
+
+#include "qcommon/q_shared.h"
+#include "qcommon/msg.h"
+#include "qcommon/net.h"
+#include "qcommon/qcommon.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;
+ bool 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 = false;
+
+ /* 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 || (client->netchan.alternateProtocol == 2 && 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;
+ bool soob;
+ byte key, *string;
+
+ srdc = msg->readcount;
+ sbit = msg->bit;
+ soob = msg->oob;
+
+ msg->oob = false;
+
+ 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 || (client->netchan.alternateProtocol == 2 && 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_FreeQueue
+=================
+*/
+void SV_Netchan_FreeQueue(client_t *client)
+{
+ netchan_buffer_t *netbuf, *next;
+
+ for(netbuf = client->netchan_start_queue; netbuf; netbuf = next)
+ {
+ next = netbuf->next;
+ Z_Free(netbuf);
+ }
+
+ client->netchan_start_queue = NULL;
+ client->netchan_end_queue = &client->netchan_start_queue;
+}
+
+/*
+=================
+SV_Netchan_TransmitNextInQueue
+=================
+*/
+void SV_Netchan_TransmitNextInQueue(client_t *client)
+{
+ netchan_buffer_t *netbuf;
+
+ Com_DPrintf("#462 Netchan_TransmitNextFragment: popping a queued message for transmit\n");
+ netbuf = client->netchan_start_queue;
+
+ 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_TransmitNextFragment
+Transmit the next fragment and the next queued packet
+Return number of ms until next message can be sent based on throughput given by client rate,
+-1 if no packet was sent.
+=================
+*/
+
+int SV_Netchan_TransmitNextFragment(client_t *client)
+{
+ if(client->netchan.unsentFragments)
+ {
+ Netchan_TransmitNextFragment(&client->netchan);
+ return SV_RateMsec(client);
+ }
+ else if(client->netchan_start_queue)
+ {
+ SV_Netchan_TransmitNextInQueue(client);
+ return SV_RateMsec(client);
+ }
+
+ return -1;
+}
+
+
+/*
+===============
+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)
+{
+ MSG_WriteByte( msg, svc_EOF );
+
+ if(client->netchan.unsentFragments || client->netchan_start_queue)
+ {
+ 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;
+ }
+ else
+ {
+ if (client->netchan.alternateProtocol != 0)
+ SV_Netchan_Encode( client, msg );
+ Netchan_Transmit( &client->netchan, msg->cursize, msg->data );
+ }
+}
+
+/*
+=================
+Netchan_SV_Process
+=================
+*/
+bool SV_Netchan_Process( client_t *client, msg_t *msg )
+{
+ bool ret = Netchan_Process( &client->netchan, msg );
+ if (!ret) return false;
+
+ if (client->netchan.alternateProtocol != 0)
+ SV_Netchan_Decode( client, msg );
+
+ return true;
+}
diff --git a/src/server/sv_snapshot.cpp b/src/server/sv_snapshot.cpp
new file mode 100644
index 0000000..07dd210
--- /dev/null
+++ b/src/server/sv_snapshot.cpp
@@ -0,0 +1,749 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+
+#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(int alternateProtocol, 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 false, this will not result
+ // in any bytes being emited if the entity has not changed at all
+ MSG_WriteDeltaEntity(alternateProtocol, msg, oldent, newent, false);
+ oldindex++;
+ newindex++;
+ continue;
+ }
+
+ if (newnum < oldnum)
+ {
+ // this is a new entity, send it from the baseline
+ MSG_WriteDeltaEntity(alternateProtocol, msg, &sv.svEntities[newnum].baseline, newent, true);
+ newindex++;
+ continue;
+ }
+
+ if (newnum > oldnum)
+ {
+ // the old entity isn't present in the new message
+ MSG_WriteDeltaEntity(alternateProtocol, msg, oldent, NULL, true);
+ 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(client->netchan.alternateProtocol, msg, &oldframe->ps, &frame->ps);
+ }
+ else
+ {
+ MSG_WriteDeltaPlayerstate(client->netchan.alternateProtocol, msg, NULL, &frame->ps);
+ }
+
+ // delta encode the entities
+ SV_EmitPacketEntities(client->netchan.alternateProtocol, 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
+
+=============================================================================
+*/
+
+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)
+{
+ 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)
+ {
+ if (~ent->r.hack.generic1 & (1 << (frame->ps.clientNum - 32))) continue;
+ }
+ else
+ {
+ 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 it's 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->r.currentOrigin, origin, dir);
+ if (VectorLengthSquared(dir) > (float)ent->s.generic1 * ent->s.generic1)
+ {
+ continue;
+ }
+ }
+ SV_AddEntitiesVisibleFromPoint(ent->s.origin2, frame, eNums);
+ }
+ }
+}
+
+/*
+=============
+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;
+ ::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);
+
+ // 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++;
+ }
+}
+
+#ifdef USE_VOIP
+/*
+==================
+SV_WriteVoipToClient
+
+Check to see if there is any VoIP queued for a client, and send if there is.
+==================
+*/
+static void SV_WriteVoipToClient(client_t *cl, msg_t *msg)
+{
+ int totalbytes = 0;
+ int i;
+ voipServerPacket_t *packet;
+
+ if (cl->queuedVoipPackets)
+ {
+ // Write as many VoIP packets as we reasonably can...
+ for (i = 0; i < cl->queuedVoipPackets; i++)
+ {
+ packet = cl->voipPacket[(i + cl->queuedVoipIndex) % ARRAY_LEN(cl->voipPacket)];
+
+ if (!*cl->downloadName)
+ {
+ totalbytes += packet->len;
+ if (totalbytes > (msg->maxsize - msg->cursize) / 2) break;
+
+ if (cl->netchan.alternateProtocol != 0) MSG_WriteByte(msg, svc_EOF);
+ MSG_WriteByte(msg, svc_voipSpeex);
+ if (cl->netchan.alternateProtocol != 0) MSG_WriteByte(msg, svc_voipSpeex + 1);
+ 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);
+ if (cl->netchan.alternateProtocol == 0) MSG_WriteBits(msg, packet->flags, VOIP_FLAGCNT);
+ MSG_WriteData(msg, packet->data, packet->len);
+ }
+
+ Z_Free(packet);
+ }
+
+ cl->queuedVoipPackets -= i;
+ cl->queuedVoipIndex += i;
+ cl->queuedVoipIndex %= ARRAY_LEN(cl->voipPacket);
+ }
+}
+#endif
+
+/*
+=======================
+SV_SendMessageToClient
+
+Called by SV_SendClientSnapshot and SV_SendClientGameState
+=======================
+*/
+void SV_SendMessageToClient(msg_t *msg, client_t *client)
+{
+ // 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);
+}
+
+/*
+=======================
+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);
+
+ MSG_Init(&msg, msg_buf, sizeof(msg_buf));
+ msg.allowoverflow = true;
+
+ // 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);
+
+#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; i < sv_maxclients->integer; i++)
+ {
+ c = &svs.clients[i];
+
+ if (!c->state) continue; // not connected
+
+ if (svs.time - c->lastSnapshotTime < c->snapshotMsec * com_timescale->value) continue; // It's not time yet
+
+ if (*c->downloadName) continue; // Client is downloading, don't send snapshots
+
+ if (c->netchan.unsentFragments || c->netchan_start_queue)
+ {
+ c->rateDelayed = true;
+ continue; // Drop this snapshot if the packet queue is still full or delta compression will break
+ }
+
+ if (!(c->netchan.remoteAddress.type == NA_LOOPBACK ||
+ (sv_lanForceRate->integer && Sys_IsLANAddress(c->netchan.remoteAddress))))
+ {
+ // rate control for clients not on LAN
+
+ if (SV_RateMsec(c) > 0)
+ {
+ // Not enough time since last packet passed through the line
+ c->rateDelayed = true;
+ continue;
+ }
+ }
+
+ // generate and send a new message
+ SV_SendClientSnapshot(c);
+ c->lastSnapshotTime = svs.time;
+ c->rateDelayed = false;
+ }
+}
diff --git a/src/server/sv_world.cpp b/src/server/sv_world.cpp
new file mode 100644
index 0000000..fd0710e
--- /dev/null
+++ b/src/server/sv_world.cpp
@@ -0,0 +1,745 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous 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 3 of the License,
+or (at your option) any later version.
+
+Tremulous 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 Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+
+// 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, true);
+ }
+
+ // 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.
+
+===============================================================================
+*/
+
+struct worldSector_t {
+ int axis; // -1 = leaf node
+ float dist;
+ worldSector_t *children[2];
+ svEntity_t *entities;
+};
+
+#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;
+
+ ::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;
+
+ 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;
+ for ( ;; )
+ {
+ 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.
+============================================================================
+*/
+
+struct areaParms_t {
+ const float *mins;
+ const float *maxs;
+ int *list;
+ int count;
+ int maxcount;
+};
+
+/*
+====================
+SV_AreaEntities_r
+
+====================
+*/
+static void SV_AreaEntities_r(worldSector_t *node, areaParms_t *ap)
+{
+ svEntity_t *check, *next;
+ sharedEntity_t *gcheck;
+
+ 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;
+}
+
+//===========================================================================
+
+struct moveclip_t {
+ vec3_t boxmins;
+ vec3_t 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;
+ traceType_t collisionType;
+};
+
+/*
+====================
+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, traceType_t type)
+{
+ sharedEntity_t *touch;
+ clipHandle_t clipHandle;
+ float *origin, *angles;
+
+ touch = SV_GentityNum(entityNum);
+
+ ::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, type);
+
+ 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->collisionType);
+
+ 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)
+ {
+ int 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, traceType_t type)
+{
+ moveclip_t clip;
+ int i;
+
+ if (!mins)
+ {
+ mins = vec3_origin;
+ }
+ if (!maxs)
+ {
+ maxs = vec3_origin;
+ }
+
+ ::memset(&clip, 0, sizeof(moveclip_t));
+
+ // clip to world
+ CM_BoxTrace(&clip.trace, start, end, mins, maxs, 0, contentmask, type);
+ 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.collisionType = type;
+
+ // 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->r.currentAngles;
+ if (!hit->r.bmodel)
+ {
+ angles = vec3_origin; // boxes don't rotate
+ }
+
+ c2 = CM_TransformedPointContents(p, clipHandle, hit->r.currentOrigin, angles);
+
+ contents |= c2;
+ }
+
+ return contents;
+}