From d309df4ce4d8ad0ed995a8e1c4267412a7782021 Mon Sep 17 00:00:00 2001 From: xolatile Date: Mon, 4 Aug 2025 22:53:42 +0200 Subject: Bunch of small changes... --- src/fpsgame/server.cpp | 7389 ++++++++++++++++++++++++------------------------ 1 file changed, 3693 insertions(+), 3696 deletions(-) (limited to 'src/fpsgame/server.cpp') diff --git a/src/fpsgame/server.cpp b/src/fpsgame/server.cpp index a3f0481..ec781c6 100644 --- a/src/fpsgame/server.cpp +++ b/src/fpsgame/server.cpp @@ -2,17 +2,17 @@ namespace game { - void parseoptions(vector &args) - { - loopv(args) + void parseoptions(vector &args) + { + loopv(args) #ifndef STANDALONE - if(!game::clientoption(args[i])) + if(!game::clientoption(args[i])) #endif - if(!server::serveroption(args[i])) - conoutf(CON_ERROR, "unknown command-line option: %s", args[i]); - } + if(!server::serveroption(args[i])) + conoutf(CON_ERROR, "unknown command-line option: %s", args[i]); + } - const char *gameident() { return "fps"; } + const char *gameident() { return "fps"; } } VAR(regenbluearmour, 0, 1, 1); @@ -21,603 +21,603 @@ extern ENetAddress masteraddress; namespace server { - struct server_entity // server side version of "entity" type - { - int type; - int spawntime; - bool spawned; - }; - - static const int DEATHMILLIS = 300; - - struct clientinfo; - - struct gameevent - { - virtual ~gameevent() {} - - virtual bool flush(clientinfo *ci, int fmillis); - virtual void process(clientinfo *ci) {} - - virtual bool keepable() const { return false; } - }; - - struct timedevent : gameevent - { - int millis; - - bool flush(clientinfo *ci, int fmillis); - }; - - struct hitinfo - { - int target; - int lifesequence; - int rays; - float dist; - vec dir; - }; - - struct shotevent : timedevent - { - int id, gun; - vec from, to; - vector hits; - - void process(clientinfo *ci); - }; - - struct explodeevent : timedevent - { - int id, gun; - vector hits; - - bool keepable() const { return true; } - - void process(clientinfo *ci); - }; - - struct suicideevent : gameevent - { - void process(clientinfo *ci); - }; - - struct pickupevent : gameevent - { - int ent; - - void process(clientinfo *ci); - }; - - template - struct projectilestate - { - int projs[N]; - int numprojs; - - projectilestate() : numprojs(0) {} - - void reset() { numprojs = 0; } - - void add(int val) - { - if(numprojs>=N) numprojs = 0; - projs[numprojs++] = val; - } - - bool remove(int val) - { - loopi(numprojs) if(projs[i]==val) - { - projs[i] = projs[--numprojs]; - return true; - } - return false; - } - }; - - struct gamestate : fpsstate - { - vec o; - int state, editstate; - int lastdeath, deadflush, lastspawn, lifesequence; - int lastshot; - projectilestate<8> rockets, grenades; - int frags, flags, deaths, teamkills, shotdamage, damage, tokens; - int lasttimeplayed, timeplayed; - float effectiveness; - - gamestate() : state(CS_DEAD), editstate(CS_DEAD), lifesequence(0) {} - - bool isalive(int gamemillis) - { - return state==CS_ALIVE || (state==CS_DEAD && gamemillis - lastdeath <= DEATHMILLIS); - } - - bool waitexpired(int gamemillis) - { - return gamemillis - lastshot >= gunwait; - } - - void reset() - { - if(state!=CS_SPECTATOR) state = editstate = CS_DEAD; - maxhealth = 100; - maxarmour = 50; - rockets.reset(); - grenades.reset(); - - timeplayed = 0; - effectiveness = 0; - frags = flags = deaths = teamkills = shotdamage = damage = tokens = 0; - - lastdeath = 0; - - respawn(); - } - - void respawn() - { - fpsstate::respawn(); - o = vec(-1e10f, -1e10f, -1e10f); - deadflush = 0; - lastspawn = -1; - lastshot = 0; - tokens = 0; - } - - void reassign() - { - respawn(); - rockets.reset(); - grenades.reset(); - } - }; - - struct savedscore - { - uint ip; - string name; - int frags, flags, deaths, teamkills, shotdamage, damage; - int timeplayed; - float effectiveness; - - void save(gamestate &gs) - { - frags = gs.frags; - flags = gs.flags; - deaths = gs.deaths; - teamkills = gs.teamkills; - shotdamage = gs.shotdamage; - damage = gs.damage; - timeplayed = gs.timeplayed; - effectiveness = gs.effectiveness; - } - - void restore(gamestate &gs) - { - gs.frags = frags; - gs.flags = flags; - gs.deaths = deaths; - gs.teamkills = teamkills; - gs.shotdamage = shotdamage; - gs.damage = damage; - gs.timeplayed = timeplayed; - gs.effectiveness = effectiveness; - } - }; - - extern int gamemillis, nextexceeded; - - struct clientinfo - { - int clientnum, ownernum, connectmillis, sessionid, overflow; - string name, team, mapvote; - int playermodel; - int modevote; - int privilege; - bool connected, local, timesync; - int gameoffset, lastevent, pushed, exceeded; - gamestate state; - vector events; - vector position, messages; - uchar *wsdata; - int wslen; - vector bots; - int ping, aireinit; - string clientmap; - int mapcrc; - bool warned, gameclip; - ENetPacket *getdemo, *getmap, *clipboard; - int lastclipboard, needclipboard; - int connectauth; - uint authreq; - string authname, authdesc; - void *authchallenge; - int authkickvictim; - char *authkickreason; - - clientinfo() : getdemo(NULL), getmap(NULL), clipboard(NULL), authchallenge(NULL), authkickreason(NULL) { reset(); } - ~clientinfo() { events.deletecontents(); cleanclipboard(); cleanauth(); } - - void addevent(gameevent *e) - { - if(state.state==CS_SPECTATOR || events.length()>100) delete e; - else events.add(e); - } - - enum - { - PUSHMILLIS = 3000 - }; - - int calcpushrange() - { - ENetPeer *peer = getclientpeer(ownernum); - return PUSHMILLIS + ((peer) ? (int) (peer->roundTripTime + peer->roundTripTimeVariance) : (int) ENET_PEER_DEFAULT_ROUND_TRIP_TIME); - } - - bool checkpushed(int millis, int range) - { - return millis >= pushed - range && millis <= pushed + range; - } - - void scheduleexceeded() - { - if(state.state!=CS_ALIVE || !exceeded) return; - int range = calcpushrange(); - if(!nextexceeded || exceeded + range < nextexceeded) nextexceeded = exceeded + range; - } - - void setexceeded() - { - if(state.state==CS_ALIVE && !exceeded && !checkpushed(gamemillis, calcpushrange())) exceeded = gamemillis; - scheduleexceeded(); - } - - void setpushed() - { - pushed = max(pushed, gamemillis); - if(exceeded && checkpushed(exceeded, calcpushrange())) exceeded = 0; - } - - bool checkexceeded() - { - return state.state==CS_ALIVE && exceeded && gamemillis > exceeded + calcpushrange(); - } - - void mapchange() - { - mapvote[0] = 0; - modevote = INT_MAX; - state.reset(); - events.deletecontents(); - overflow = 0; - timesync = false; - lastevent = 0; - exceeded = 0; - pushed = 0; - clientmap[0] = '\0'; - mapcrc = 0; - warned = false; - gameclip = false; - } - - void reassign() - { - state.reassign(); - events.deletecontents(); - timesync = false; - lastevent = 0; - } - - void cleanclipboard(bool fullclean = true) - { - if(clipboard) { if(--clipboard->referenceCount <= 0) enet_packet_destroy(clipboard); clipboard = NULL; } - if(fullclean) lastclipboard = 0; - } - - void cleanauthkick() - { - authkickvictim = -1; - DELETEA(authkickreason); - } - - void cleanauth(bool full = true) - { - authreq = 0; - if(authchallenge) { freechallenge(authchallenge); authchallenge = NULL; } - if(full) cleanauthkick(); - } - - void reset() - { - name[0] = team[0] = 0; - playermodel = -1; - privilege = PRIV_NONE; - connected = local = false; - connectauth = 0; - position.setsize(0); - messages.setsize(0); - ping = 0; - aireinit = 0; - needclipboard = 0; - cleanclipboard(); - cleanauth(); - mapchange(); - } - - int geteventmillis(int servmillis, int clientmillis) - { - if(!timesync || (events.empty() && state.waitexpired(servmillis))) - { - timesync = true; - gameoffset = servmillis - clientmillis; - return servmillis; - } - else return gameoffset + clientmillis; - } - }; - - struct ban - { - int time, expire; - uint ip; - }; - - namespace aiman - { - extern void removeai(clientinfo *ci); - extern void clearai(); - extern void checkai(); - extern void reqadd(clientinfo *ci, int skill); - extern void reqdel(clientinfo *ci); - extern void setbotlimit(clientinfo *ci, int limit); - extern void setbotbalance(clientinfo *ci, bool balance); - extern void changemap(); - extern void addclient(clientinfo *ci); - extern void changeteam(clientinfo *ci); - } - - #define MM_MODE 0xF - #define MM_AUTOAPPROVE 0x1000 - #define MM_PRIVSERV (MM_MODE | MM_AUTOAPPROVE) - #define MM_PUBSERV ((1< allowedips; - vector bannedips; - - void addban(uint ip, int expire) - { - allowedips.removeobj(ip); - ban b; - b.time = totalmillis; - b.expire = totalmillis + expire; - b.ip = ip; - loopv(bannedips) if(bannedips[i].expire - b.expire > 0) { bannedips.insert(i, b); return; } - bannedips.add(b); - } - - vector connects, clients, bots; - - void kickclients(uint ip, clientinfo *actor = NULL, int priv = PRIV_NONE) - { - loopvrev(clients) - { - clientinfo &c = *clients[i]; - if(c.state.aitype != AI_NONE || c.privilege >= PRIV_ADMIN || c.local) continue; - if(actor && ((c.privilege > priv && !actor->local) || c.clientnum == actor->clientnum)) continue; - //~if(getclientip(c.clientnum) == ip) disconnect_client(c.clientnum, DISC_KICK); - } - } - - struct maprotation - { - static int exclude; - int modes; - string map; - - int calcmodemask() const { return modes&(1< maprotations; - int curmaprotation = 0; - - VAR(lockmaprotation, 0, 0, 2); - - void maprotationreset() - { - maprotations.setsize(0); - curmaprotation = 0; - maprotation::exclude = 0; - } - - void nextmaprotation() - { - curmaprotation++; - if(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes) return; - do curmaprotation--; - while(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes); - curmaprotation++; - } - - int findmaprotation(int mode, const char *map) - { - for(int i = max(curmaprotation, 0); i < maprotations.length(); i++) - { - maprotation &rot = maprotations[i]; - if(!rot.modes) break; - if(rot.match(mode, map)) return i; - } - int start; - for(start = max(curmaprotation, 0) - 1; start >= 0; start--) if(!maprotations[start].modes) break; - start++; - for(int i = start; i < curmaprotation; i++) - { - maprotation &rot = maprotations[i]; - if(!rot.modes) break; - if(rot.match(mode, map)) return i; - } - int best = -1; - loopv(maprotations) - { - maprotation &rot = maprotations[i]; - if(rot.match(mode, map) && (best < 0 || maprotations[best].includes(rot))) best = i; - } - return best; - } - - bool searchmodename(const char *haystack, const char *needle) - { - if(!needle[0]) return true; - do - { - if(needle[0] != '.') - { - haystack = strchr(haystack, needle[0]); - if(!haystack) break; - haystack++; - } - const char *h = haystack, *n = needle+1; - for(; *h && *n; h++) - { - if(*h == *n) n++; - else if(*h != ' ') break; - } - if(!*n) return true; - if(*n == '.') return !*h; - } while(needle[0] != '.'); - return false; - } - - int genmodemask(vector &modes) - { - int modemask = 0; - loopv(modes) - { - const char *mode = modes[i]; - int op = mode[0]; - switch(mode[0]) - { - case '*': - modemask |= 1< modes, maps; - for(int i = 0; i + 1 < numargs; i += 2) - { - explodelist(args[i].getstr(), modes); - explodelist(args[i+1].getstr(), maps); - int modemask = genmodemask(modes); - if(maps.length()) loopvj(maps) addmaprotation(modemask, maps[j]); - else addmaprotation(modemask, ""); - modes.deletearrays(); - maps.deletearrays(); - } - if(maprotations.length() && maprotations.last().modes) - { - maprotation &rot = maprotations.add(); - rot.modes = 0; - rot.map[0] = '\0'; - } - } - - COMMAND(maprotationreset, ""); - COMMANDN(maprotation, addmaprotations, "ss2V"); - - struct demofile - { - string info; - uchar *data; - int len; - }; - - vector demos; - - bool demonextmatch = false; - stream *demotmp = NULL, *demorecord = NULL, *demoplayback = NULL; - int nextplayback = 0; - - VAR(maxdemos, 0, 5, 25); - VAR(maxdemosize, 0, 16, 31); - VAR(restrictdemos, 0, 1, 1); - VARF(autorecorddemo, 0, 0, 1, demonextmatch = autorecorddemo!=0); - - VAR(restrictpausegame, 0, 1, 1); - VAR(restrictgamespeed, 0, 1, 1); - - SVAR(serverdesc, ""); - SVAR(serverpass, ""); - SVAR(adminpass, ""); - VARF(publicserver, 0, 0, 2, { + struct server_entity // server side version of "entity" type + { + int type; + int spawntime; + bool spawned; + }; + + static const int DEATHMILLIS = 300; + + struct clientinfo; + + struct gameevent + { + virtual ~gameevent() {} + + virtual bool flush(clientinfo *ci, int fmillis); + virtual void process(clientinfo *ci) {} + + virtual bool keepable() const { return false; } + }; + + struct timedevent : gameevent + { + int millis; + + bool flush(clientinfo *ci, int fmillis); + }; + + struct hitinfo + { + int target; + int lifesequence; + int rays; + float dist; + vec dir; + }; + + struct shotevent : timedevent + { + int id, gun; + vec from, to; + vector hits; + + void process(clientinfo *ci); + }; + + struct explodeevent : timedevent + { + int id, gun; + vector hits; + + bool keepable() const { return true; } + + void process(clientinfo *ci); + }; + + struct suicideevent : gameevent + { + void process(clientinfo *ci); + }; + + struct pickupevent : gameevent + { + int ent; + + void process(clientinfo *ci); + }; + + template + struct projectilestate + { + int projs[N]; + int numprojs; + + projectilestate() : numprojs(0) {} + + void reset() { numprojs = 0; } + + void add(int val) + { + if(numprojs>=N) numprojs = 0; + projs[numprojs++] = val; + } + + bool remove(int val) + { + loopi(numprojs) if(projs[i]==val) + { + projs[i] = projs[--numprojs]; + return true; + } + return false; + } + }; + + struct gamestate : fpsstate + { + vec o; + int state, editstate; + int lastdeath, deadflush, lastspawn, lifesequence; + int lastshot; + projectilestate<8> rockets, grenades; + int frags, flags, deaths, teamkills, shotdamage, damage, tokens; + int lasttimeplayed, timeplayed; + float effectiveness; + + gamestate() : state(CS_DEAD), editstate(CS_DEAD), lifesequence(0) {} + + bool isalive(int gamemillis) + { + return state==CS_ALIVE || (state==CS_DEAD && gamemillis - lastdeath <= DEATHMILLIS); + } + + bool waitexpired(int gamemillis) + { + return gamemillis - lastshot >= gunwait; + } + + void reset() + { + if(state!=CS_SPECTATOR) state = editstate = CS_DEAD; + maxhealth = 100; + maxarmour = 50; + rockets.reset(); + grenades.reset(); + + timeplayed = 0; + effectiveness = 0; + frags = flags = deaths = teamkills = shotdamage = damage = tokens = 0; + + lastdeath = 0; + + respawn(); + } + + void respawn() + { + fpsstate::respawn(); + o = vec(-1e10f, -1e10f, -1e10f); + deadflush = 0; + lastspawn = -1; + lastshot = 0; + tokens = 0; + } + + void reassign() + { + respawn(); + rockets.reset(); + grenades.reset(); + } + }; + + struct savedscore + { + uint ip; + string name; + int frags, flags, deaths, teamkills, shotdamage, damage; + int timeplayed; + float effectiveness; + + void save(gamestate &gs) + { + frags = gs.frags; + flags = gs.flags; + deaths = gs.deaths; + teamkills = gs.teamkills; + shotdamage = gs.shotdamage; + damage = gs.damage; + timeplayed = gs.timeplayed; + effectiveness = gs.effectiveness; + } + + void restore(gamestate &gs) + { + gs.frags = frags; + gs.flags = flags; + gs.deaths = deaths; + gs.teamkills = teamkills; + gs.shotdamage = shotdamage; + gs.damage = damage; + gs.timeplayed = timeplayed; + gs.effectiveness = effectiveness; + } + }; + + extern int gamemillis, nextexceeded; + + struct clientinfo + { + int clientnum, ownernum, connectmillis, sessionid, overflow; + string name, team, mapvote; + int playermodel; + int modevote; + int privilege; + bool connected, local, timesync; + int gameoffset, lastevent, pushed, exceeded; + gamestate state; + vector events; + vector position, messages; + uchar *wsdata; + int wslen; + vector bots; + int ping, aireinit; + string clientmap; + int mapcrc; + bool warned, gameclip; + ENetPacket *getdemo, *getmap, *clipboard; + int lastclipboard, needclipboard; + int connectauth; + uint authreq; + string authname, authdesc; + void *authchallenge; + int authkickvictim; + char *authkickreason; + + clientinfo() : getdemo(NULL), getmap(NULL), clipboard(NULL), authchallenge(NULL), authkickreason(NULL) { reset(); } + ~clientinfo() { events.deletecontents(); cleanclipboard(); cleanauth(); } + + void addevent(gameevent *e) + { + if(state.state==CS_SPECTATOR || events.length()>100) delete e; + else events.add(e); + } + + enum + { + PUSHMILLIS = 3000 + }; + + int calcpushrange() + { + ENetPeer *peer = getclientpeer(ownernum); + return PUSHMILLIS + ((peer) ? (int) (peer->roundTripTime + peer->roundTripTimeVariance) : (int) ENET_PEER_DEFAULT_ROUND_TRIP_TIME); + } + + bool checkpushed(int millis, int range) + { + return millis >= pushed - range && millis <= pushed + range; + } + + void scheduleexceeded() + { + if(state.state!=CS_ALIVE || !exceeded) return; + int range = calcpushrange(); + if(!nextexceeded || exceeded + range < nextexceeded) nextexceeded = exceeded + range; + } + + void setexceeded() + { + if(state.state==CS_ALIVE && !exceeded && !checkpushed(gamemillis, calcpushrange())) exceeded = gamemillis; + scheduleexceeded(); + } + + void setpushed() + { + pushed = max(pushed, gamemillis); + if(exceeded && checkpushed(exceeded, calcpushrange())) exceeded = 0; + } + + bool checkexceeded() + { + return state.state==CS_ALIVE && exceeded && gamemillis > exceeded + calcpushrange(); + } + + void mapchange() + { + mapvote[0] = 0; + modevote = INT_MAX; + state.reset(); + events.deletecontents(); + overflow = 0; + timesync = false; + lastevent = 0; + exceeded = 0; + pushed = 0; + clientmap[0] = '\0'; + mapcrc = 0; + warned = false; + gameclip = false; + } + + void reassign() + { + state.reassign(); + events.deletecontents(); + timesync = false; + lastevent = 0; + } + + void cleanclipboard(bool fullclean = true) + { + if(clipboard) { if(--clipboard->referenceCount <= 0) enet_packet_destroy(clipboard); clipboard = NULL; } + if(fullclean) lastclipboard = 0; + } + + void cleanauthkick() + { + authkickvictim = -1; + DELETEA(authkickreason); + } + + void cleanauth(bool full = true) + { + authreq = 0; + if(authchallenge) { freechallenge(authchallenge); authchallenge = NULL; } + if(full) cleanauthkick(); + } + + void reset() + { + name[0] = team[0] = 0; + playermodel = -1; + privilege = PRIV_NONE; + connected = local = false; + connectauth = 0; + position.setsize(0); + messages.setsize(0); + ping = 0; + aireinit = 0; + needclipboard = 0; + cleanclipboard(); + cleanauth(); + mapchange(); + } + + int geteventmillis(int servmillis, int clientmillis) + { + if(!timesync || (events.empty() && state.waitexpired(servmillis))) + { + timesync = true; + gameoffset = servmillis - clientmillis; + return servmillis; + } + else return gameoffset + clientmillis; + } + }; + + struct ban + { + int time, expire; + uint ip; + }; + + namespace aiman + { + extern void removeai(clientinfo *ci); + extern void clearai(); + extern void checkai(); + extern void reqadd(clientinfo *ci, int skill); + extern void reqdel(clientinfo *ci); + extern void setbotlimit(clientinfo *ci, int limit); + extern void setbotbalance(clientinfo *ci, bool balance); + extern void changemap(); + extern void addclient(clientinfo *ci); + extern void changeteam(clientinfo *ci); + } + + #define MM_MODE 0xF + #define MM_AUTOAPPROVE 0x1000 + #define MM_PRIVSERV (MM_MODE | MM_AUTOAPPROVE) + #define MM_PUBSERV ((1< allowedips; + vector bannedips; + + void addban(uint ip, int expire) + { + allowedips.removeobj(ip); + ban b; + b.time = totalmillis; + b.expire = totalmillis + expire; + b.ip = ip; + loopv(bannedips) if(bannedips[i].expire - b.expire > 0) { bannedips.insert(i, b); return; } + bannedips.add(b); + } + + vector connects, clients, bots; + + void kickclients(uint ip, clientinfo *actor = NULL, int priv = PRIV_NONE) + { + loopvrev(clients) + { + clientinfo &c = *clients[i]; + if(c.state.aitype != AI_NONE || c.privilege >= PRIV_ADMIN || c.local) continue; + if(actor && ((c.privilege > priv && !actor->local) || c.clientnum == actor->clientnum)) continue; + //~if(getclientip(c.clientnum) == ip) disconnect_client(c.clientnum, DISC_KICK); + } + } + + struct maprotation + { + static int exclude; + int modes; + string map; + + int calcmodemask() const { return modes&(1< maprotations; + int curmaprotation = 0; + + VAR(lockmaprotation, 0, 0, 2); + + void maprotationreset() + { + maprotations.setsize(0); + curmaprotation = 0; + maprotation::exclude = 0; + } + + void nextmaprotation() + { + curmaprotation++; + if(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes) return; + do curmaprotation--; + while(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes); + curmaprotation++; + } + + int findmaprotation(int mode, const char *map) + { + for(int i = max(curmaprotation, 0); i < maprotations.length(); i++) + { + maprotation &rot = maprotations[i]; + if(!rot.modes) break; + if(rot.match(mode, map)) return i; + } + int start; + for(start = max(curmaprotation, 0) - 1; start >= 0; start--) if(!maprotations[start].modes) break; + start++; + for(int i = start; i < curmaprotation; i++) + { + maprotation &rot = maprotations[i]; + if(!rot.modes) break; + if(rot.match(mode, map)) return i; + } + int best = -1; + loopv(maprotations) + { + maprotation &rot = maprotations[i]; + if(rot.match(mode, map) && (best < 0 || maprotations[best].includes(rot))) best = i; + } + return best; + } + + bool searchmodename(const char *haystack, const char *needle) + { + if(!needle[0]) return true; + do + { + if(needle[0] != '.') + { + haystack = strchr(haystack, needle[0]); + if(!haystack) break; + haystack++; + } + const char *h = haystack, *n = needle+1; + for(; *h && *n; h++) + { + if(*h == *n) n++; + else if(*h != ' ') break; + } + if(!*n) return true; + if(*n == '.') return !*h; + } while(needle[0] != '.'); + return false; + } + + int genmodemask(vector &modes) + { + int modemask = 0; + loopv(modes) + { + const char *mode = modes[i]; + int op = mode[0]; + switch(mode[0]) + { + case '*': + modemask |= 1< modes, maps; + for(int i = 0; i + 1 < numargs; i += 2) + { + explodelist(args[i].getstr(), modes); + explodelist(args[i+1].getstr(), maps); + int modemask = genmodemask(modes); + if(maps.length()) loopvj(maps) addmaprotation(modemask, maps[j]); + else addmaprotation(modemask, ""); + modes.deletearrays(); + maps.deletearrays(); + } + if(maprotations.length() && maprotations.last().modes) + { + maprotation &rot = maprotations.add(); + rot.modes = 0; + rot.map[0] = '\0'; + } + } + + COMMAND(maprotationreset, ""); + COMMANDN(maprotation, addmaprotations, "ss2V"); + + struct demofile + { + string info; + uchar *data; + int len; + }; + + vector demos; + + bool demonextmatch = false; + stream *demotmp = NULL, *demorecord = NULL, *demoplayback = NULL; + int nextplayback = 0; + + VAR(maxdemos, 0, 5, 25); + VAR(maxdemosize, 0, 16, 31); + VAR(restrictdemos, 0, 1, 1); + VARF(autorecorddemo, 0, 0, 1, demonextmatch = autorecorddemo!=0); + + VAR(restrictpausegame, 0, 1, 1); + VAR(restrictgamespeed, 0, 1, 1); + + SVAR(serverdesc, ""); + SVAR(serverpass, ""); + SVAR(adminpass, ""); + VARF(publicserver, 0, 0, 2, { switch(publicserver) { case 0: default: mastermask = MM_PRIVSERV; break; @@ -625,3096 +625,3093 @@ namespace server case 2: mastermask = MM_COOPSERV; break; } }); - SVAR(servermotd, ""); - - struct teamkillkick - { - int modes, limit, ban; - - bool match(int mode) const - { - return (modes&(1<<(mode-STARTGAMEMODE)))!=0; - } - - bool includes(const teamkillkick &tk) const - { - return tk.modes != modes && (tk.modes & modes) == tk.modes; - } - }; - vector teamkillkicks; - - void teamkillkickreset() - { - teamkillkicks.setsize(0); - } - - void addteamkillkick(char *modestr, int *limit, int *ban) - { - vector modes; - explodelist(modestr, modes); - teamkillkick &kick = teamkillkicks.add(); - kick.modes = genmodemask(modes); - kick.limit = *limit; - kick.ban = *ban > 0 ? *ban*60000 : (*ban < 0 ? 0 : 30*60000); - modes.deletearrays(); - } - - COMMAND(teamkillkickreset, ""); - COMMANDN(teamkillkick, addteamkillkick, "sii"); - - struct teamkillinfo - { - uint ip; - int teamkills; - }; - vector teamkills; - bool shouldcheckteamkills = false; - - void addteamkill(clientinfo *actor, clientinfo *victim, int n) - { - if(!m_timed || actor->state.aitype != AI_NONE || actor->local || actor->privilege || (victim && victim->state.aitype != AI_NONE)) return; - shouldcheckteamkills = true; - uint ip = getclientip(actor->clientnum); - loopv(teamkills) if(teamkills[i].ip == ip) - { - teamkills[i].teamkills += n; - return; - } - teamkillinfo &tk = teamkills.add(); - tk.ip = ip; - tk.teamkills = n; - } - - void checkteamkills() - { - teamkillkick *kick = NULL; - if(m_timed) loopv(teamkillkicks) if(teamkillkicks[i].match(gamemode) && (!kick || kick->includes(teamkillkicks[i]))) - kick = &teamkillkicks[i]; - if(kick) loopvrev(teamkills) - { - teamkillinfo &tk = teamkills[i]; - if(tk.teamkills >= kick->limit) - { - if(kick->ban > 0) addban(tk.ip, kick->ban); - kickclients(tk.ip); - teamkills.removeunordered(i); - } - } - shouldcheckteamkills = false; - } - - void *newclientinfo() { return new clientinfo; } - void deleteclientinfo(void *ci) { delete (clientinfo *)ci; } - - clientinfo *getinfo(int n) - { - if(n < MAXCLIENTS) return (clientinfo *)getclientinfo(n); - n -= MAXCLIENTS; - return bots.inrange(n) ? bots[n] : NULL; - } - - uint mcrc = 0; - vector ments; - vector sents; - vector scores; - - int msgsizelookup(int msg) - { - static int sizetable[NUMMSG] = { -1 }; - if(sizetable[0] < 0) - { - memset(sizetable, -1, sizeof(sizetable)); - for(const int *p = msgsizes; *p >= 0; p += 2) sizetable[p[0]] = p[1]; - } - return msg >= 0 && msg < NUMMSG ? sizetable[msg] : -1; - } - - const char *modename(int n, const char *unknown) - { - if(m_valid(n)) return gamemodes[n - STARTGAMEMODE].name; - return unknown; - } - - const char *mastermodename(int n, const char *unknown) - { - return (n>=MM_START && size_t(n-MM_START)clientnum!=exclude && (!nospec || ci->state.state!=CS_SPECTATOR || (priv && (ci->privilege || ci->local))) && (!noai || ci->state.aitype == AI_NONE)) n++; - } - return n; - } - - bool duplicatename(clientinfo *ci, const char *name) - { - if(!name) name = ci->name; - loopv(clients) if(clients[i]!=ci && !strcmp(name, clients[i]->name)) return true; - return false; - } - - const char *colorname(clientinfo *ci, const char *name = NULL) - { - if(!name) name = ci->name; - if(name[0] && !duplicatename(ci, name) && ci->state.aitype == AI_NONE) return name; - static string cname[3]; - static int cidx = 0; - cidx = (cidx+1)%3; - formatstring(cname[cidx], ci->state.aitype == AI_NONE ? "%s \fs\f5(%d)\fr" : "%s \fs\f5[%d]\fr", name, ci->clientnum); - return cname[cidx]; - } - - bool canspawnitem(int type) { return !m_noitems && (type>=I_SHELLS && type<=I_QUAD && (!m_noammo || typeI_CARTRIDGES)); } - - int spawntime(int type) - { - int np = numclients(-1, true, false); - np = np<3 ? 4 : (np>4 ? 2 : 3); // spawn times are dependent on number of players - int sec = 0; - switch(type) - { - case I_SHELLS: - case I_BULLETS: - case I_ROCKETS: - case I_ROUNDS: - case I_GRENADES: - case I_CARTRIDGES: sec = np*4; break; - case I_HEALTH: sec = np*5; break; - case I_TINYHEALTH: sec = np*5; break; - case I_TINYARMOUR: sec = np*5; break; - case I_GREENARMOUR: sec = 20; break; - case I_YELLOWARMOUR: sec = 30; break; - case I_BOOST: sec = 60; break; - case I_QUAD: sec = 70; break; - } - return sec*1000; - } - - bool delayspawn(int type) - { - switch(type) - { - case I_GREENARMOUR: - case I_YELLOWARMOUR: - case I_BOOST: - case I_QUAD: - return true; - default: - return false; - } - } - - bool pickup(int i, int sender) // server side item pickup, acknowledge first client that gets it - { - if((m_timed && gamemillis>=gamelimit) || !sents.inrange(i) || !sents[i].spawned) return false; - clientinfo *ci = getinfo(sender); - if(!ci) return false; - if(!ci->local && !ci->state.canpickup(sents[i].type)) - { - sendf(sender, 1, "ri3", N_ITEMACC, i, -1); - return false; - } - sents[i].spawned = false; - sents[i].spawntime = spawntime(sents[i].type); - sendf(-1, 1, "ri3", N_ITEMACC, i, sender); - ci->state.pickup(sents[i].type); - return true; - } - - static hashset teaminfos; - - void clearteaminfo() - { - teaminfos.clear(); - } - - bool teamhasplayers(const char *team) { loopv(clients) if(!strcmp(clients[i]->team, team)) return true; return false; } - - bool pruneteaminfo() - { - int oldteams = teaminfos.numelems; - enumerate(teaminfos, teaminfo, old, - if(!old.frags && !teamhasplayers(old.team)) teaminfos.remove(old.team); - ); - return teaminfos.numelems < oldteams; - } - - teaminfo *addteaminfo(const char *team) - { - teaminfo *t = teaminfos.access(team); - if(!t) - { - if(teaminfos.numelems >= MAXTEAMS && !pruneteaminfo()) return NULL; - t = &teaminfos[team]; - copystring(t->team, team, sizeof(t->team)); - t->frags = 0; - } - return t; - } - - clientinfo *choosebestclient(float &bestrank) - { - clientinfo *best = NULL; - bestrank = -1; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.timeplayed<0) continue; - float rank = ci->state.state!=CS_SPECTATOR ? ci->state.effectiveness/max(ci->state.timeplayed, 1) : -1; - if(!best || rank > bestrank) { best = ci; bestrank = rank; } - } - return best; - } - - VAR(persistteams, 0, 0, 1); - - void autoteam() - { - static const char * const teamnames[2] = {"good", "evil"}; - vector team[2]; - float teamrank[2] = {0, 0}; - for(int round = 0, remaining = clients.length(); remaining>=0; round++) - { - int first = round&1, second = (round+1)&1, selected = 0; - while(teamrank[first] <= teamrank[second]) - { - float rank; - clientinfo *ci = choosebestclient(rank); - if(!ci) break; - if(selected && rank<=0) break; - ci->state.timeplayed = -1; - team[first].add(ci); - if(rank>0) teamrank[first] += rank; - selected++; - if(rank<=0) break; - } - if(!selected) break; - remaining -= selected; - } - loopi(sizeof(team)/sizeof(team[0])) - { - addteaminfo(teamnames[i]); - loopvj(team[i]) - { - clientinfo *ci = team[i][j]; - if(!strcmp(ci->team, teamnames[i])) continue; - if(persistteams && ci->team[0]) - { - addteaminfo(ci->team); - continue; - } - copystring(ci->team, teamnames[i], MAXTEAMLEN+1); - sendf(-1, 1, "riisi", N_SETTEAM, ci->clientnum, teamnames[i], -1); - } - } - } - - struct teamrank - { - const char *name; - float rank; - int clients; - - teamrank(const char *name) : name(name), rank(0), clients(0) {} - }; - - const char *chooseworstteam(const char *suggest = NULL, clientinfo *exclude = NULL) - { - teamrank teamranks[2] = { teamrank("good"), teamrank("evil") }; - const int numteams = sizeof(teamranks)/sizeof(teamranks[0]); - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci==exclude || ci->state.aitype!=AI_NONE || ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - ci->state.lasttimeplayed = lastmillis; - - loopj(numteams) if(!strcmp(ci->team, teamranks[j].name)) - { - teamrank &ts = teamranks[j]; - ts.rank += ci->state.effectiveness/max(ci->state.timeplayed, 1); - ts.clients++; - break; - } - } - teamrank *worst = &teamranks[numteams-1]; - loopi(numteams-1) - { - teamrank &ts = teamranks[i]; - if(ts.rank < worst->rank || (ts.rank == worst->rank && ts.clients < worst->clients)) worst = &ts; - } - return worst->name; - } - - void prunedemos(int extra = 0) - { - int n = clamp(demos.length() + extra - maxdemos, 0, demos.length()); - if(n <= 0) return; - loopi(n) delete[] demos[i].data; - demos.remove(0, n); - } - - void adddemo() - { - if(!demotmp) return; - int len = (int)min(demotmp->size(), stream::offset((maxdemosize<<20) + 0x10000)); - demofile &d = demos.add(); - time_t t = time(NULL); - char *timestr = ctime(&t), *trim = timestr + strlen(timestr); - while(trim>timestr && iscubespace(*--trim)) *trim = '\0'; - formatstring(d.info, "%s: %s, %s, %.2f%s", timestr, modename(gamemode), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB"); - sendservmsgf("demo \"%s\" recorded", d.info); - d.data = new uchar[len]; - d.len = len; - demotmp->seek(0, SEEK_SET); - demotmp->read(d.data, len); - DELETEP(demotmp); - } - - void enddemorecord() - { - if(!demorecord) return; - - DELETEP(demorecord); - - if(!demotmp) return; - if(!maxdemos || !maxdemosize) { DELETEP(demotmp); return; } - - prunedemos(1); - adddemo(); - } - - void writedemo(int chan, void *data, int len) - { - if(!demorecord) return; - int stamp[3] = { gamemillis, chan, len }; - lilswap(stamp, 3); - demorecord->write(stamp, sizeof(stamp)); - demorecord->write(data, len); - if(demorecord->rawtell() >= (maxdemosize<<20)) enddemorecord(); - } - - void recordpacket(int chan, void *data, int len) - { - writedemo(chan, data, len); - } - - int welcomepacket(packetbuf &p, clientinfo *ci); - void sendwelcome(clientinfo *ci); - - void setupdemorecord() - { - if(!m_mp(gamemode) || m_edit) return; - - demotmp = opentempfile("demorecord", "w+b"); - if(!demotmp) return; - - stream *f = opengzfile(NULL, "wb", demotmp); - if(!f) { DELETEP(demotmp); return; } - - sendservmsg("recording demo"); - - demorecord = f; - - demoheader hdr; - memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic)); - hdr.version = DEMO_VERSION; - hdr.protocol = PROTOCOL_VERSION; - lilswap(&hdr.version, 2); - demorecord->write(&hdr, sizeof(demoheader)); - - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - welcomepacket(p, NULL); - writedemo(1, p.buf, p.len); - } - - void listdemos(int cn) - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_SENDDEMOLIST); - putint(p, demos.length()); - loopv(demos) sendstring(demos[i].info, p); - sendpacket(cn, 1, p.finalize()); - } - - void cleardemos(int n) - { - if(!n) - { - loopv(demos) delete[] demos[i].data; - demos.shrink(0); - sendservmsg("cleared all demos"); - } - else if(demos.inrange(n-1)) - { - delete[] demos[n-1].data; - demos.remove(n-1); - sendservmsgf("cleared demo %d", n); - } - } - - static void freegetmap(ENetPacket *packet) - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->getmap == packet) ci->getmap = NULL; - } - } - - static void freegetdemo(ENetPacket *packet) - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->getdemo == packet) ci->getdemo = NULL; - } - } - - void senddemo(clientinfo *ci, int num, int tag) - { - if(ci->getdemo) return; - if(!num) num = demos.length(); - if(!demos.inrange(num-1)) return; - demofile &d = demos[num-1]; - if((ci->getdemo = sendf(ci->clientnum, 2, "riim", N_SENDDEMO, tag, d.len, d.data))) - ci->getdemo->freeCallback = freegetdemo; - } - - void enddemoplayback() - { - if(!demoplayback) return; - DELETEP(demoplayback); - - loopv(clients) sendf(clients[i]->clientnum, 1, "ri3", N_DEMOPLAYBACK, 0, clients[i]->clientnum); - - sendservmsg("demo playback finished"); - - loopv(clients) sendwelcome(clients[i]); - } - - SVARP(demodir, "demo"); - - const char *getdemofile(const char *file, bool init) - { - if(!demodir[0]) return NULL; - static string buf; - copystring(buf, demodir); - int dirlen = strlen(buf); - if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; } - if(init) - { - const char *dir = findfile(buf, "w"); - if(!fileexists(dir, "w")) createdir(dir); - } - concatstring(buf, file); - return buf; - } - - void setupdemoplayback() - { - if(demoplayback) return; - demoheader hdr; - string msg; - msg[0] = '\0'; - string file; - copystring(file, smapname); - int len = strlen(file); - if(len < 4 || strcasecmp(&file[len-4], ".dmo")) concatstring(file, ".dmo"); - if(const char *buf = getdemofile(file, false)) demoplayback = opengzfile(buf, "rb"); - if(!demoplayback) demoplayback = opengzfile(file, "rb"); - if(!demoplayback) formatstring(msg, "could not read demo \"%s\"", file); - else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic))) - formatstring(msg, "\"%s\" is not a demo file", file); - else - { - lilswap(&hdr.version, 2); - if(hdr.version!=DEMO_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.versionread(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) - { - enddemoplayback(); - return; - } - lilswap(&nextplayback, 1); - } - - void readdemo() - { - if(!demoplayback) return; - while(gamemillis>=nextplayback) - { - int chan, len; - if(demoplayback->read(&chan, sizeof(chan))!=sizeof(chan) || - demoplayback->read(&len, sizeof(len))!=sizeof(len)) - { - enddemoplayback(); - return; - } - lilswap(&chan, 1); - lilswap(&len, 1); - ENetPacket *packet = enet_packet_create(NULL, len+1, 0); - if(!packet || demoplayback->read(packet->data+1, len)!=size_t(len)) - { - if(packet) enet_packet_destroy(packet); - enddemoplayback(); - return; - } - packet->data[0] = N_DEMOPACKET; - sendpacket(-1, chan, packet); - if(!packet->referenceCount) enet_packet_destroy(packet); - if(!demoplayback) break; - if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) - { - enddemoplayback(); - return; - } - lilswap(&nextplayback, 1); - } - } - - void timeupdate(int secs) - { - if(!demoplayback) return; - if(secs <= 0) interm = -1; - else gamelimit = max(gamelimit, nextplayback + secs*1000); - } - - void seekdemo(char *t) - { - if(!demoplayback) return; - bool rev = *t == '-'; - if(rev) t++; - int mins = strtoul(t, &t, 10), secs = 0, millis = 0; - if(*t == ':') secs = strtoul(t+1, &t, 10); - else { secs = mins; mins = 0; } - if(*t == '.') millis = strtoul(t+1, &t, 10); - int offset = max(millis + (mins*60 + secs)*1000, 0), prevmillis = gamemillis; - if(rev) while(gamelimit - offset > gamemillis) - { - gamemillis = gamelimit - offset; - readdemo(); - } - else if(offset > gamemillis) - { - gamemillis = offset; - readdemo(); - } - if(gamemillis > prevmillis) - { - if(!interm) sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); -#ifndef STANDALONE - cleardamagescreen(); -#endif - } - } - - ICOMMAND(seekdemo, "sN$", (char *t, int *numargs, ident *id), - { - if(*numargs > 0) seekdemo(t); - else - { - int secs = gamemillis/1000; - defformatstring(str, "%d:%02d.%03d", secs/60, secs%60, gamemillis%1000); - if(*numargs < 0) result(str); - else printsvar(id, str); - } - }); - - void stopdemo() - { - if(m_demo) enddemoplayback(); - else enddemorecord(); - } - - void pausegame(bool val, clientinfo *ci = NULL) - { - if(gamepaused==val) return; - gamepaused = val; - sendf(-1, 1, "riii", N_PAUSEGAME, gamepaused ? 1 : 0, ci ? ci->clientnum : -1); - } - - void checkpausegame() - { - if(!gamepaused) return; - int admins = 0; - loopv(clients) if(clients[i]->privilege >= (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) || clients[i]->local) admins++; - if(!admins) pausegame(false); - } - - void forcepaused(bool paused) - { - pausegame(paused); - } - - bool ispaused() { return gamepaused; } - - void changegamespeed(int val, clientinfo *ci = NULL) - { - val = clamp(val, 10, 1000); - if(gamespeed==val) return; - gamespeed = val; - sendf(-1, 1, "riii", N_GAMESPEED, gamespeed, ci ? ci->clientnum : -1); - } - - void forcegamespeed(int speed) - { - changegamespeed(speed); - } - - int scaletime(int t) { return t*gamespeed; } - - SVAR(serverauth, ""); - - struct userkey - { - char *name; - char *desc; - - userkey() : name(NULL), desc(NULL) {} - userkey(char *name, char *desc) : name(name), desc(desc) {} - }; - - static inline uint hthash(const userkey &k) { return ::hthash(k.name); } - static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } - - struct userinfo : userkey - { - void *pubkey; - int privilege; - - userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} - ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } - }; - hashset users; - - void adduser(char *name, char *desc, char *pubkey, char *priv) - { - userkey key(name, desc); - userinfo &u = users[key]; - if(u.pubkey) { freepubkey(u.pubkey); u.pubkey = NULL; } - if(!u.name) u.name = newstring(name); - if(!u.desc) u.desc = newstring(desc); - u.pubkey = parsepubkey(pubkey); - switch(priv[0]) - { - case 'a': case 'A': u.privilege = PRIV_ADMIN; break; - case 'm': case 'M': default: u.privilege = PRIV_AUTH; break; - case 'n': case 'N': u.privilege = PRIV_NONE; break; - } - } - COMMAND(adduser, "ssss"); - - void clearusers() - { - users.clear(); - } - COMMAND(clearusers, ""); - - void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen) - { - char buf[2*sizeof(string)]; - formatstring(buf, "%d %d ", cn, sessionid); - concatstring(buf, pwd, sizeof(buf)); - if(!hashstring(buf, result, maxlen)) *result = '\0'; - } - - bool checkpassword(clientinfo *ci, const char *wanted, const char *given) - { - string hash; - hashpassword(ci->clientnum, ci->sessionid, wanted, hash, sizeof(hash)); - return !strcmp(hash, given); - } - - void revokemaster(clientinfo *ci) - { - ci->privilege = PRIV_NONE; - if(ci->state.state==CS_SPECTATOR && !ci->local) aiman::removeai(ci); - } - - extern void connected(clientinfo *ci); - - bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false) - { - if(authname && !val) return false; - const char *name = ""; - if(val) - { - bool haspass = adminpass[0] && checkpassword(ci, adminpass, pass); - int wantpriv = ci->local || haspass ? PRIV_ADMIN : authpriv; - if(wantpriv <= ci->privilege) return true; - else if(wantpriv <= PRIV_MASTER && !force) - { - if(ci->state.state==CS_SPECTATOR) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Spectators may not claim master."); - return false; - } - loopv(clients) if(ci!=clients[i] && clients[i]->privilege) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Master is already claimed."); - return false; - } - if(!authname && !(mastermask&MM_AUTOAPPROVE) && !ci->privilege && !ci->local) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "This server requires you to use the \"/auth\" command to claim master."); - return false; - } - } - if(trial) return true; - ci->privilege = wantpriv; - name = privname(ci->privilege); - } - else - { - if(!ci->privilege) return false; - if(trial) return true; - name = privname(ci->privilege); - revokemaster(ci); - } - bool hasmaster = false; - loopv(clients) if(clients[i]->local || clients[i]->privilege >= PRIV_MASTER) hasmaster = true; - if(!hasmaster) - { - mastermode = MM_OPEN; - allowedips.shrink(0); - } - string msg; - if(val && authname) - { - if(authdesc && authdesc[0]) formatstring(msg, "%s claimed %s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), name, authname, authdesc); - else formatstring(msg, "%s claimed %s as '\fs\f5%s\fr'", colorname(ci), name, authname); - } - else formatstring(msg, "%s %s %s", colorname(ci), val ? "claimed" : "relinquished", name); - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_SERVMSG); - sendstring(msg, p); - putint(p, N_CURRENTMASTER); - putint(p, mastermode); - loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) - { - putint(p, clients[i]->clientnum); - putint(p, clients[i]->privilege); - } - putint(p, -1); - sendpacket(-1, 1, p.finalize()); - checkpausegame(); - return true; - } - - bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false) - { - int priv = ci->privilege; - if(authname) - { - if(priv >= authpriv || ci->local) authname = authdesc = NULL; - else priv = authpriv; - } - if((priv || ci->local) && ci->clientnum!=victim) - { - clientinfo *vinfo = (clientinfo *)getclientinfo(victim); - if(vinfo && vinfo->connected && (priv >= vinfo->privilege || ci->local) && vinfo->privilege < PRIV_ADMIN && !vinfo->local) - { - if(trial) return true; - string kicker; - if(authname) - { - if(authdesc && authdesc[0]) formatstring(kicker, "%s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), authname, authdesc); - else formatstring(kicker, "%s as '\fs\f5%s\fr'", colorname(ci), authname); - } - else copystring(kicker, colorname(ci)); - if(reason && reason[0]) sendservmsgf("%s kicked %s because: %s", kicker, colorname(vinfo), reason); - else sendservmsgf("%s kicked %s", kicker, colorname(vinfo)); - uint ip = getclientip(victim); - addban(ip, 4*60*60000); - kickclients(ip, ci, priv); - } - } - return false; - } - - savedscore *findscore(clientinfo *ci, bool insert) - { - uint ip = getclientip(ci->clientnum); - if(!ip && !ci->local) return 0; - if(!insert) - { - loopv(clients) - { - clientinfo *oi = clients[i]; - if(oi->clientnum != ci->clientnum && getclientip(oi->clientnum) == ip && !strcmp(oi->name, ci->name)) - { - oi->state.timeplayed += lastmillis - oi->state.lasttimeplayed; - oi->state.lasttimeplayed = lastmillis; - static savedscore curscore; - curscore.save(oi->state); - return &curscore; - } - } - } - loopv(scores) - { - savedscore &sc = scores[i]; - if(sc.ip == ip && !strcmp(sc.name, ci->name)) return ≻ - } - if(!insert) return 0; - savedscore &sc = scores.add(); - sc.ip = ip; - copystring(sc.name, ci->name); - return ≻ - } - - void savescore(clientinfo *ci) - { - savedscore *sc = findscore(ci, true); - if(sc) sc->save(ci->state); - } - - static struct msgfilter - { - uchar msgmask[NUMMSG]; - - msgfilter(int msg, ...) - { - memset(msgmask, 0, sizeof(msgmask)); - va_list msgs; - va_start(msgs, msg); - for(uchar val = 1; msg < NUMMSG; msg = va_arg(msgs, int)) - { - if(msg < 0) val = uchar(-msg); - else msgmask[msg] = val; - } - va_end(msgs); - } - - uchar operator[](int msg) const { return msg >= 0 && msg < NUMMSG ? msgmask[msg] : 0; } - } msgfilter(-1, N_CONNECT, N_SERVINFO, N_INITCLIENT, N_WELCOME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, N_DIED, - N_SPAWNSTATE, N_FORCEDEATH, N_TEAMINFO, N_ITEMACC, N_ITEMSPAWN, N_TIMEUP, N_CDIS, N_CURRENTMASTER, N_PONG, N_RESUME, - N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_SENDMAP, - N_CLIENT, N_AUTHCHAL, N_INITAI, N_EXPIRETOKENS, N_DROPTOKENS, N_STEALTOKENS, N_DEMOPACKET, -2, N_REMIP, - N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, -3, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, - N_DELCUBE, N_EDITVAR, N_EDITVSLOT, N_UNDO, N_REDO, -4, N_POS, NUMMSG), - connectfilter(-1, N_CONNECT, -2, N_AUTHANS, -3, N_PING, NUMMSG); - - int checktype(int type, clientinfo *ci) - { - if(ci) - { - if(!ci->connected) switch(connectfilter[type]) - { - // allow only before authconnect - case 1: return !ci->connectauth ? type : -1; - // allow only during authconnect - case 2: return ci->connectauth ? type : -1; - // always allow - case 3: return type; - // never allow - default: return -1; - } - if(ci->local) return type; - } - switch(msgfilter[type]) - { - // server-only messages - case 1: return ci ? -1 : type; - // only allowed in coop-edit - case 2: if(m_edit) break; return -1; - // only allowed in coop-edit, no overflow check - case 3: return m_edit ? type : -1; - // no overflow check - case 4: return type; - } - if(ci && ++ci->overflow >= 200) return -2; - return type; - } - - struct worldstate - { - int uses, len; - uchar *data; - - worldstate() : uses(0), len(0), data(NULL) {} - - void setup(int n) { len = n; data = new uchar[n]; } - void cleanup() { DELETEA(data); len = 0; } - bool contains(const uchar *p) const { return p >= data && p < &data[len]; } - }; - vector worldstates; - bool reliablemessages = false; - - void cleanworldstate(ENetPacket *packet) - { - loopv(worldstates) - { - worldstate &ws = worldstates[i]; - if(!ws.contains(packet->data)) continue; - ws.uses--; - if(ws.uses <= 0) - { - ws.cleanup(); - worldstates.removeunordered(i); - } - break; - } - } - - void flushclientposition(clientinfo &ci) - { - if(ci.position.empty() || (!hasnonlocalclients() && !demorecord)) return; - packetbuf p(ci.position.length(), 0); - p.put(ci.position.getbuf(), ci.position.length()); - ci.position.setsize(0); - sendpacket(-1, 0, p.finalize(), ci.ownernum); - } - - static void sendpositions(worldstate &ws, ucharbuf &wsbuf) - { - if(wsbuf.empty()) return; - int wslen = wsbuf.length(); - recordpacket(0, wsbuf.buf, wslen); - wsbuf.put(wsbuf.buf, wslen); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - uchar *data = wsbuf.buf; - int size = wslen; - if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } - if(size <= 0) continue; - ENetPacket *packet = enet_packet_create(data, size, ENET_PACKET_FLAG_NO_ALLOCATE); - sendpacket(ci.clientnum, 0, packet); - if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } - else enet_packet_destroy(packet); - } - wsbuf.offset(wsbuf.length()); - } - - static inline void addposition(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) - { - if(bi.position.empty()) return; - if(wsbuf.length() + bi.position.length() > mtu) sendpositions(ws, wsbuf); - int offset = wsbuf.length(); - wsbuf.put(bi.position.getbuf(), bi.position.length()); - bi.position.setsize(0); - int len = wsbuf.length() - offset; - if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } - else ci.wslen += len; - } - - static void sendmessages(worldstate &ws, ucharbuf &wsbuf) - { - if(wsbuf.empty()) return; - int wslen = wsbuf.length(); - recordpacket(1, wsbuf.buf, wslen); - wsbuf.put(wsbuf.buf, wslen); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - uchar *data = wsbuf.buf; - int size = wslen; - if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } - if(size <= 0) continue; - ENetPacket *packet = enet_packet_create(data, size, (reliablemessages ? ENET_PACKET_FLAG_RELIABLE : 0) | ENET_PACKET_FLAG_NO_ALLOCATE); - sendpacket(ci.clientnum, 1, packet); - if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } - else enet_packet_destroy(packet); - } - wsbuf.offset(wsbuf.length()); - } - - static inline void addmessages(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) - { - if(bi.messages.empty()) return; - if(wsbuf.length() + 10 + bi.messages.length() > mtu) sendmessages(ws, wsbuf); - int offset = wsbuf.length(); - putint(wsbuf, N_CLIENT); - putint(wsbuf, bi.clientnum); - putuint(wsbuf, bi.messages.length()); - wsbuf.put(bi.messages.getbuf(), bi.messages.length()); - bi.messages.setsize(0); - int len = wsbuf.length() - offset; - if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } - else ci.wslen += len; - } - - bool buildworldstate() - { - int wsmax = 0; - loopv(clients) - { - clientinfo &ci = *clients[i]; - ci.overflow = 0; - ci.wsdata = NULL; - wsmax += ci.position.length(); - if(ci.messages.length()) wsmax += 10 + ci.messages.length(); - } - if(wsmax <= 0) - { - reliablemessages = false; - return false; - } - worldstate &ws = worldstates.add(); - ws.setup(2*wsmax); - int mtu = getservermtu() - 100; - if(mtu <= 0) mtu = ws.len; - ucharbuf wsbuf(ws.data, ws.len); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - addposition(ws, wsbuf, mtu, ci, ci); - loopvj(ci.bots) addposition(ws, wsbuf, mtu, *ci.bots[j], ci); - } - sendpositions(ws, wsbuf); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - addmessages(ws, wsbuf, mtu, ci, ci); - loopvj(ci.bots) addmessages(ws, wsbuf, mtu, *ci.bots[j], ci); - } - sendmessages(ws, wsbuf); - reliablemessages = false; - if(ws.uses) return true; - ws.cleanup(); - worldstates.drop(); - return false; - } - - bool sendpackets(bool force) - { - if(clients.empty() || (!hasnonlocalclients() && !demorecord)) return false; - enet_uint32 curtime = enet_time_get()-lastsend; - if(curtime<33 && !force) return false; - bool flush = buildworldstate(); - lastsend += curtime - (curtime%33); - return flush; - } - - template - void sendstate(gamestate &gs, T &p) - { - putint(p, gs.lifesequence); - putint(p, gs.health); - putint(p, gs.maxhealth); - putint(p, gs.armour); - putint(p, gs.maxarmour); - putint(p, gs.armourtype); - putint(p, gs.gunselect); - loopi(GUN_PISTOL-GUN_SG+1) putint(p, gs.ammo[GUN_SG+i]); - } - - void spawnstate(clientinfo *ci) - { - gamestate &gs = ci->state; - gs.spawnstate(gamemode); - gs.lifesequence = (gs.lifesequence + 1)&0x7F; - } - - void sendspawn(clientinfo *ci) - { - gamestate &gs = ci->state; - spawnstate(ci); - sendf(ci->ownernum, 1, "rii8v", N_SPAWNSTATE, ci->clientnum, gs.lifesequence, - gs.health, gs.maxhealth, - gs.armour, gs.maxarmour, gs.armourtype, - gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG]); - gs.lastspawn = gamemillis; - } - - void sendwelcome(clientinfo *ci) - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - int chan = welcomepacket(p, ci); - sendpacket(ci->clientnum, chan, p.finalize()); - } - - void putinitclient(clientinfo *ci, packetbuf &p) - { - if(ci->state.aitype != AI_NONE) - { - putint(p, N_INITAI); - putint(p, ci->clientnum); - putint(p, ci->ownernum); - putint(p, ci->state.aitype); - putint(p, ci->state.skill); - putint(p, ci->playermodel); - sendstring(ci->name, p); - sendstring(ci->team, p); - } - else - { - putint(p, N_INITCLIENT); - putint(p, ci->clientnum); - sendstring(ci->name, p); - sendstring(ci->team, p); - putint(p, ci->playermodel); - } - } - - void welcomeinitclient(packetbuf &p, int exclude = -1) - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(!ci->connected || ci->clientnum == exclude) continue; - - putinitclient(ci, p); - } - } - - bool hasmap(clientinfo *ci) - { - return (m_edit && (clients.length() > 0 || ci->local)) || - (smapname[0] && (!m_timed || gamemillis < gamelimit || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || numclients(ci->clientnum, true, true, true))); - } - - int welcomepacket(packetbuf &p, clientinfo *ci) - { - putint(p, N_WELCOME); - putint(p, N_MAPCHANGE); - sendstring(smapname, p); - putint(p, gamemode); - putint(p, notgotitems ? 1 : 0); - if(!ci || (m_timed && smapname[0])) - { - putint(p, N_TIMEUP); - putint(p, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); - } - if(!notgotitems) - { - putint(p, N_ITEMLIST); - loopv(sents) if(sents[i].spawned) - { - putint(p, i); - putint(p, sents[i].type); - } - putint(p, -1); - } - bool hasmaster = false; - if(mastermode != MM_OPEN) - { - putint(p, N_CURRENTMASTER); - putint(p, mastermode); - hasmaster = true; - } - loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) - { - if(!hasmaster) - { - putint(p, N_CURRENTMASTER); - putint(p, mastermode); - hasmaster = true; - } - putint(p, clients[i]->clientnum); - putint(p, clients[i]->privilege); - } - if(hasmaster) putint(p, -1); - if(gamepaused) - { - putint(p, N_PAUSEGAME); - putint(p, 1); - putint(p, -1); - } - if(gamespeed != 100) - { - putint(p, N_GAMESPEED); - putint(p, gamespeed); - putint(p, -1); - } - if(m_teammode) - { - putint(p, N_TEAMINFO); - enumerate(teaminfos, teaminfo, t, - if(t.frags) { sendstring(t.team, p); putint(p, t.frags); } - ); - sendstring("", p); - } - if(ci) - { - putint(p, N_SETTEAM); - putint(p, ci->clientnum); - sendstring(ci->team, p); - putint(p, -1); - } - if(ci && (m_demo || m_mp(gamemode)) && ci->state.state!=CS_SPECTATOR) - { - gamestate &gs = ci->state; - spawnstate(ci); - putint(p, N_SPAWNSTATE); - putint(p, ci->clientnum); - sendstate(gs, p); - gs.lastspawn = gamemillis; - } - if(ci && ci->state.state==CS_SPECTATOR) - { - putint(p, N_SPECTATOR); - putint(p, ci->clientnum); - putint(p, 1); - sendf(-1, 1, "ri3x", N_SPECTATOR, ci->clientnum, 1, ci->clientnum); - } - if(!ci || clients.length()>1) - { - putint(p, N_RESUME); - loopv(clients) - { - clientinfo *oi = clients[i]; - if(ci && oi->clientnum==ci->clientnum) continue; - putint(p, oi->clientnum); - putint(p, oi->state.state); - putint(p, oi->state.frags); - putint(p, oi->state.flags); - putint(p, oi->state.deaths); - putint(p, oi->state.quadmillis); - sendstate(oi->state, p); - } - putint(p, -1); - welcomeinitclient(p, ci ? ci->clientnum : -1); - } - return 1; - } - - bool restorescore(clientinfo *ci) - { - //if(ci->local) return false; - savedscore *sc = findscore(ci, false); - if(sc) - { - sc->restore(ci->state); - return true; - } - return false; - } - - void sendresume(clientinfo *ci) - { - gamestate &gs = ci->state; - sendf(-1, 1, "ri3i5i6vi", N_RESUME, ci->clientnum, gs.state, - gs.frags, gs.flags, gs.deaths, gs.quadmillis, - gs.lifesequence, - gs.health, gs.maxhealth, - gs.armour, gs.maxarmour, gs.armourtype, - gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG], -1); - } - - void sendinitclient(clientinfo *ci) - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putinitclient(ci, p); - sendpacket(-1, 1, p.finalize(), ci->clientnum); - } - - void loaditems() - { - resetitems(); - notgotitems = true; - if(m_edit || !loadents(smapname, ments, &mcrc)) - return; - loopv(ments) if(canspawnitem(ments[i].type)) - { - server_entity se = { NOTUSED, 0, false }; - while(sents.length()<=i) sents.add(se); - sents[i].type = ments[i].type; - if(m_mp(gamemode) && delayspawn(sents[i].type)) sents[i].spawntime = spawntime(sents[i].type); - else sents[i].spawned = true; - } - notgotitems = false; - } - - void changemap(const char *s, int mode) - { - stopdemo(); - pausegame(false); - changegamespeed(100); - aiman::clearai(); - - gamemode = mode; - gamemillis = 0; - gamelimit = 10*60000; - interm = 0; - nextexceeded = 0; - copystring(smapname, s); - loaditems(); - scores.shrink(0); - shouldcheckteamkills = false; - teamkills.shrink(0); - loopv(clients) - { - clientinfo *ci = clients[i]; - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - } - - if(!m_mp(gamemode)) kicknonlocalclients(DISC_LOCAL); - - sendf(-1, 1, "risii", N_MAPCHANGE, smapname, gamemode, 1); - - clearteaminfo(); - if(m_teammode) autoteam(); - - if(m_timed && smapname[0]) sendf(-1, 1, "ri2", N_TIMEUP, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); - loopv(clients) - { - clientinfo *ci = clients[i]; - ci->mapchange(); - ci->state.lasttimeplayed = lastmillis; - if(m_mp(gamemode) && ci->state.state!=CS_SPECTATOR) sendspawn(ci); - } - - aiman::changemap(); - - if(m_demo) - { - if(clients.length()) setupdemoplayback(); - } - else - { - if(demonextmatch) setupdemorecord(); - demonextmatch = autorecorddemo!=0; - } - } - - void rotatemap(bool next) - { - if(!maprotations.inrange(curmaprotation)) - { - changemap("", 1); - return; - } - if(next) - { - curmaprotation = findmaprotation(gamemode, smapname); - if(curmaprotation >= 0) nextmaprotation(); - else curmaprotation = smapname[0] ? max(findmaprotation(gamemode, ""), 0) : 0; - } - maprotation &rot = maprotations[curmaprotation]; - changemap(rot.map, rot.findmode(gamemode)); - } - - struct votecount - { - char *map; - int mode, count; - votecount() {} - votecount(char *s, int n) : map(s), mode(n), count(0) {} - }; - - void checkvotes(bool force = false) - { - vector votes; - int maxvotes = 0; - loopv(clients) - { - clientinfo *oi = clients[i]; - if(oi->state.state==CS_SPECTATOR && !oi->privilege && !oi->local) continue; - if(oi->state.aitype!=AI_NONE) continue; - maxvotes++; - if(!m_valid(oi->modevote)) continue; - votecount *vc = NULL; - loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote==votes[j].mode) - { - vc = &votes[j]; - break; - } - if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote)); - vc->count++; - } - votecount *best = NULL; - loopv(votes) if(!best || votes[i].count > best->count || (votes[i].count == best->count && rnd(2))) best = &votes[i]; - if(force || (best && best->count > maxvotes/2)) - { - sendpackets(true); - if(demorecord) enddemorecord(); - if(best && (best->count > (force ? 1 : maxvotes/2))) - { - sendservmsg(force ? "vote passed by default" : "vote passed by majority"); - changemap(best->map, best->mode); - } - else rotatemap(true); - } - } - - void forcemap(const char *map, int mode) - { - stopdemo(); - if(!map[0] && !m_check(mode, M_EDIT)) - { - int idx = findmaprotation(mode, smapname); - if(idx < 0 && smapname[0]) idx = findmaprotation(mode, ""); - if(idx < 0) return; - map = maprotations[idx].map; - } - if(hasnonlocalclients()) sendservmsgf("local player forced %s on map %s", modename(mode), map[0] ? map : "[new map]"); - changemap(map, mode); - } - - void vote(const char *map, int reqmode, int sender) - { - clientinfo *ci = getinfo(sender); - if(!ci || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || (!ci->local && !m_mp(reqmode))) return; - if(!m_valid(reqmode)) return; - if(!map[0] && !m_check(reqmode, M_EDIT)) - { - int idx = findmaprotation(reqmode, smapname); - if(idx < 0 && smapname[0]) idx = findmaprotation(reqmode, ""); - if(idx < 0) return; - map = maprotations[idx].map; - } - if(lockmaprotation && !ci->local && ci->privilege < (lockmaprotation > 1 ? PRIV_ADMIN : PRIV_MASTER) && findmaprotation(reqmode, map) < 0) - { - sendf(sender, 1, "ris", N_SERVMSG, "This server has locked the map rotation."); - return; - } - copystring(ci->mapvote, map); - ci->modevote = reqmode; - if(ci->local || (ci->privilege && mastermode>=MM_VETO)) - { - sendpackets(true); - if(demorecord) enddemorecord(); - if(!ci->local || hasnonlocalclients()) - sendservmsgf("%s forced %s on map %s", colorname(ci), modename(ci->modevote), ci->mapvote[0] ? ci->mapvote : "[new map]"); - changemap(ci->mapvote, ci->modevote); - } - else - { - sendservmsgf("%s suggests %s on map %s (select map to vote)", colorname(ci), modename(reqmode), map[0] ? map : "[new map]"); - checkvotes(); - } - } - - VAR(overtime, 0, 0, 1); - - bool checkovertime() - { - if(!m_timed || !overtime) return false; - const char* topteam = NULL; - int topscore = INT_MIN; - bool tied = false; - if(m_teammode) - { - vector scores; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; - int score = 0; - if(teaminfo *ti = teaminfos.access(ci->team)) score = ti->frags; - if(!topteam || score > topscore) { topteam = ci->team; topscore = score; tied = false; } - else if(score == topscore && strcmp(ci->team, topteam)) tied = true; - } - } - else - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR) continue; - int score = ci->state.frags; - if(score > topscore) { topscore = score; tied = false; } - else if(score == topscore) tied = true; - } - } - if(!tied) return false; - sendservmsg("the game is tied with overtime"); - gamelimit = max(gamemillis, gamelimit) + 2*60000; - sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); - return true; - } - - void checkintermission(bool force = false) - { - if(gamemillis >= gamelimit && !interm && (force || !checkovertime())) - { - sendf(-1, 1, "ri2", N_TIMEUP, 0); - changegamespeed(100); - interm = gamemillis + 10000; - } - } - - void startintermission() { gamelimit = min(gamelimit, gamemillis); checkintermission(true); } - - void dodamage(clientinfo *target, clientinfo *actor, int damage, int gun, const vec &hitpush = vec(0, 0, 0)) - { - gamestate &ts = target->state; - ts.dodamage(damage); - if(target!=actor && !isteam(target->team, actor->team)) actor->state.damage += damage; - sendf(-1, 1, "ri6", N_DAMAGE, target->clientnum, actor->clientnum, damage, ts.armour, ts.health); - if(target==actor) target->setpushed(); - else if(!hitpush.iszero()) - { - ivec v(vec(hitpush).rescale(DNF)); - sendf(ts.health<=0 ? -1 : target->ownernum, 1, "ri7", N_HITPUSH, target->clientnum, gun, damage, v.x, v.y, v.z); - target->setpushed(); - } - if(ts.health<=0) - { - target->state.deaths++; - int fragvalue = (target==actor || isteam(target->team, actor->team) ? -1 : 1); - actor->state.frags += fragvalue; - if(fragvalue>0) - { - int friends = 0, enemies = 0; // note: friends also includes the fragger - if(m_teammode) loopv(clients) if(strcmp(clients[i]->team, actor->team)) enemies++; else friends++; - else { friends = 1; enemies = clients.length()-1; } - actor->state.effectiveness += fragvalue*friends/float(max(enemies, 1)); - } - teaminfo *t = m_teammode ? teaminfos.access(actor->team) : NULL; - if(t) t->frags += fragvalue; - sendf(-1, 1, "ri5", N_DIED, target->clientnum, actor->clientnum, actor->state.frags, t ? t->frags : 0); - target->position.setsize(0); - ts.state = CS_DEAD; - ts.lastdeath = gamemillis; - if(actor!=target && isteam(actor->team, target->team)) - { - actor->state.teamkills++; - addteamkill(actor, target, 1); - } - ts.deadflush = ts.lastdeath + DEATHMILLIS; - // don't issue respawn yet until DEATHMILLIS has elapsed - // ts.respawn(); - } - } - - void suicide(clientinfo *ci) - { - gamestate &gs = ci->state; - if(gs.state!=CS_ALIVE) return; - int fragvalue = -1; - ci->state.frags += fragvalue; - ci->state.deaths++; - teaminfo *t = m_teammode ? teaminfos.access(ci->team) : NULL; - if(t) t->frags += fragvalue; - sendf(-1, 1, "ri5", N_DIED, ci->clientnum, ci->clientnum, gs.frags, t ? t->frags : 0); - ci->position.setsize(0); - gs.state = CS_DEAD; - gs.lastdeath = gamemillis; - gs.respawn(); - } - - void suicideevent::process(clientinfo *ci) - { - suicide(ci); - } - - void explodeevent::process(clientinfo *ci) - { - gamestate &gs = ci->state; - switch(gun) - { - case GUN_RL: - if(!gs.rockets.remove(id)) return; - break; - - case GUN_GL: - if(!gs.grenades.remove(id)) return; - break; - - default: - return; - } - sendf(-1, 1, "ri4x", N_EXPLODEFX, ci->clientnum, gun, id, ci->ownernum); - loopv(hits) - { - hitinfo &h = hits[i]; - clientinfo *target = getinfo(h.target); - if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.dist<0 || h.dist>guns[gun].exprad) continue; - - bool dup = false; - loopj(i) if(hits[j].target==h.target) { dup = true; break; } - if(dup) continue; - - int damage = guns[gun].damage; - if(gs.quadmillis) damage *= 4; - damage = int(damage*(1-h.dist/EXP_DISTSCALE/guns[gun].exprad)); - if(target==ci) damage /= EXP_SELFDAMDIV; - dodamage(target, ci, damage, gun, h.dir); - } - } - - void shotevent::process(clientinfo *ci) - { - gamestate &gs = ci->state; - int wait = millis - gs.lastshot; - if(!gs.isalive(gamemillis) || - waitGUN_PISTOL || - gs.ammo[gun]<=0 || (guns[gun].range && from.dist(to) > guns[gun].range + 1)) - return; - if(gun!=GUN_FIST) gs.ammo[gun]--; - gs.lastshot = millis; - gs.gunwait = guns[gun].attackdelay; - sendf(-1, 1, "rii9x", N_SHOTFX, ci->clientnum, gun, id, - int(from.x*DMF), int(from.y*DMF), int(from.z*DMF), - int(to.x*DMF), int(to.y*DMF), int(to.z*DMF), - ci->ownernum); - gs.shotdamage += guns[gun].damage*(gs.quadmillis ? 4 : 1)*guns[gun].rays; - switch(gun) - { - case GUN_RL: gs.rockets.add(id); break; - case GUN_GL: gs.grenades.add(id); break; - default: - { - int totalrays = 0, maxrays = guns[gun].rays; - loopv(hits) - { - hitinfo &h = hits[i]; - clientinfo *target = getinfo(h.target); - if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.rays<1 || h.dist > guns[gun].range + 1) continue; - - totalrays += h.rays; - if(totalrays>maxrays) continue; - int damage = h.rays*guns[gun].damage; - if(gs.quadmillis) damage *= 4; - dodamage(target, ci, damage, gun, h.dir); - } - break; - } - } - } - - void pickupevent::process(clientinfo *ci) - { - gamestate &gs = ci->state; - if(m_mp(gamemode) && !gs.isalive(gamemillis)) return; - pickup(ent, ci->clientnum); - } - - bool gameevent::flush(clientinfo *ci, int fmillis) - { - process(ci); - return true; - } - - bool timedevent::flush(clientinfo *ci, int fmillis) - { - if(millis > fmillis) return false; - else if(millis >= ci->lastevent) - { - ci->lastevent = millis; - process(ci); - } - return true; - } - - void clearevent(clientinfo *ci) - { - delete ci->events.remove(0); - } - - void flushevents(clientinfo *ci, int millis) - { - while(ci->events.length()) - { - gameevent *ev = ci->events[0]; - if(ev->flush(ci, millis)) clearevent(ci); - else break; - } - } - - void processevents() - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(curtime>0 && ci->state.quadmillis) ci->state.quadmillis = max(ci->state.quadmillis-curtime, 0); - flushevents(ci, gamemillis); - } - } - - void cleartimedevents(clientinfo *ci) - { - int keep = 0; - loopv(ci->events) - { - if(ci->events[i]->keepable()) - { - if(keep < i) - { - for(int j = keep; j < i; j++) delete ci->events[j]; - ci->events.remove(keep, i - keep); - i = keep; - } - keep = i+1; - continue; - } - } - while(ci->events.length() > keep) delete ci->events.pop(); - ci->timesync = false; - } - - void serverupdate() - { - if(shouldstep && !gamepaused) - { - gamemillis += curtime; - - if(m_demo) readdemo(); - else if(!m_timed || gamemillis < gamelimit) - { - processevents(); - if(curtime) - { - loopv(sents) if(sents[i].spawntime) // spawn entities when timer reached - { - int oldtime = sents[i].spawntime; - sents[i].spawntime -= curtime; - if(sents[i].spawntime<=0) - { - sents[i].spawntime = 0; - sents[i].spawned = true; - sendf(-1, 1, "ri2", N_ITEMSPAWN, i); - } - else if(sents[i].spawntime<=10000 && oldtime>10000 && (sents[i].type==I_QUAD || sents[i].type==I_BOOST)) - { - sendf(-1, 1, "ri2", N_ANNOUNCE, sents[i].type); - } - } - } - aiman::checkai(); - } - } - - while(bannedips.length() && bannedips[0].expire-totalmillis <= 0) bannedips.remove(0); - //~loopv(connects) if(totalmillis-connects[i]->connectmillis>15000) disconnect_client(connects[i]->clientnum, DISC_TIMEOUT); - - if(nextexceeded && gamemillis > nextexceeded && (!m_timed || gamemillis < gamelimit)) - { - nextexceeded = 0; - loopvrev(clients) - { - clientinfo &c = *clients[i]; - if(c.state.aitype != AI_NONE) continue; - //~if(c.checkexceeded()) disconnect_client(c.clientnum, DISC_MSGERR); - else c.scheduleexceeded(); - } - } - - if(shouldcheckteamkills) checkteamkills(); - - if(shouldstep && !gamepaused) - { - if(m_timed && smapname[0] && gamemillis-curtime>0) checkintermission(); - if(interm > 0 && gamemillis>interm) - { - if(demorecord) enddemorecord(); - interm = -1; - checkvotes(true); - } - } - - shouldstep = clients.length() > 0; - } - - void forcespectator(clientinfo *ci) - { - if(ci->state.state==CS_ALIVE) suicide(ci); - ci->state.state = CS_SPECTATOR; - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - if(!ci->local && (!ci->privilege || ci->warned)) aiman::removeai(ci); - sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 1); - } - - struct crcinfo - { - int crc, matches; - - crcinfo() {} - crcinfo(int crc, int matches) : crc(crc), matches(matches) {} - - static bool compare(const crcinfo &x, const crcinfo &y) { return x.matches > y.matches; } - }; - - VAR(modifiedmapspectator, 0, 1, 2); - - void checkmaps(int req = -1) - { - if(m_edit || !smapname[0]) return; - vector crcs; - int total = 0, unsent = 0, invalid = 0; - if(mcrc) crcs.add(crcinfo(mcrc, clients.length() + 1)); - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE) continue; - total++; - if(!ci->clientmap[0]) - { - if(ci->mapcrc < 0) invalid++; - else if(!ci->mapcrc) unsent++; - } - else - { - crcinfo *match = NULL; - loopvj(crcs) if(crcs[j].crc == ci->mapcrc) { match = &crcs[j]; break; } - if(!match) crcs.add(crcinfo(ci->mapcrc, 1)); - else match->matches++; - } - } - if(!mcrc && total - unsent < min(total, 4)) return; - crcs.sort(crcinfo::compare); - string msg; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue; - formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); - sendf(req, 1, "ris", N_SERVMSG, msg); - if(req < 0) ci->warned = true; - } - if(crcs.length() >= 2) loopv(crcs) - { - crcinfo &info = crcs[i]; - if(i || info.matches <= crcs[i+1].matches) loopvj(clients) - { - clientinfo *ci = clients[j]; - if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue; - formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); - sendf(req, 1, "ris", N_SERVMSG, msg); - if(req < 0) ci->warned = true; - } - } - if(req < 0 && modifiedmapspectator && (mcrc || modifiedmapspectator > 1)) loopv(clients) - { - clientinfo *ci = clients[i]; - if(!ci->local && ci->warned && ci->state.state != CS_SPECTATOR) forcespectator(ci); - } - } - - bool shouldspectate(clientinfo *ci) - { - return !ci->local && ci->warned && modifiedmapspectator && (mcrc || modifiedmapspectator > 1); - } - - void unspectate(clientinfo *ci) - { - if(shouldspectate(ci)) return; - ci->state.state = CS_DEAD; - ci->state.respawn(); - ci->state.lasttimeplayed = lastmillis; - aiman::addclient(ci); - sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 0); - if(ci->clientmap[0] || ci->mapcrc) checkmaps(); - if(!hasmap(ci)) rotatemap(true); - } - - void sendservinfo(clientinfo *ci) - { - sendf(ci->clientnum, 1, "ri5ss", N_SERVINFO, ci->clientnum, PROTOCOL_VERSION, ci->sessionid, serverpass[0] ? 1 : 0, serverdesc, serverauth); - } - - void noclients() - { - bannedips.shrink(0); - aiman::clearai(); - } - - void localconnect(int n) - { - clientinfo *ci = getinfo(n); - ci->clientnum = ci->ownernum = n; - ci->connectmillis = totalmillis; - ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; - ci->local = true; - - connects.add(ci); - sendservinfo(ci); - } - - void localdisconnect(int n) - { - if(m_demo) enddemoplayback(); - clientdisconnect(n); - } - - int clientconnect(int n) - { - clientinfo *ci = getinfo(n); - ci->clientnum = ci->ownernum = n; - ci->connectmillis = totalmillis; - ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; - - connects.add(ci); - if(!m_mp(gamemode)) return DISC_LOCAL; - sendservinfo(ci); - return DISC_NONE; - } - - void clientdisconnect(int n) - { - clientinfo *ci = getinfo(n); - loopv(clients) if(clients[i]->authkickvictim == ci->clientnum) clients[i]->cleanauth(); - if(ci->connected) - { - if(ci->privilege) setmaster(ci, false); - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - savescore(ci); - sendf(-1, 1, "ri2", N_CDIS, n); - clients.removeobj(ci); - aiman::removeai(ci); - if(!numclients(-1, false, true)) noclients(); // bans clear when server empties - if(ci->local) checkpausegame(); - } - else connects.removeobj(ci); - } - - int reserveclients() { return 3; } - - extern void verifybans(); - - struct banlist - { - vector bans; - - void clear() { bans.shrink(0); } - - bool check(uint ip) - { - loopv(bans) if(bans[i].check(ip)) return true; - return false; - } - - void add(const char *ipname) - { - ipmask ban; - ban.parse(ipname); - bans.add(ban); - - verifybans(); - } - } ipbans, gbans; - - bool checkbans(uint ip) - { - loopv(bannedips) if(bannedips[i].ip==ip) return true; - return ipbans.check(ip) || gbans.check(ip); - } - - void verifybans() - { - loopvrev(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.aitype != AI_NONE || ci->local || ci->privilege >= PRIV_ADMIN) continue; - //~if(checkbans(getclientip(ci->clientnum))) disconnect_client(ci->clientnum, DISC_IPBAN); - } - } - - ICOMMAND(clearipbans, "", (), ipbans.clear()); - ICOMMAND(ipban, "s", (const char *ipname), ipbans.add(ipname)); - - int allowconnect(clientinfo *ci, const char *pwd = "") - { - if(ci->local) return DISC_NONE; - if(!m_mp(gamemode)) return DISC_LOCAL; - if(serverpass[0]) - { - if(!checkpassword(ci, serverpass, pwd)) return DISC_PASSWORD; - return DISC_NONE; - } - if(adminpass[0] && checkpassword(ci, adminpass, pwd)) return DISC_NONE; - if(numclients(-1, false, true)>=maxclients) return DISC_MAXCLIENTS; - uint ip = getclientip(ci->clientnum); - if(checkbans(ip)) return DISC_IPBAN; - if(mastermode>=MM_PRIVATE && allowedips.find(ip)<0) return DISC_PRIVATE; - return DISC_NONE; - } - - bool allowbroadcast(int n) - { - clientinfo *ci = getinfo(n); - return ci && ci->connected; - } - - clientinfo *findauth(uint id) - { - loopv(clients) if(clients[i]->authreq == id) return clients[i]; - return NULL; - } - - - void authfailed(clientinfo *ci) - { - if(!ci) return; - ci->cleanauth(); - //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); - } - - void authfailed(uint id) - { - authfailed(findauth(id)); - } - - void authsucceeded(uint id) - { - clientinfo *ci = findauth(id); - if(!ci) return; - ci->cleanauth(ci->connectauth!=0); - if(ci->connectauth) connected(ci); - if(ci->authkickvictim >= 0) - { - if(setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH, false, true)) - trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH); - ci->cleanauthkick(); - } - else setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH); - } - - void authchallenged(uint id, const char *val, const char *desc = "") - { - clientinfo *ci = findauth(id); - if(!ci) return; - sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val); - } - - uint nextauthreq = 0; - - bool tryauth(clientinfo *ci, const char *user, const char *desc) - { - ci->cleanauth(); - if(!nextauthreq) nextauthreq = 1; - ci->authreq = nextauthreq++; - filtertext(ci->authname, user, false, false, 100); - copystring(ci->authdesc, desc); - if(ci->authdesc[0]) - { - userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); - if(u) - { - uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() }; - vector buf; - ci->authchallenge = genchallenge(u->pubkey, seed, sizeof(seed), buf); - sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, ci->authreq, buf.getbuf()); - } - else ci->cleanauth(); - } - else if(!requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname)) - { - ci->cleanauth(); - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); - } - if(ci->authreq) return true; - //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); - return false; - } - - bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc) - { - if(ci->authreq != id || strcmp(ci->authdesc, desc)) - { - ci->cleanauth(); - return !ci->connectauth; - } - for(char *s = val; *s; s++) - { - if(!isxdigit(*s)) { *s = '\0'; break; } - } - if(desc[0]) - { - if(ci->authchallenge && checkchallenge(val, ci->authchallenge)) - { - userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); - if(u) - { - if(ci->connectauth) connected(ci); - if(ci->authkickvictim >= 0) - { - if(setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege, false, true)) - trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, u->privilege); - } - else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege); - } - } - ci->cleanauth(); - } - else if(!requestmasterf("confauth %u %s\n", id, val)) - { - ci->cleanauth(); - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); - } - return ci->authreq || !ci->connectauth; - } - - void masterconnected() - { - } - - void masterdisconnected() - { - loopvrev(clients) - { - clientinfo *ci = clients[i]; - if(ci->authreq) authfailed(ci); - } - } - - void processmasterinput(const char *cmd, int cmdlen, const char *args) - { - uint id; - string val; - if(sscanf(cmd, "failauth %u", &id) == 1) - authfailed(id); - else if(sscanf(cmd, "succauth %u", &id) == 1) - authsucceeded(id); - else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2) - authchallenged(id, val); - else if(matchstring(cmd, cmdlen, "cleargbans")) - gbans.clear(); - else if(sscanf(cmd, "addgban %100s", val) == 1) - gbans.add(val); - } - - void receivefile(int sender, uchar *data, int len) - { - if(!m_edit || len <= 0 || len > 4*1024*1024) return; - clientinfo *ci = getinfo(sender); - if(ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) return; - if(mapdata) DELETEP(mapdata); - mapdata = opentempfile("mapdata", "w+b"); - if(!mapdata) { sendf(sender, 1, "ris", N_SERVMSG, "failed to open temporary file for map"); return; } - mapdata->write(data, len); - sendservmsgf("[%s sent a map to server, \"/getmap\" to receive it]", colorname(ci)); - } - - void sendclipboard(clientinfo *ci) - { - if(!ci->lastclipboard || !ci->clipboard) return; - bool flushed = false; - loopv(clients) - { - clientinfo &e = *clients[i]; - if(e.clientnum != ci->clientnum && e.needclipboard - ci->lastclipboard >= 0) - { - if(!flushed) { flushserver(true); flushed = true; } - sendpacket(e.clientnum, 1, ci->clipboard); - } - } - } - - void connected(clientinfo *ci) - { - if(m_demo) enddemoplayback(); - - if(!hasmap(ci)) rotatemap(false); - - shouldstep = true; - - connects.removeobj(ci); - clients.add(ci); - - ci->connectauth = 0; - ci->connected = true; - ci->needclipboard = totalmillis ? totalmillis : 1; - if(mastermode>=MM_LOCKED) ci->state.state = CS_SPECTATOR; - ci->state.lasttimeplayed = lastmillis; - - const char *worst = m_teammode ? chooseworstteam(NULL, ci) : NULL; - copystring(ci->team, worst ? worst : "good", MAXTEAMLEN+1); - - sendwelcome(ci); - if(restorescore(ci)) sendresume(ci); - sendinitclient(ci); - - aiman::addclient(ci); - - if(m_demo) setupdemoplayback(); - - if(servermotd[0]) sendf(ci->clientnum, 1, "ris", N_SERVMSG, servermotd); - } - - void parsepacket(int sender, int chan, packetbuf &p) // has to parse exactly each byte of the packet - { - if(sender<0 || p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED || chan > 2) return; - char text[MAXTRANS]; - int type; - clientinfo *ci = sender>=0 ? getinfo(sender) : NULL, *cq = ci, *cm = ci; - if(ci && !ci->connected) - { - if(chan==0) return; - //~else if(chan!=1) { disconnect_client(sender, DISC_MSGERR); return; } - else while(p.length() < p.maxlen) switch(checktype(getint(p), ci)) - { - case N_CONNECT: - { - getstring(text, p); - filtertext(text, text, false, false, MAXNAMELEN); - if(!text[0]) copystring(text, "Anonymous"); - copystring(ci->name, text, MAXNAMELEN+1); - ci->playermodel = 0; - - string password, authdesc, authname; - getstring(password, p, sizeof(password)); - getstring(authdesc, p, sizeof(authdesc)); - getstring(authname, p, sizeof(authname)); - int disc = allowconnect(ci, password); - if(disc) - { - if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !tryauth(ci, authname, authdesc)) - { - //~disconnect_client(sender, disc); - return; - } - ci->connectauth = disc; - } - else connected(ci); - break; - } - - case N_AUTHANS: - { - string desc, ans; - getstring(desc, p, sizeof(desc)); - uint id = (uint)getint(p); - getstring(ans, p, sizeof(ans)); - if(!answerchallenge(ci, id, ans, desc)) - { - //~disconnect_client(sender, ci->connectauth); - return; - } - break; - } - - case N_PING: - getint(p); - break; - - default: - //~disconnect_client(sender, DISC_MSGERR); - return; - } - return; - } - else if(chan==2) - { - receivefile(sender, p.buf, p.maxlen); - return; - } - - if(p.packet->flags&ENET_PACKET_FLAG_RELIABLE) reliablemessages = true; - #define QUEUE_AI clientinfo *cm = cq; - #define QUEUE_MSG { if(cm && (!cm->local || demorecord || hasnonlocalclients())) while(curmsgmessages.add(p.buf[curmsg++]); } - #define QUEUE_BUF(body) { \ - if(cm && (!cm->local || demorecord || hasnonlocalclients())) \ - { \ - curmsg = p.length(); \ - { body; } \ - } \ - } - #define QUEUE_INT(n) QUEUE_BUF(putint(cm->messages, n)) - #define QUEUE_UINT(n) QUEUE_BUF(putuint(cm->messages, n)) - #define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cm->messages)) - int curmsg; - while((curmsg = p.length()) < p.maxlen) switch(type = checktype(getint(p), ci)) - { - case N_POS: - { - int pcn = getuint(p); - p.get(); - uint flags = getuint(p); - clientinfo *cp = getinfo(pcn); - if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; - vec pos; - loopk(3) - { - int n = p.get(); n |= p.get()<<8; if(flags&(1<local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) - { - if(!ci->local && !m_edit && max(vel.magnitude2(), (float)fabs(vel.z)) >= 180) - cp->setexceeded(); - cp->position.setsize(0); - while(curmsgposition.add(p.buf[curmsg++]); - } - cp->state.o = pos; - cp->gameclip = (flags&0x80)!=0; - } - break; - } - - case N_TELEPORT: - { - int pcn = getint(p), teleport = getint(p), teledest = getint(p); - clientinfo *cp = getinfo(pcn); - if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; - if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) - { - flushclientposition(*cp); - sendf(-1, 0, "ri4x", N_TELEPORT, pcn, teleport, teledest, cp->ownernum); - } - break; - } - - case N_JUMPPAD: - { - int pcn = getint(p), jumppad = getint(p); - clientinfo *cp = getinfo(pcn); - if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; - if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) - { - cp->setpushed(); - flushclientposition(*cp); - sendf(-1, 0, "ri3x", N_JUMPPAD, pcn, jumppad, cp->ownernum); - } - break; - } - - case N_FROMAI: - { - int qcn = getint(p); - if(qcn < 0) cq = ci; - else - { - cq = getinfo(qcn); - if(cq && qcn != sender && cq->ownernum != sender) cq = NULL; - } - break; - } - - case N_EDITMODE: - { - int val = getint(p); - if(!ci->local && !m_edit) break; - if(val ? ci->state.state!=CS_ALIVE && ci->state.state!=CS_DEAD : ci->state.state!=CS_EDITING) break; - if(val) - { - ci->state.editstate = ci->state.state; - ci->state.state = CS_EDITING; - ci->events.setsize(0); - ci->state.rockets.reset(); - ci->state.grenades.reset(); - } - else ci->state.state = ci->state.editstate; - QUEUE_MSG; - break; - } - - case N_MAPCRC: - { - getstring(text, p); - int crc = getint(p); - if(!ci) break; - if(strcmp(text, smapname)) - { - if(ci->clientmap[0]) - { - ci->clientmap[0] = '\0'; - ci->mapcrc = 0; - } - else if(ci->mapcrc > 0) ci->mapcrc = 0; - break; - } - copystring(ci->clientmap, text); - ci->mapcrc = text[0] ? crc : 1; - checkmaps(); - if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; - break; - } - - case N_CHECKMAPS: - checkmaps(sender); - break; - - case N_TRYSPAWN: - if(!ci || !cq || cq->state.state!=CS_DEAD || cq->state.lastspawn>=0) break; - if(!ci->clientmap[0] && !ci->mapcrc) - { - ci->mapcrc = -1; - checkmaps(); - if(ci == cq) { if(ci->state.state != CS_DEAD) break; } - else if(cq->ownernum != ci->clientnum) { cq = NULL; break; } - } - if(cq->state.deadflush) - { - flushevents(cq, cq->state.deadflush); - cq->state.respawn(); - } - cleartimedevents(cq); - sendspawn(cq); - break; - - case N_GUNSELECT: - { - int gunselect = getint(p); - if(!cq || cq->state.state!=CS_ALIVE) break; - cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; - QUEUE_AI; - QUEUE_MSG; - break; - } - - case N_SPAWN: - { - int ls = getint(p), gunselect = getint(p); - if(!cq || (cq->state.state!=CS_ALIVE && cq->state.state!=CS_DEAD && cq->state.state!=CS_EDITING) || ls!=cq->state.lifesequence || cq->state.lastspawn<0) break; - cq->state.lastspawn = -1; - cq->state.state = CS_ALIVE; - cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; - cq->exceeded = 0; - QUEUE_AI; - QUEUE_BUF({ - putint(cm->messages, N_SPAWN); - sendstate(cq->state, cm->messages); - }); - break; - } - - case N_SUICIDE: - { - if(cq) cq->addevent(new suicideevent); - break; - } - - case N_SHOOT: - { - shotevent *shot = new shotevent; - shot->id = getint(p); - shot->millis = cq ? cq->geteventmillis(gamemillis, shot->id) : 0; - shot->gun = getint(p); - loopk(3) shot->from[k] = getint(p)/DMF; - loopk(3) shot->to[k] = getint(p)/DMF; - int hits = getint(p); - loopk(hits) - { - if(p.overread()) break; - hitinfo &hit = shot->hits.add(); - hit.target = getint(p); - hit.lifesequence = getint(p); - hit.dist = getint(p)/DMF; - hit.rays = getint(p); - loopk(3) hit.dir[k] = getint(p)/DNF; - } - if(cq) - { - cq->addevent(shot); - cq->setpushed(); - } - else delete shot; - break; - } - - case N_EXPLODE: - { - explodeevent *exp = new explodeevent; - int cmillis = getint(p); - exp->millis = cq ? cq->geteventmillis(gamemillis, cmillis) : 0; - exp->gun = getint(p); - exp->id = getint(p); - int hits = getint(p); - loopk(hits) - { - if(p.overread()) break; - hitinfo &hit = exp->hits.add(); - hit.target = getint(p); - hit.lifesequence = getint(p); - hit.dist = getint(p)/DMF; - hit.rays = getint(p); - loopk(3) hit.dir[k] = getint(p)/DNF; - } - if(cq) cq->addevent(exp); - else delete exp; - break; - } - - case N_ITEMPICKUP: - { - int n = getint(p); - if(!cq) break; - pickupevent *pickup = new pickupevent; - pickup->ent = n; - cq->addevent(pickup); - break; - } - - case N_TEXT: - { - QUEUE_AI; - QUEUE_MSG; - getstring(text, p); - filtertext(text, text, true, true); - QUEUE_STR(text); - if(isdedicatedserver() && cq) logoutf("%s: %s", colorname(cq), text); - break; - } - - case N_SAYTEAM: - { - getstring(text, p); - if(!ci || !cq || (ci->state.state==CS_SPECTATOR && !ci->local && !ci->privilege) || !m_teammode || !cq->team[0]) break; - filtertext(text, text, true, true); - loopv(clients) - { - clientinfo *t = clients[i]; - if(t==cq || t->state.state==CS_SPECTATOR || t->state.aitype != AI_NONE || strcmp(cq->team, t->team)) continue; - sendf(t->clientnum, 1, "riis", N_SAYTEAM, cq->clientnum, text); - } - if(isdedicatedserver() && cq) logoutf("%s <%s>: %s", colorname(cq), cq->team, text); - break; - } - - case N_SWITCHNAME: - { - QUEUE_MSG; - getstring(text, p); - filtertext(ci->name, text, false, false, MAXNAMELEN); - if(!ci->name[0]) copystring(ci->name, "Anonymous"); - QUEUE_STR(ci->name); - break; - } - - case N_SWITCHMODEL: - { - ci->playermodel = 0; - QUEUE_MSG; - break; - } - - case N_SWITCHTEAM: - { - getstring(text, p); - filtertext(text, text, false, false, MAXTEAMLEN); - if(m_teammode && text[0] && strcmp(ci->team, text) && addteaminfo(text)) - { - if(ci->state.state==CS_ALIVE) suicide(ci); - copystring(ci->team, text); - aiman::changeteam(ci); - sendf(-1, 1, "riisi", N_SETTEAM, sender, ci->team, ci->state.state==CS_SPECTATOR ? -1 : 0); - } - break; - } - - case N_MAPVOTE: - { - getstring(text, p); - filtertext(text, text, false); - fixmapname(text); - int reqmode = getint(p); - vote(text, reqmode, sender); - break; - } - - case N_ITEMLIST: - { - if((ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || !notgotitems || strcmp(ci->clientmap, smapname)) { while(getint(p)>=0 && !p.overread()) getint(p); break; } - int n; - while((n = getint(p))>=0 && nstate.state==CS_SPECTATOR) break; - QUEUE_MSG; - bool canspawn = canspawnitem(type); - if(istate.state!=CS_SPECTATOR) QUEUE_MSG; - break; - } - - case N_PING: - sendf(sender, 1, "i2", N_PONG, getint(p)); - break; - - case N_CLIENTPING: - { - int ping = getint(p); - if(ci) - { - ci->ping = ping; - loopv(ci->bots) ci->bots[i]->ping = ping; - } - QUEUE_MSG; - break; - } - - case N_MASTERMODE: - { - int mm = getint(p); - if((ci->privilege || ci->local) && mm>=MM_OPEN && mm<=MM_PRIVATE) - { - if((ci->privilege>=PRIV_ADMIN || ci->local) || (mastermask&(1<=MM_PRIVATE) - { - loopv(clients) allowedips.add(getclientip(clients[i]->clientnum)); - } - sendf(-1, 1, "rii", N_MASTERMODE, mastermode); - //sendservmsgf("mastermode is now %s (%d)", mastermodename(mastermode), mastermode); - } - else - { - defformatstring(s, "mastermode %d is disabled on this server", mm); - sendf(sender, 1, "ris", N_SERVMSG, s); - } - } - break; - } - - case N_CLEARBANS: - { - if(ci->privilege || ci->local) - { - bannedips.shrink(0); - sendservmsg("cleared all bans"); - } - break; - } - - case N_KICK: - { - int victim = getint(p); - getstring(text, p); - filtertext(text, text); - trykick(ci, victim, text); - break; - } - - case N_SPECTATOR: - { - int spectator = getint(p), val = getint(p); - if(!ci->privilege && !ci->local && (spectator!=sender || (ci->state.state==CS_SPECTATOR && mastermode>=MM_LOCKED))) break; - clientinfo *spinfo = (clientinfo *)getclientinfo(spectator); // no bots - if(!spinfo || !spinfo->connected || (spinfo->state.state==CS_SPECTATOR ? val : !val)) break; - - if(spinfo->state.state!=CS_SPECTATOR && val) forcespectator(spinfo); - else if(spinfo->state.state==CS_SPECTATOR && !val) unspectate(spinfo); - - if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; - break; - } - - case N_SETTEAM: - { - int who = getint(p); - getstring(text, p); - filtertext(text, text, false, false, MAXTEAMLEN); - if(!ci->privilege && !ci->local) break; - clientinfo *wi = getinfo(who); - if(!m_teammode || !text[0] || !wi || !wi->connected || !strcmp(wi->team, text)) break; - if(addteaminfo(text)) - { - if(wi->state.state==CS_ALIVE) suicide(wi); - copystring(wi->team, text, MAXTEAMLEN+1); - } - aiman::changeteam(wi); - sendf(-1, 1, "riisi", N_SETTEAM, who, wi->team, 1); - break; - } - - case N_FORCEINTERMISSION: - if(ci->local && !hasnonlocalclients()) startintermission(); - break; - - case N_RECORDDEMO: - { - int val = getint(p); - if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - if(!maxdemos || !maxdemosize) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "the server has disabled demo recording"); - break; - } - demonextmatch = val!=0; - sendservmsgf("demo recording is %s for next match", demonextmatch ? "enabled" : "disabled"); - break; - } - - case N_STOPDEMO: - { - if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - stopdemo(); - break; - } - - case N_CLEARDEMOS: - { - int demo = getint(p); - if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - cleardemos(demo); - break; - } - - case N_LISTDEMOS: - if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; - listdemos(sender); - break; - - case N_GETDEMO: - { - int n = getint(p), tag = getint(p); - if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; - senddemo(ci, n, tag); - break; - } - - case N_GETMAP: - if(!mapdata) sendf(sender, 1, "ris", N_SERVMSG, "no map to send"); - else if(ci->getmap) sendf(sender, 1, "ris", N_SERVMSG, "already sending map"); - else - { - sendservmsgf("[%s is getting the map]", colorname(ci)); - if((ci->getmap = sendfile(sender, 2, mapdata, "ri", N_SENDMAP))) - ci->getmap->freeCallback = freegetmap; - ci->needclipboard = totalmillis ? totalmillis : 1; - } - break; - - case N_NEWMAP: - { - int size = getint(p); - if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; - if(size>=0) - { - smapname[0] = '\0'; - resetitems(); - notgotitems = false; - } - QUEUE_MSG; - break; - } - - case N_SETMASTER: - { - int mn = getint(p), val = getint(p); - getstring(text, p); - if(mn != ci->clientnum) - { - if(!ci->privilege && !ci->local) break; - clientinfo *minfo = (clientinfo *)getclientinfo(mn); - if(!minfo || !minfo->connected || (!ci->local && minfo->privilege >= ci->privilege) || (val && minfo->privilege)) break; - setmaster(minfo, val!=0, "", NULL, NULL, PRIV_MASTER, true); - } - else setmaster(ci, val!=0, text); - // don't broadcast the master password - break; - } - - case N_ADDBOT: - { - aiman::reqadd(ci, getint(p)); - break; - } - - case N_DELBOT: - { - aiman::reqdel(ci); - break; - } - - case N_BOTLIMIT: - { - int limit = getint(p); - if(ci) aiman::setbotlimit(ci, limit); - break; - } - - case N_BOTBALANCE: - { - int balance = getint(p); - if(ci) aiman::setbotbalance(ci, balance!=0); - break; - } - - case N_AUTHTRY: - { - string desc, name; - getstring(desc, p, sizeof(desc)); - getstring(name, p, sizeof(name)); - tryauth(ci, name, desc); - break; - } - - case N_AUTHKICK: - { - string desc, name; - getstring(desc, p, sizeof(desc)); - getstring(name, p, sizeof(name)); - int victim = getint(p); - getstring(text, p); - filtertext(text, text); - int authpriv = PRIV_AUTH; - if(desc[0]) - { - userinfo *u = users.access(userkey(name, desc)); - if(u) authpriv = u->privilege; else break; - } - if(ci->local || ci->privilege >= authpriv) trykick(ci, victim, text); - else if(trykick(ci, victim, text, name, desc, authpriv, true) && tryauth(ci, name, desc)) - { - ci->authkickvictim = victim; - ci->authkickreason = newstring(text); - } - break; - } - - case N_AUTHANS: - { - string desc, ans; - getstring(desc, p, sizeof(desc)); - uint id = (uint)getint(p); - getstring(ans, p, sizeof(ans)); - answerchallenge(ci, id, ans, desc); - break; - } - - case N_PAUSEGAME: - { - int val = getint(p); - if(ci->privilege < (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - pausegame(val > 0, ci); - break; - } - - case N_GAMESPEED: - { - int val = getint(p); - if(ci->privilege < (restrictgamespeed ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - changegamespeed(val, ci); - break; - } - - case N_COPY: - ci->cleanclipboard(); - ci->lastclipboard = totalmillis ? totalmillis : 1; - goto genericmsg; - - case N_PASTE: - if(ci->state.state!=CS_SPECTATOR) sendclipboard(ci); - goto genericmsg; - - case N_CLIPBOARD: - { - int unpacklen = getint(p), packlen = getint(p); - ci->cleanclipboard(false); - if(ci->state.state==CS_SPECTATOR) - { - if(packlen > 0) p.subbuf(packlen); - break; - } - if(packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) - { - if(packlen > 0) p.subbuf(packlen); - packlen = unpacklen = 0; - } - packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); - putint(q, N_CLIPBOARD); - putint(q, ci->clientnum); - putint(q, unpacklen); - putint(q, packlen); - if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); - ci->clipboard = q.finalize(); - ci->clipboard->referenceCount++; - break; - } - - case N_EDITT: - case N_REPLACE: - case N_EDITVSLOT: - { - int size = server::msgsizelookup(type); - //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } - loopi(size-1) getint(p); - //~if(p.remaining() < 2) { disconnect_client(sender, DISC_MSGERR); return; } - int extra = lilswap(*(const ushort *)p.pad(2)); - //~if(p.remaining() < extra) { disconnect_client(sender, DISC_MSGERR); return; } - p.pad(extra); - if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG; - break; - } - - case N_UNDO: - case N_REDO: - { - int unpacklen = getint(p), packlen = getint(p); - if(!ci || ci->state.state==CS_SPECTATOR || packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) - { - if(packlen > 0) p.subbuf(packlen); - break; - } - //~if(p.remaining() < packlen) { disconnect_client(sender, DISC_MSGERR); return; } - packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); - putint(q, type); - putint(q, ci->clientnum); - putint(q, unpacklen); - putint(q, packlen); - if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); - sendpacket(-1, 1, q.finalize(), ci->clientnum); - break; - } - - case N_SERVCMD: - getstring(text, p); - break; - - - case -1: - //~disconnect_client(sender, DISC_MSGERR); - return; - - case -2: - //~disconnect_client(sender, DISC_OVERFLOW); - return; - - default: genericmsg: - { - int size = server::msgsizelookup(type); - //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } - loopi(size-1) getint(p); - if(ci) switch(msgfilter[type]) - { - case 2: case 3: if(ci->state.state != CS_SPECTATOR) QUEUE_MSG; break; - default: if(cq && (ci != cq || ci->state.state!=CS_SPECTATOR)) { QUEUE_AI; QUEUE_MSG; } break; - } - break; - } - } - } - - int laninfoport() { return SAUERBRATEN_LANINFO_PORT; } - int serverinfoport(int servport) { return servport < 0 ? SAUERBRATEN_SERVINFO_PORT : servport+1; } - int serverport(int infoport) { return infoport < 0 ? SAUERBRATEN_SERVER_PORT : infoport-1; } - const char *defaultmaster() { return "master.sauerbraten.org"; } - int masterport() { return SAUERBRATEN_MASTER_PORT; } - int numchannels() { return 3; } - - #include "extinfo.h" - - void serverinforeply(ucharbuf &req, ucharbuf &p) - { - if(req.remaining() && !getint(req)) - { - extserverinforeply(req, p); - return; - } - - putint(p, numclients(-1, false, true)); - putint(p, gamepaused || gamespeed != 100 ? 7 : 5); // number of attrs following - putint(p, PROTOCOL_VERSION); // generic attributes, passed back below - putint(p, gamemode); - putint(p, m_timed ? max((gamelimit - gamemillis)/1000, 0) : 0); - putint(p, maxclients); - putint(p, serverpass[0] ? MM_PASSWORD : (!m_mp(gamemode) ? MM_PRIVATE : (mastermode || mastermask&MM_AUTOAPPROVE ? mastermode : MM_AUTH))); - if(gamepaused || gamespeed != 100) - { - putint(p, gamepaused ? 1 : 0); - putint(p, gamespeed); - } - sendstring(smapname, p); - sendstring(serverdesc, p); - sendserverinforeply(p); - } - - #include "aiman.h" + SVAR(servermotd, ""); + + struct teamkillkick + { + int modes, limit, ban; + + bool match(int mode) const + { + return (modes&(1<<(mode-STARTGAMEMODE)))!=0; + } + + bool includes(const teamkillkick &tk) const + { + return tk.modes != modes && (tk.modes & modes) == tk.modes; + } + }; + vector teamkillkicks; + + void teamkillkickreset() + { + teamkillkicks.setsize(0); + } + + void addteamkillkick(char *modestr, int *limit, int *ban) + { + vector modes; + explodelist(modestr, modes); + teamkillkick &kick = teamkillkicks.add(); + kick.modes = genmodemask(modes); + kick.limit = *limit; + kick.ban = *ban > 0 ? *ban*60000 : (*ban < 0 ? 0 : 30*60000); + modes.deletearrays(); + } + + COMMAND(teamkillkickreset, ""); + COMMANDN(teamkillkick, addteamkillkick, "sii"); + + struct teamkillinfo + { + uint ip; + int teamkills; + }; + vector teamkills; + bool shouldcheckteamkills = false; + + void addteamkill(clientinfo *actor, clientinfo *victim, int n) + { + if(!m_timed || actor->state.aitype != AI_NONE || actor->local || actor->privilege || (victim && victim->state.aitype != AI_NONE)) return; + shouldcheckteamkills = true; + uint ip = getclientip(actor->clientnum); + loopv(teamkills) if(teamkills[i].ip == ip) + { + teamkills[i].teamkills += n; + return; + } + teamkillinfo &tk = teamkills.add(); + tk.ip = ip; + tk.teamkills = n; + } + + void checkteamkills() + { + teamkillkick *kick = NULL; + if(m_timed) loopv(teamkillkicks) if(teamkillkicks[i].match(gamemode) && (!kick || kick->includes(teamkillkicks[i]))) + kick = &teamkillkicks[i]; + if(kick) loopvrev(teamkills) + { + teamkillinfo &tk = teamkills[i]; + if(tk.teamkills >= kick->limit) + { + if(kick->ban > 0) addban(tk.ip, kick->ban); + kickclients(tk.ip); + teamkills.removeunordered(i); + } + } + shouldcheckteamkills = false; + } + + void *newclientinfo() { return new clientinfo; } + void deleteclientinfo(void *ci) { delete (clientinfo *)ci; } + + clientinfo *getinfo(int n) + { + if(n < MAXCLIENTS) return (clientinfo *)getclientinfo(n); + n -= MAXCLIENTS; + return bots.inrange(n) ? bots[n] : NULL; + } + + uint mcrc = 0; + vector ments; + vector sents; + vector scores; + + int msgsizelookup(int msg) + { + static int sizetable[NUMMSG] = { -1 }; + if(sizetable[0] < 0) + { + memset(sizetable, -1, sizeof(sizetable)); + for(const int *p = msgsizes; *p >= 0; p += 2) sizetable[p[0]] = p[1]; + } + return msg >= 0 && msg < NUMMSG ? sizetable[msg] : -1; + } + + const char *modename(int n, const char *unknown) + { + if(m_valid(n)) return gamemodes[n - STARTGAMEMODE].name; + return unknown; + } + + const char *mastermodename(int n, const char *unknown) + { + return (n>=MM_START && size_t(n-MM_START)clientnum!=exclude && (!nospec || ci->state.state!=CS_SPECTATOR || (priv && (ci->privilege || ci->local))) && (!noai || ci->state.aitype == AI_NONE)) n++; + } + return n; + } + + bool duplicatename(clientinfo *ci, const char *name) + { + if(!name) name = ci->name; + loopv(clients) if(clients[i]!=ci && !strcmp(name, clients[i]->name)) return true; + return false; + } + + const char *colorname(clientinfo *ci, const char *name = NULL) + { + if(!name) name = ci->name; + if(name[0] && !duplicatename(ci, name) && ci->state.aitype == AI_NONE) return name; + static string cname[3]; + static int cidx = 0; + cidx = (cidx+1)%3; + formatstring(cname[cidx], ci->state.aitype == AI_NONE ? "%s \fs\f5(%d)\fr" : "%s \fs\f5[%d]\fr", name, ci->clientnum); + return cname[cidx]; + } + + bool canspawnitem(int type) { return !m_noitems && (type>=I_SHELLS && type<=I_QUAD && (!m_noammo || typeI_CARTRIDGES)); } + + int spawntime(int type) + { + int np = numclients(-1, true, false); + np = np<3 ? 4 : (np>4 ? 2 : 3); // spawn times are dependent on number of players + int sec = 0; + switch(type) + { + case I_SHELLS: + case I_BULLETS: + case I_ROCKETS: + case I_ROUNDS: + case I_GRENADES: + case I_CARTRIDGES: sec = np*4; break; + case I_HEALTH: sec = np*5; break; + case I_TINYHEALTH: sec = np*5; break; + case I_TINYARMOUR: sec = np*5; break; + case I_GREENARMOUR: sec = 20; break; + case I_YELLOWARMOUR: sec = 30; break; + case I_BOOST: sec = 60; break; + case I_QUAD: sec = 70; break; + } + return sec*1000; + } + + bool delayspawn(int type) + { + switch(type) + { + case I_GREENARMOUR: + case I_YELLOWARMOUR: + case I_BOOST: + case I_QUAD: + return true; + default: + return false; + } + } + + bool pickup(int i, int sender) // server side item pickup, acknowledge first client that gets it + { + if((m_timed && gamemillis>=gamelimit) || !sents.inrange(i) || !sents[i].spawned) return false; + clientinfo *ci = getinfo(sender); + if(!ci) return false; + if(!ci->local && !ci->state.canpickup(sents[i].type)) + { + sendf(sender, 1, "ri3", N_ITEMACC, i, -1); + return false; + } + sents[i].spawned = false; + sents[i].spawntime = spawntime(sents[i].type); + sendf(-1, 1, "ri3", N_ITEMACC, i, sender); + ci->state.pickup(sents[i].type); + return true; + } + + static hashset teaminfos; + + void clearteaminfo() + { + teaminfos.clear(); + } + + bool teamhasplayers(const char *team) { loopv(clients) if(!strcmp(clients[i]->team, team)) return true; return false; } + + bool pruneteaminfo() + { + int oldteams = teaminfos.numelems; + enumerate(teaminfos, teaminfo, old, + if(!old.frags && !teamhasplayers(old.team)) teaminfos.remove(old.team); + ); + return teaminfos.numelems < oldteams; + } + + teaminfo *addteaminfo(const char *team) + { + teaminfo *t = teaminfos.access(team); + if(!t) + { + if(teaminfos.numelems >= MAXTEAMS && !pruneteaminfo()) return NULL; + t = &teaminfos[team]; + copystring(t->team, team, sizeof(t->team)); + t->frags = 0; + } + return t; + } + + clientinfo *choosebestclient(float &bestrank) + { + clientinfo *best = NULL; + bestrank = -1; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.timeplayed<0) continue; + float rank = ci->state.state!=CS_SPECTATOR ? ci->state.effectiveness/max(ci->state.timeplayed, 1) : -1; + if(!best || rank > bestrank) { best = ci; bestrank = rank; } + } + return best; + } + + VAR(persistteams, 0, 0, 1); + + void autoteam() + { + static const char * const teamnames[2] = {"good", "evil"}; + vector team[2]; + float teamrank[2] = {0, 0}; + for(int round = 0, remaining = clients.length(); remaining>=0; round++) + { + int first = round&1, second = (round+1)&1, selected = 0; + while(teamrank[first] <= teamrank[second]) + { + float rank; + clientinfo *ci = choosebestclient(rank); + if(!ci) break; + if(selected && rank<=0) break; + ci->state.timeplayed = -1; + team[first].add(ci); + if(rank>0) teamrank[first] += rank; + selected++; + if(rank<=0) break; + } + if(!selected) break; + remaining -= selected; + } + loopi(sizeof(team)/sizeof(team[0])) + { + addteaminfo(teamnames[i]); + loopvj(team[i]) + { + clientinfo *ci = team[i][j]; + if(!strcmp(ci->team, teamnames[i])) continue; + if(persistteams && ci->team[0]) + { + addteaminfo(ci->team); + continue; + } + copystring(ci->team, teamnames[i], MAXTEAMLEN+1); + sendf(-1, 1, "riisi", N_SETTEAM, ci->clientnum, teamnames[i], -1); + } + } + } + + struct teamrank + { + const char *name; + float rank; + int clients; + + teamrank(const char *name) : name(name), rank(0), clients(0) {} + }; + + const char *chooseworstteam(const char *suggest = NULL, clientinfo *exclude = NULL) + { + teamrank teamranks[2] = { teamrank("good"), teamrank("evil") }; + const int numteams = sizeof(teamranks)/sizeof(teamranks[0]); + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci==exclude || ci->state.aitype!=AI_NONE || ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + ci->state.lasttimeplayed = lastmillis; + + loopj(numteams) if(!strcmp(ci->team, teamranks[j].name)) + { + teamrank &ts = teamranks[j]; + ts.rank += ci->state.effectiveness/max(ci->state.timeplayed, 1); + ts.clients++; + break; + } + } + teamrank *worst = &teamranks[numteams-1]; + loopi(numteams-1) + { + teamrank &ts = teamranks[i]; + if(ts.rank < worst->rank || (ts.rank == worst->rank && ts.clients < worst->clients)) worst = &ts; + } + return worst->name; + } + + void prunedemos(int extra = 0) + { + int n = clamp(demos.length() + extra - maxdemos, 0, demos.length()); + if(n <= 0) return; + loopi(n) delete[] demos[i].data; + demos.remove(0, n); + } + + void adddemo() + { + if(!demotmp) return; + int len = (int)min(demotmp->size(), stream::offset((maxdemosize<<20) + 0x10000)); + demofile &d = demos.add(); + time_t t = time(NULL); + char *timestr = ctime(&t), *trim = timestr + strlen(timestr); + while(trim>timestr && iscubespace(*--trim)) *trim = '\0'; + formatstring(d.info, "%s: %s, %s, %.2f%s", timestr, modename(gamemode), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB"); + sendservmsgf("demo \"%s\" recorded", d.info); + d.data = new uchar[len]; + d.len = len; + demotmp->seek(0, SEEK_SET); + demotmp->read(d.data, len); + DELETEP(demotmp); + } + + void enddemorecord() + { + if(!demorecord) return; + + DELETEP(demorecord); + + if(!demotmp) return; + if(!maxdemos || !maxdemosize) { DELETEP(demotmp); return; } + + prunedemos(1); + adddemo(); + } + + void writedemo(int chan, void *data, int len) + { + if(!demorecord) return; + int stamp[3] = { gamemillis, chan, len }; + lilswap(stamp, 3); + demorecord->write(stamp, sizeof(stamp)); + demorecord->write(data, len); + if(demorecord->rawtell() >= (maxdemosize<<20)) enddemorecord(); + } + + void recordpacket(int chan, void *data, int len) + { + writedemo(chan, data, len); + } + + int welcomepacket(packetbuf &p, clientinfo *ci); + void sendwelcome(clientinfo *ci); + + void setupdemorecord() + { + if(!m_mp(gamemode) || m_edit) return; + + demotmp = opentempfile("demorecord", "w+b"); + if(!demotmp) return; + + stream *f = opengzfile(NULL, "wb", demotmp); + if(!f) { DELETEP(demotmp); return; } + + sendservmsg("recording demo"); + + demorecord = f; + + demoheader hdr; + memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic)); + hdr.version = DEMO_VERSION; + hdr.protocol = PROTOCOL_VERSION; + lilswap(&hdr.version, 2); + demorecord->write(&hdr, sizeof(demoheader)); + + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + welcomepacket(p, NULL); + writedemo(1, p.buf, p.len); + } + + void listdemos(int cn) + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_SENDDEMOLIST); + putint(p, demos.length()); + loopv(demos) sendstring(demos[i].info, p); + sendpacket(cn, 1, p.finalize()); + } + + void cleardemos(int n) + { + if(!n) + { + loopv(demos) delete[] demos[i].data; + demos.shrink(0); + sendservmsg("cleared all demos"); + } + else if(demos.inrange(n-1)) + { + delete[] demos[n-1].data; + demos.remove(n-1); + sendservmsgf("cleared demo %d", n); + } + } + + static void freegetmap(ENetPacket *packet) + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->getmap == packet) ci->getmap = NULL; + } + } + + static void freegetdemo(ENetPacket *packet) + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->getdemo == packet) ci->getdemo = NULL; + } + } + + void senddemo(clientinfo *ci, int num, int tag) + { + if(ci->getdemo) return; + if(!num) num = demos.length(); + if(!demos.inrange(num-1)) return; + demofile &d = demos[num-1]; + if((ci->getdemo = sendf(ci->clientnum, 2, "riim", N_SENDDEMO, tag, d.len, d.data))) + ci->getdemo->freeCallback = freegetdemo; + } + + void enddemoplayback() + { + if(!demoplayback) return; + DELETEP(demoplayback); + + loopv(clients) sendf(clients[i]->clientnum, 1, "ri3", N_DEMOPLAYBACK, 0, clients[i]->clientnum); + + sendservmsg("demo playback finished"); + + loopv(clients) sendwelcome(clients[i]); + } + + SVARP(demodir, "demo"); + + const char *getdemofile(const char *file, bool init) + { + if(!demodir[0]) return NULL; + static string buf; + copystring(buf, demodir); + int dirlen = strlen(buf); + if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; } + if(init) + { + const char *dir = findfile(buf, "w"); + if(!fileexists(dir, "w")) createdir(dir); + } + concatstring(buf, file); + return buf; + } + + void setupdemoplayback() + { + if(demoplayback) return; + demoheader hdr; + string msg; + msg[0] = '\0'; + string file; + copystring(file, smapname); + int len = strlen(file); + if(len < 4 || strcasecmp(&file[len-4], ".dmo")) concatstring(file, ".dmo"); + if(const char *buf = getdemofile(file, false)) demoplayback = opengzfile(buf, "rb"); + if(!demoplayback) demoplayback = opengzfile(file, "rb"); + if(!demoplayback) formatstring(msg, "could not read demo \"%s\"", file); + else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic))) + formatstring(msg, "\"%s\" is not a demo file", file); + else + { + lilswap(&hdr.version, 2); + if(hdr.version!=DEMO_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.versionread(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) + { + enddemoplayback(); + return; + } + lilswap(&nextplayback, 1); + } + + void readdemo() + { + if(!demoplayback) return; + while(gamemillis>=nextplayback) + { + int chan, len; + if(demoplayback->read(&chan, sizeof(chan))!=sizeof(chan) || + demoplayback->read(&len, sizeof(len))!=sizeof(len)) + { + enddemoplayback(); + return; + } + lilswap(&chan, 1); + lilswap(&len, 1); + ENetPacket *packet = enet_packet_create(NULL, len+1, 0); + if(!packet || demoplayback->read(packet->data+1, len)!=size_t(len)) + { + if(packet) enet_packet_destroy(packet); + enddemoplayback(); + return; + } + packet->data[0] = N_DEMOPACKET; + sendpacket(-1, chan, packet); + if(!packet->referenceCount) enet_packet_destroy(packet); + if(!demoplayback) break; + if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) + { + enddemoplayback(); + return; + } + lilswap(&nextplayback, 1); + } + } + + void timeupdate(int secs) + { + if(!demoplayback) return; + if(secs <= 0) interm = -1; + else gamelimit = max(gamelimit, nextplayback + secs*1000); + } + + void seekdemo(char *t) + { + if(!demoplayback) return; + bool rev = *t == '-'; + if(rev) t++; + int mins = strtoul(t, &t, 10), secs = 0, millis = 0; + if(*t == ':') secs = strtoul(t+1, &t, 10); + else { secs = mins; mins = 0; } + if(*t == '.') millis = strtoul(t+1, &t, 10); + int offset = max(millis + (mins*60 + secs)*1000, 0), prevmillis = gamemillis; + if(rev) while(gamelimit - offset > gamemillis) + { + gamemillis = gamelimit - offset; + readdemo(); + } + else if(offset > gamemillis) + { + gamemillis = offset; + readdemo(); + } + if(gamemillis > prevmillis) + { + if(!interm) sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); + } + } + + ICOMMAND(seekdemo, "sN$", (char *t, int *numargs, ident *id), + { + if(*numargs > 0) seekdemo(t); + else + { + int secs = gamemillis/1000; + defformatstring(str, "%d:%02d.%03d", secs/60, secs%60, gamemillis%1000); + if(*numargs < 0) result(str); + else printsvar(id, str); + } + }); + + void stopdemo() + { + if(m_demo) enddemoplayback(); + else enddemorecord(); + } + + void pausegame(bool val, clientinfo *ci = NULL) + { + if(gamepaused==val) return; + gamepaused = val; + sendf(-1, 1, "riii", N_PAUSEGAME, gamepaused ? 1 : 0, ci ? ci->clientnum : -1); + } + + void checkpausegame() + { + if(!gamepaused) return; + int admins = 0; + loopv(clients) if(clients[i]->privilege >= (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) || clients[i]->local) admins++; + if(!admins) pausegame(false); + } + + void forcepaused(bool paused) + { + pausegame(paused); + } + + bool ispaused() { return gamepaused; } + + void changegamespeed(int val, clientinfo *ci = NULL) + { + val = clamp(val, 10, 1000); + if(gamespeed==val) return; + gamespeed = val; + sendf(-1, 1, "riii", N_GAMESPEED, gamespeed, ci ? ci->clientnum : -1); + } + + void forcegamespeed(int speed) + { + changegamespeed(speed); + } + + int scaletime(int t) { return t*gamespeed; } + + SVAR(serverauth, ""); + + struct userkey + { + char *name; + char *desc; + + userkey() : name(NULL), desc(NULL) {} + userkey(char *name, char *desc) : name(name), desc(desc) {} + }; + + static inline uint hthash(const userkey &k) { return ::hthash(k.name); } + static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } + + struct userinfo : userkey + { + void *pubkey; + int privilege; + + userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} + ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } + }; + hashset users; + + void adduser(char *name, char *desc, char *pubkey, char *priv) + { + userkey key(name, desc); + userinfo &u = users[key]; + if(u.pubkey) { freepubkey(u.pubkey); u.pubkey = NULL; } + if(!u.name) u.name = newstring(name); + if(!u.desc) u.desc = newstring(desc); + u.pubkey = parsepubkey(pubkey); + switch(priv[0]) + { + case 'a': case 'A': u.privilege = PRIV_ADMIN; break; + case 'm': case 'M': default: u.privilege = PRIV_AUTH; break; + case 'n': case 'N': u.privilege = PRIV_NONE; break; + } + } + COMMAND(adduser, "ssss"); + + void clearusers() + { + users.clear(); + } + COMMAND(clearusers, ""); + + void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen) + { + char buf[2*sizeof(string)]; + formatstring(buf, "%d %d ", cn, sessionid); + concatstring(buf, pwd, sizeof(buf)); + if(!hashstring(buf, result, maxlen)) *result = '\0'; + } + + bool checkpassword(clientinfo *ci, const char *wanted, const char *given) + { + string hash; + hashpassword(ci->clientnum, ci->sessionid, wanted, hash, sizeof(hash)); + return !strcmp(hash, given); + } + + void revokemaster(clientinfo *ci) + { + ci->privilege = PRIV_NONE; + if(ci->state.state==CS_SPECTATOR && !ci->local) aiman::removeai(ci); + } + + extern void connected(clientinfo *ci); + + bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false) + { + if(authname && !val) return false; + const char *name = ""; + if(val) + { + bool haspass = adminpass[0] && checkpassword(ci, adminpass, pass); + int wantpriv = ci->local || haspass ? PRIV_ADMIN : authpriv; + if(wantpriv <= ci->privilege) return true; + else if(wantpriv <= PRIV_MASTER && !force) + { + if(ci->state.state==CS_SPECTATOR) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Spectators may not claim master."); + return false; + } + loopv(clients) if(ci!=clients[i] && clients[i]->privilege) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Master is already claimed."); + return false; + } + if(!authname && !(mastermask&MM_AUTOAPPROVE) && !ci->privilege && !ci->local) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "This server requires you to use the \"/auth\" command to claim master."); + return false; + } + } + if(trial) return true; + ci->privilege = wantpriv; + name = privname(ci->privilege); + } + else + { + if(!ci->privilege) return false; + if(trial) return true; + name = privname(ci->privilege); + revokemaster(ci); + } + bool hasmaster = false; + loopv(clients) if(clients[i]->local || clients[i]->privilege >= PRIV_MASTER) hasmaster = true; + if(!hasmaster) + { + mastermode = MM_OPEN; + allowedips.shrink(0); + } + string msg; + if(val && authname) + { + if(authdesc && authdesc[0]) formatstring(msg, "%s claimed %s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), name, authname, authdesc); + else formatstring(msg, "%s claimed %s as '\fs\f5%s\fr'", colorname(ci), name, authname); + } + else formatstring(msg, "%s %s %s", colorname(ci), val ? "claimed" : "relinquished", name); + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_SERVMSG); + sendstring(msg, p); + putint(p, N_CURRENTMASTER); + putint(p, mastermode); + loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) + { + putint(p, clients[i]->clientnum); + putint(p, clients[i]->privilege); + } + putint(p, -1); + sendpacket(-1, 1, p.finalize()); + checkpausegame(); + return true; + } + + bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false) + { + int priv = ci->privilege; + if(authname) + { + if(priv >= authpriv || ci->local) authname = authdesc = NULL; + else priv = authpriv; + } + if((priv || ci->local) && ci->clientnum!=victim) + { + clientinfo *vinfo = (clientinfo *)getclientinfo(victim); + if(vinfo && vinfo->connected && (priv >= vinfo->privilege || ci->local) && vinfo->privilege < PRIV_ADMIN && !vinfo->local) + { + if(trial) return true; + string kicker; + if(authname) + { + if(authdesc && authdesc[0]) formatstring(kicker, "%s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), authname, authdesc); + else formatstring(kicker, "%s as '\fs\f5%s\fr'", colorname(ci), authname); + } + else copystring(kicker, colorname(ci)); + if(reason && reason[0]) sendservmsgf("%s kicked %s because: %s", kicker, colorname(vinfo), reason); + else sendservmsgf("%s kicked %s", kicker, colorname(vinfo)); + uint ip = getclientip(victim); + addban(ip, 4*60*60000); + kickclients(ip, ci, priv); + } + } + return false; + } + + savedscore *findscore(clientinfo *ci, bool insert) + { + uint ip = getclientip(ci->clientnum); + if(!ip && !ci->local) return 0; + if(!insert) + { + loopv(clients) + { + clientinfo *oi = clients[i]; + if(oi->clientnum != ci->clientnum && getclientip(oi->clientnum) == ip && !strcmp(oi->name, ci->name)) + { + oi->state.timeplayed += lastmillis - oi->state.lasttimeplayed; + oi->state.lasttimeplayed = lastmillis; + static savedscore curscore; + curscore.save(oi->state); + return &curscore; + } + } + } + loopv(scores) + { + savedscore &sc = scores[i]; + if(sc.ip == ip && !strcmp(sc.name, ci->name)) return ≻ + } + if(!insert) return 0; + savedscore &sc = scores.add(); + sc.ip = ip; + copystring(sc.name, ci->name); + return ≻ + } + + void savescore(clientinfo *ci) + { + savedscore *sc = findscore(ci, true); + if(sc) sc->save(ci->state); + } + + static struct msgfilter + { + uchar msgmask[NUMMSG]; + + msgfilter(int msg, ...) + { + memset(msgmask, 0, sizeof(msgmask)); + va_list msgs; + va_start(msgs, msg); + for(uchar val = 1; msg < NUMMSG; msg = va_arg(msgs, int)) + { + if(msg < 0) val = uchar(-msg); + else msgmask[msg] = val; + } + va_end(msgs); + } + + uchar operator[](int msg) const { return msg >= 0 && msg < NUMMSG ? msgmask[msg] : 0; } + } msgfilter(-1, N_CONNECT, N_SERVINFO, N_INITCLIENT, N_WELCOME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, N_DIED, + N_SPAWNSTATE, N_FORCEDEATH, N_TEAMINFO, N_ITEMACC, N_ITEMSPAWN, N_TIMEUP, N_CDIS, N_CURRENTMASTER, N_PONG, N_RESUME, + N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_SENDMAP, + N_CLIENT, N_AUTHCHAL, N_INITAI, N_EXPIRETOKENS, N_DROPTOKENS, N_STEALTOKENS, N_DEMOPACKET, -2, N_REMIP, + N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, -3, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, + N_DELCUBE, N_EDITVAR, N_EDITVSLOT, N_UNDO, N_REDO, -4, N_POS, NUMMSG), + connectfilter(-1, N_CONNECT, -2, N_AUTHANS, -3, N_PING, NUMMSG); + + int checktype(int type, clientinfo *ci) + { + if(ci) + { + if(!ci->connected) switch(connectfilter[type]) + { + // allow only before authconnect + case 1: return !ci->connectauth ? type : -1; + // allow only during authconnect + case 2: return ci->connectauth ? type : -1; + // always allow + case 3: return type; + // never allow + default: return -1; + } + if(ci->local) return type; + } + switch(msgfilter[type]) + { + // server-only messages + case 1: return ci ? -1 : type; + // only allowed in coop-edit + case 2: if(m_edit) break; return -1; + // only allowed in coop-edit, no overflow check + case 3: return m_edit ? type : -1; + // no overflow check + case 4: return type; + } + if(ci && ++ci->overflow >= 200) return -2; + return type; + } + + struct worldstate + { + int uses, len; + uchar *data; + + worldstate() : uses(0), len(0), data(NULL) {} + + void setup(int n) { len = n; data = new uchar[n]; } + void cleanup() { DELETEA(data); len = 0; } + bool contains(const uchar *p) const { return p >= data && p < &data[len]; } + }; + vector worldstates; + bool reliablemessages = false; + + void cleanworldstate(ENetPacket *packet) + { + loopv(worldstates) + { + worldstate &ws = worldstates[i]; + if(!ws.contains(packet->data)) continue; + ws.uses--; + if(ws.uses <= 0) + { + ws.cleanup(); + worldstates.removeunordered(i); + } + break; + } + } + + void flushclientposition(clientinfo &ci) + { + if(ci.position.empty() || (!hasnonlocalclients() && !demorecord)) return; + packetbuf p(ci.position.length(), 0); + p.put(ci.position.getbuf(), ci.position.length()); + ci.position.setsize(0); + sendpacket(-1, 0, p.finalize(), ci.ownernum); + } + + static void sendpositions(worldstate &ws, ucharbuf &wsbuf) + { + if(wsbuf.empty()) return; + int wslen = wsbuf.length(); + recordpacket(0, wsbuf.buf, wslen); + wsbuf.put(wsbuf.buf, wslen); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + uchar *data = wsbuf.buf; + int size = wslen; + if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } + if(size <= 0) continue; + ENetPacket *packet = enet_packet_create(data, size, ENET_PACKET_FLAG_NO_ALLOCATE); + sendpacket(ci.clientnum, 0, packet); + if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } + else enet_packet_destroy(packet); + } + wsbuf.offset(wsbuf.length()); + } + + static inline void addposition(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) + { + if(bi.position.empty()) return; + if(wsbuf.length() + bi.position.length() > mtu) sendpositions(ws, wsbuf); + int offset = wsbuf.length(); + wsbuf.put(bi.position.getbuf(), bi.position.length()); + bi.position.setsize(0); + int len = wsbuf.length() - offset; + if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } + else ci.wslen += len; + } + + static void sendmessages(worldstate &ws, ucharbuf &wsbuf) + { + if(wsbuf.empty()) return; + int wslen = wsbuf.length(); + recordpacket(1, wsbuf.buf, wslen); + wsbuf.put(wsbuf.buf, wslen); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + uchar *data = wsbuf.buf; + int size = wslen; + if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } + if(size <= 0) continue; + ENetPacket *packet = enet_packet_create(data, size, (reliablemessages ? ENET_PACKET_FLAG_RELIABLE : 0) | ENET_PACKET_FLAG_NO_ALLOCATE); + sendpacket(ci.clientnum, 1, packet); + if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } + else enet_packet_destroy(packet); + } + wsbuf.offset(wsbuf.length()); + } + + static inline void addmessages(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) + { + if(bi.messages.empty()) return; + if(wsbuf.length() + 10 + bi.messages.length() > mtu) sendmessages(ws, wsbuf); + int offset = wsbuf.length(); + putint(wsbuf, N_CLIENT); + putint(wsbuf, bi.clientnum); + putuint(wsbuf, bi.messages.length()); + wsbuf.put(bi.messages.getbuf(), bi.messages.length()); + bi.messages.setsize(0); + int len = wsbuf.length() - offset; + if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } + else ci.wslen += len; + } + + bool buildworldstate() + { + int wsmax = 0; + loopv(clients) + { + clientinfo &ci = *clients[i]; + ci.overflow = 0; + ci.wsdata = NULL; + wsmax += ci.position.length(); + if(ci.messages.length()) wsmax += 10 + ci.messages.length(); + } + if(wsmax <= 0) + { + reliablemessages = false; + return false; + } + worldstate &ws = worldstates.add(); + ws.setup(2*wsmax); + int mtu = getservermtu() - 100; + if(mtu <= 0) mtu = ws.len; + ucharbuf wsbuf(ws.data, ws.len); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + addposition(ws, wsbuf, mtu, ci, ci); + loopvj(ci.bots) addposition(ws, wsbuf, mtu, *ci.bots[j], ci); + } + sendpositions(ws, wsbuf); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + addmessages(ws, wsbuf, mtu, ci, ci); + loopvj(ci.bots) addmessages(ws, wsbuf, mtu, *ci.bots[j], ci); + } + sendmessages(ws, wsbuf); + reliablemessages = false; + if(ws.uses) return true; + ws.cleanup(); + worldstates.drop(); + return false; + } + + bool sendpackets(bool force) + { + if(clients.empty() || (!hasnonlocalclients() && !demorecord)) return false; + enet_uint32 curtime = enet_time_get()-lastsend; + if(curtime<33 && !force) return false; + bool flush = buildworldstate(); + lastsend += curtime - (curtime%33); + return flush; + } + + template + void sendstate(gamestate &gs, T &p) + { + putint(p, gs.lifesequence); + putint(p, gs.health); + putint(p, gs.maxhealth); + putint(p, gs.armour); + putint(p, gs.maxarmour); + putint(p, gs.armourtype); + putint(p, gs.gunselect); + loopi(GUN_PISTOL-GUN_SG+1) putint(p, gs.ammo[GUN_SG+i]); + } + + void spawnstate(clientinfo *ci) + { + gamestate &gs = ci->state; + gs.spawnstate(gamemode); + gs.lifesequence = (gs.lifesequence + 1)&0x7F; + } + + void sendspawn(clientinfo *ci) + { + gamestate &gs = ci->state; + spawnstate(ci); + sendf(ci->ownernum, 1, "rii8v", N_SPAWNSTATE, ci->clientnum, gs.lifesequence, + gs.health, gs.maxhealth, + gs.armour, gs.maxarmour, gs.armourtype, + gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG]); + gs.lastspawn = gamemillis; + } + + void sendwelcome(clientinfo *ci) + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + int chan = welcomepacket(p, ci); + sendpacket(ci->clientnum, chan, p.finalize()); + } + + void putinitclient(clientinfo *ci, packetbuf &p) + { + if(ci->state.aitype != AI_NONE) + { + putint(p, N_INITAI); + putint(p, ci->clientnum); + putint(p, ci->ownernum); + putint(p, ci->state.aitype); + putint(p, ci->state.skill); + putint(p, ci->playermodel); + sendstring(ci->name, p); + sendstring(ci->team, p); + } + else + { + putint(p, N_INITCLIENT); + putint(p, ci->clientnum); + sendstring(ci->name, p); + sendstring(ci->team, p); + putint(p, ci->playermodel); + } + } + + void welcomeinitclient(packetbuf &p, int exclude = -1) + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(!ci->connected || ci->clientnum == exclude) continue; + + putinitclient(ci, p); + } + } + + bool hasmap(clientinfo *ci) + { + return (m_edit && (clients.length() > 0 || ci->local)) || + (smapname[0] && (!m_timed || gamemillis < gamelimit || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || numclients(ci->clientnum, true, true, true))); + } + + int welcomepacket(packetbuf &p, clientinfo *ci) + { + putint(p, N_WELCOME); + putint(p, N_MAPCHANGE); + sendstring(smapname, p); + putint(p, gamemode); + putint(p, notgotitems ? 1 : 0); + if(!ci || (m_timed && smapname[0])) + { + putint(p, N_TIMEUP); + putint(p, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); + } + if(!notgotitems) + { + putint(p, N_ITEMLIST); + loopv(sents) if(sents[i].spawned) + { + putint(p, i); + putint(p, sents[i].type); + } + putint(p, -1); + } + bool hasmaster = false; + if(mastermode != MM_OPEN) + { + putint(p, N_CURRENTMASTER); + putint(p, mastermode); + hasmaster = true; + } + loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) + { + if(!hasmaster) + { + putint(p, N_CURRENTMASTER); + putint(p, mastermode); + hasmaster = true; + } + putint(p, clients[i]->clientnum); + putint(p, clients[i]->privilege); + } + if(hasmaster) putint(p, -1); + if(gamepaused) + { + putint(p, N_PAUSEGAME); + putint(p, 1); + putint(p, -1); + } + if(gamespeed != 100) + { + putint(p, N_GAMESPEED); + putint(p, gamespeed); + putint(p, -1); + } + if(m_teammode) + { + putint(p, N_TEAMINFO); + enumerate(teaminfos, teaminfo, t, + if(t.frags) { sendstring(t.team, p); putint(p, t.frags); } + ); + sendstring("", p); + } + if(ci) + { + putint(p, N_SETTEAM); + putint(p, ci->clientnum); + sendstring(ci->team, p); + putint(p, -1); + } + if(ci && (m_demo || m_mp(gamemode)) && ci->state.state!=CS_SPECTATOR) + { + gamestate &gs = ci->state; + spawnstate(ci); + putint(p, N_SPAWNSTATE); + putint(p, ci->clientnum); + sendstate(gs, p); + gs.lastspawn = gamemillis; + } + if(ci && ci->state.state==CS_SPECTATOR) + { + putint(p, N_SPECTATOR); + putint(p, ci->clientnum); + putint(p, 1); + sendf(-1, 1, "ri3x", N_SPECTATOR, ci->clientnum, 1, ci->clientnum); + } + if(!ci || clients.length()>1) + { + putint(p, N_RESUME); + loopv(clients) + { + clientinfo *oi = clients[i]; + if(ci && oi->clientnum==ci->clientnum) continue; + putint(p, oi->clientnum); + putint(p, oi->state.state); + putint(p, oi->state.frags); + putint(p, oi->state.flags); + putint(p, oi->state.deaths); + putint(p, oi->state.quadmillis); + sendstate(oi->state, p); + } + putint(p, -1); + welcomeinitclient(p, ci ? ci->clientnum : -1); + } + return 1; + } + + bool restorescore(clientinfo *ci) + { + //if(ci->local) return false; + savedscore *sc = findscore(ci, false); + if(sc) + { + sc->restore(ci->state); + return true; + } + return false; + } + + void sendresume(clientinfo *ci) + { + gamestate &gs = ci->state; + sendf(-1, 1, "ri3i5i6vi", N_RESUME, ci->clientnum, gs.state, + gs.frags, gs.flags, gs.deaths, gs.quadmillis, + gs.lifesequence, + gs.health, gs.maxhealth, + gs.armour, gs.maxarmour, gs.armourtype, + gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG], -1); + } + + void sendinitclient(clientinfo *ci) + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putinitclient(ci, p); + sendpacket(-1, 1, p.finalize(), ci->clientnum); + } + + void loaditems() + { + resetitems(); + notgotitems = true; + if(m_edit || !loadents(smapname, ments, &mcrc)) + return; + loopv(ments) if(canspawnitem(ments[i].type)) + { + server_entity se = { NOTUSED, 0, false }; + while(sents.length()<=i) sents.add(se); + sents[i].type = ments[i].type; + if(m_mp(gamemode) && delayspawn(sents[i].type)) sents[i].spawntime = spawntime(sents[i].type); + else sents[i].spawned = true; + } + notgotitems = false; + } + + void changemap(const char *s, int mode) + { + stopdemo(); + pausegame(false); + changegamespeed(100); + aiman::clearai(); + + gamemode = mode; + gamemillis = 0; + gamelimit = 10*60000; + interm = 0; + nextexceeded = 0; + copystring(smapname, s); + loaditems(); + scores.shrink(0); + shouldcheckteamkills = false; + teamkills.shrink(0); + loopv(clients) + { + clientinfo *ci = clients[i]; + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + } + + if(!m_mp(gamemode)) kicknonlocalclients(DISC_LOCAL); + + sendf(-1, 1, "risii", N_MAPCHANGE, smapname, gamemode, 1); + + clearteaminfo(); + if(m_teammode) autoteam(); + + if(m_timed && smapname[0]) sendf(-1, 1, "ri2", N_TIMEUP, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); + loopv(clients) + { + clientinfo *ci = clients[i]; + ci->mapchange(); + ci->state.lasttimeplayed = lastmillis; + if(m_mp(gamemode) && ci->state.state!=CS_SPECTATOR) sendspawn(ci); + } + + aiman::changemap(); + + if(m_demo) + { + if(clients.length()) setupdemoplayback(); + } + else + { + if(demonextmatch) setupdemorecord(); + demonextmatch = autorecorddemo!=0; + } + } + + void rotatemap(bool next) + { + if(!maprotations.inrange(curmaprotation)) + { + changemap("", 1); + return; + } + if(next) + { + curmaprotation = findmaprotation(gamemode, smapname); + if(curmaprotation >= 0) nextmaprotation(); + else curmaprotation = smapname[0] ? max(findmaprotation(gamemode, ""), 0) : 0; + } + maprotation &rot = maprotations[curmaprotation]; + changemap(rot.map, rot.findmode(gamemode)); + } + + struct votecount + { + char *map; + int mode, count; + votecount() {} + votecount(char *s, int n) : map(s), mode(n), count(0) {} + }; + + void checkvotes(bool force = false) + { + vector votes; + int maxvotes = 0; + loopv(clients) + { + clientinfo *oi = clients[i]; + if(oi->state.state==CS_SPECTATOR && !oi->privilege && !oi->local) continue; + if(oi->state.aitype!=AI_NONE) continue; + maxvotes++; + if(!m_valid(oi->modevote)) continue; + votecount *vc = NULL; + loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote==votes[j].mode) + { + vc = &votes[j]; + break; + } + if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote)); + vc->count++; + } + votecount *best = NULL; + loopv(votes) if(!best || votes[i].count > best->count || (votes[i].count == best->count && rnd(2))) best = &votes[i]; + if(force || (best && best->count > maxvotes/2)) + { + sendpackets(true); + if(demorecord) enddemorecord(); + if(best && (best->count > (force ? 1 : maxvotes/2))) + { + sendservmsg(force ? "vote passed by default" : "vote passed by majority"); + changemap(best->map, best->mode); + } + else rotatemap(true); + } + } + + void forcemap(const char *map, int mode) + { + stopdemo(); + if(!map[0] && !m_check(mode, M_EDIT)) + { + int idx = findmaprotation(mode, smapname); + if(idx < 0 && smapname[0]) idx = findmaprotation(mode, ""); + if(idx < 0) return; + map = maprotations[idx].map; + } + if(hasnonlocalclients()) sendservmsgf("local player forced %s on map %s", modename(mode), map[0] ? map : "[new map]"); + changemap(map, mode); + } + + void vote(const char *map, int reqmode, int sender) + { + clientinfo *ci = getinfo(sender); + if(!ci || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || (!ci->local && !m_mp(reqmode))) return; + if(!m_valid(reqmode)) return; + if(!map[0] && !m_check(reqmode, M_EDIT)) + { + int idx = findmaprotation(reqmode, smapname); + if(idx < 0 && smapname[0]) idx = findmaprotation(reqmode, ""); + if(idx < 0) return; + map = maprotations[idx].map; + } + if(lockmaprotation && !ci->local && ci->privilege < (lockmaprotation > 1 ? PRIV_ADMIN : PRIV_MASTER) && findmaprotation(reqmode, map) < 0) + { + sendf(sender, 1, "ris", N_SERVMSG, "This server has locked the map rotation."); + return; + } + copystring(ci->mapvote, map); + ci->modevote = reqmode; + if(ci->local || (ci->privilege && mastermode>=MM_VETO)) + { + sendpackets(true); + if(demorecord) enddemorecord(); + if(!ci->local || hasnonlocalclients()) + sendservmsgf("%s forced %s on map %s", colorname(ci), modename(ci->modevote), ci->mapvote[0] ? ci->mapvote : "[new map]"); + changemap(ci->mapvote, ci->modevote); + } + else + { + sendservmsgf("%s suggests %s on map %s (select map to vote)", colorname(ci), modename(reqmode), map[0] ? map : "[new map]"); + checkvotes(); + } + } + + VAR(overtime, 0, 0, 1); + + bool checkovertime() + { + if(!m_timed || !overtime) return false; + const char* topteam = NULL; + int topscore = INT_MIN; + bool tied = false; + if(m_teammode) + { + vector scores; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; + int score = 0; + if(teaminfo *ti = teaminfos.access(ci->team)) score = ti->frags; + if(!topteam || score > topscore) { topteam = ci->team; topscore = score; tied = false; } + else if(score == topscore && strcmp(ci->team, topteam)) tied = true; + } + } + else + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR) continue; + int score = ci->state.frags; + if(score > topscore) { topscore = score; tied = false; } + else if(score == topscore) tied = true; + } + } + if(!tied) return false; + sendservmsg("the game is tied with overtime"); + gamelimit = max(gamemillis, gamelimit) + 2*60000; + sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); + return true; + } + + void checkintermission(bool force = false) + { + if(gamemillis >= gamelimit && !interm && (force || !checkovertime())) + { + sendf(-1, 1, "ri2", N_TIMEUP, 0); + changegamespeed(100); + interm = gamemillis + 10000; + } + } + + void startintermission() { gamelimit = min(gamelimit, gamemillis); checkintermission(true); } + + void dodamage(clientinfo *target, clientinfo *actor, int damage, int gun, const vec &hitpush = vec(0, 0, 0)) + { + gamestate &ts = target->state; + ts.dodamage(damage); + if(target!=actor && !isteam(target->team, actor->team)) actor->state.damage += damage; + sendf(-1, 1, "ri6", N_DAMAGE, target->clientnum, actor->clientnum, damage, ts.armour, ts.health); + if(target==actor) target->setpushed(); + else if(!hitpush.iszero()) + { + ivec v(vec(hitpush).rescale(DNF)); + sendf(ts.health<=0 ? -1 : target->ownernum, 1, "ri7", N_HITPUSH, target->clientnum, gun, damage, v.x, v.y, v.z); + target->setpushed(); + } + if(ts.health<=0) + { + target->state.deaths++; + int fragvalue = (target==actor || isteam(target->team, actor->team) ? -1 : 1); + actor->state.frags += fragvalue; + if(fragvalue>0) + { + int friends = 0, enemies = 0; // note: friends also includes the fragger + if(m_teammode) loopv(clients) if(strcmp(clients[i]->team, actor->team)) enemies++; else friends++; + else { friends = 1; enemies = clients.length()-1; } + actor->state.effectiveness += fragvalue*friends/float(max(enemies, 1)); + } + teaminfo *t = m_teammode ? teaminfos.access(actor->team) : NULL; + if(t) t->frags += fragvalue; + sendf(-1, 1, "ri5", N_DIED, target->clientnum, actor->clientnum, actor->state.frags, t ? t->frags : 0); + target->position.setsize(0); + ts.state = CS_DEAD; + ts.lastdeath = gamemillis; + if(actor!=target && isteam(actor->team, target->team)) + { + actor->state.teamkills++; + addteamkill(actor, target, 1); + } + ts.deadflush = ts.lastdeath + DEATHMILLIS; + // don't issue respawn yet until DEATHMILLIS has elapsed + // ts.respawn(); + } + } + + void suicide(clientinfo *ci) + { + gamestate &gs = ci->state; + if(gs.state!=CS_ALIVE) return; + int fragvalue = -1; + ci->state.frags += fragvalue; + ci->state.deaths++; + teaminfo *t = m_teammode ? teaminfos.access(ci->team) : NULL; + if(t) t->frags += fragvalue; + sendf(-1, 1, "ri5", N_DIED, ci->clientnum, ci->clientnum, gs.frags, t ? t->frags : 0); + ci->position.setsize(0); + gs.state = CS_DEAD; + gs.lastdeath = gamemillis; + gs.respawn(); + } + + void suicideevent::process(clientinfo *ci) + { + suicide(ci); + } + + void explodeevent::process(clientinfo *ci) + { + gamestate &gs = ci->state; + switch(gun) + { + case GUN_RL: + if(!gs.rockets.remove(id)) return; + break; + + case GUN_GL: + if(!gs.grenades.remove(id)) return; + break; + + default: + return; + } + sendf(-1, 1, "ri4x", N_EXPLODEFX, ci->clientnum, gun, id, ci->ownernum); + loopv(hits) + { + hitinfo &h = hits[i]; + clientinfo *target = getinfo(h.target); + if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.dist<0 || h.dist>guns[gun].exprad) continue; + + bool dup = false; + loopj(i) if(hits[j].target==h.target) { dup = true; break; } + if(dup) continue; + + int damage = guns[gun].damage; + if(gs.quadmillis) damage *= 4; + damage = int(damage*(1-h.dist/EXP_DISTSCALE/guns[gun].exprad)); + if(target==ci) damage /= EXP_SELFDAMDIV; + dodamage(target, ci, damage, gun, h.dir); + } + } + + void shotevent::process(clientinfo *ci) + { + gamestate &gs = ci->state; + int wait = millis - gs.lastshot; + if(!gs.isalive(gamemillis) || + waitGUN_PISTOL || + gs.ammo[gun]<=0 || (guns[gun].range && from.dist(to) > guns[gun].range + 1)) + return; + if(gun!=GUN_FIST) gs.ammo[gun]--; + gs.lastshot = millis; + gs.gunwait = guns[gun].attackdelay; + sendf(-1, 1, "rii9x", N_SHOTFX, ci->clientnum, gun, id, + int(from.x*DMF), int(from.y*DMF), int(from.z*DMF), + int(to.x*DMF), int(to.y*DMF), int(to.z*DMF), + ci->ownernum); + gs.shotdamage += guns[gun].damage*(gs.quadmillis ? 4 : 1)*guns[gun].rays; + switch(gun) + { + case GUN_RL: gs.rockets.add(id); break; + case GUN_GL: gs.grenades.add(id); break; + default: + { + int totalrays = 0, maxrays = guns[gun].rays; + loopv(hits) + { + hitinfo &h = hits[i]; + clientinfo *target = getinfo(h.target); + if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.rays<1 || h.dist > guns[gun].range + 1) continue; + + totalrays += h.rays; + if(totalrays>maxrays) continue; + int damage = h.rays*guns[gun].damage; + if(gs.quadmillis) damage *= 4; + dodamage(target, ci, damage, gun, h.dir); + } + break; + } + } + } + + void pickupevent::process(clientinfo *ci) + { + gamestate &gs = ci->state; + if(m_mp(gamemode) && !gs.isalive(gamemillis)) return; + pickup(ent, ci->clientnum); + } + + bool gameevent::flush(clientinfo *ci, int fmillis) + { + process(ci); + return true; + } + + bool timedevent::flush(clientinfo *ci, int fmillis) + { + if(millis > fmillis) return false; + else if(millis >= ci->lastevent) + { + ci->lastevent = millis; + process(ci); + } + return true; + } + + void clearevent(clientinfo *ci) + { + delete ci->events.remove(0); + } + + void flushevents(clientinfo *ci, int millis) + { + while(ci->events.length()) + { + gameevent *ev = ci->events[0]; + if(ev->flush(ci, millis)) clearevent(ci); + else break; + } + } + + void processevents() + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(curtime>0 && ci->state.quadmillis) ci->state.quadmillis = max(ci->state.quadmillis-curtime, 0); + flushevents(ci, gamemillis); + } + } + + void cleartimedevents(clientinfo *ci) + { + int keep = 0; + loopv(ci->events) + { + if(ci->events[i]->keepable()) + { + if(keep < i) + { + for(int j = keep; j < i; j++) delete ci->events[j]; + ci->events.remove(keep, i - keep); + i = keep; + } + keep = i+1; + continue; + } + } + while(ci->events.length() > keep) delete ci->events.pop(); + ci->timesync = false; + } + + void serverupdate() + { + if(shouldstep && !gamepaused) + { + gamemillis += curtime; + + if(m_demo) readdemo(); + else if(!m_timed || gamemillis < gamelimit) + { + processevents(); + if(curtime) + { + loopv(sents) if(sents[i].spawntime) // spawn entities when timer reached + { + int oldtime = sents[i].spawntime; + sents[i].spawntime -= curtime; + if(sents[i].spawntime<=0) + { + sents[i].spawntime = 0; + sents[i].spawned = true; + sendf(-1, 1, "ri2", N_ITEMSPAWN, i); + } + else if(sents[i].spawntime<=10000 && oldtime>10000 && (sents[i].type==I_QUAD || sents[i].type==I_BOOST)) + { + sendf(-1, 1, "ri2", N_ANNOUNCE, sents[i].type); + } + } + } + aiman::checkai(); + } + } + + while(bannedips.length() && bannedips[0].expire-totalmillis <= 0) bannedips.remove(0); + //~loopv(connects) if(totalmillis-connects[i]->connectmillis>15000) disconnect_client(connects[i]->clientnum, DISC_TIMEOUT); + + if(nextexceeded && gamemillis > nextexceeded && (!m_timed || gamemillis < gamelimit)) + { + nextexceeded = 0; + loopvrev(clients) + { + clientinfo &c = *clients[i]; + if(c.state.aitype != AI_NONE) continue; + //~if(c.checkexceeded()) disconnect_client(c.clientnum, DISC_MSGERR); + else c.scheduleexceeded(); + } + } + + if(shouldcheckteamkills) checkteamkills(); + + if(shouldstep && !gamepaused) + { + if(m_timed && smapname[0] && gamemillis-curtime>0) checkintermission(); + if(interm > 0 && gamemillis>interm) + { + if(demorecord) enddemorecord(); + interm = -1; + checkvotes(true); + } + } + + shouldstep = clients.length() > 0; + } + + void forcespectator(clientinfo *ci) + { + if(ci->state.state==CS_ALIVE) suicide(ci); + ci->state.state = CS_SPECTATOR; + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + if(!ci->local && (!ci->privilege || ci->warned)) aiman::removeai(ci); + sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 1); + } + + struct crcinfo + { + int crc, matches; + + crcinfo() {} + crcinfo(int crc, int matches) : crc(crc), matches(matches) {} + + static bool compare(const crcinfo &x, const crcinfo &y) { return x.matches > y.matches; } + }; + + VAR(modifiedmapspectator, 0, 1, 2); + + void checkmaps(int req = -1) + { + if(m_edit || !smapname[0]) return; + vector crcs; + int total = 0, unsent = 0, invalid = 0; + if(mcrc) crcs.add(crcinfo(mcrc, clients.length() + 1)); + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE) continue; + total++; + if(!ci->clientmap[0]) + { + if(ci->mapcrc < 0) invalid++; + else if(!ci->mapcrc) unsent++; + } + else + { + crcinfo *match = NULL; + loopvj(crcs) if(crcs[j].crc == ci->mapcrc) { match = &crcs[j]; break; } + if(!match) crcs.add(crcinfo(ci->mapcrc, 1)); + else match->matches++; + } + } + if(!mcrc && total - unsent < min(total, 4)) return; + crcs.sort(crcinfo::compare); + string msg; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue; + formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); + sendf(req, 1, "ris", N_SERVMSG, msg); + if(req < 0) ci->warned = true; + } + if(crcs.length() >= 2) loopv(crcs) + { + crcinfo &info = crcs[i]; + if(i || info.matches <= crcs[i+1].matches) loopvj(clients) + { + clientinfo *ci = clients[j]; + if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue; + formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); + sendf(req, 1, "ris", N_SERVMSG, msg); + if(req < 0) ci->warned = true; + } + } + if(req < 0 && modifiedmapspectator && (mcrc || modifiedmapspectator > 1)) loopv(clients) + { + clientinfo *ci = clients[i]; + if(!ci->local && ci->warned && ci->state.state != CS_SPECTATOR) forcespectator(ci); + } + } + + bool shouldspectate(clientinfo *ci) + { + return !ci->local && ci->warned && modifiedmapspectator && (mcrc || modifiedmapspectator > 1); + } + + void unspectate(clientinfo *ci) + { + if(shouldspectate(ci)) return; + ci->state.state = CS_DEAD; + ci->state.respawn(); + ci->state.lasttimeplayed = lastmillis; + aiman::addclient(ci); + sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 0); + if(ci->clientmap[0] || ci->mapcrc) checkmaps(); + if(!hasmap(ci)) rotatemap(true); + } + + void sendservinfo(clientinfo *ci) + { + sendf(ci->clientnum, 1, "ri5ss", N_SERVINFO, ci->clientnum, PROTOCOL_VERSION, ci->sessionid, serverpass[0] ? 1 : 0, serverdesc, serverauth); + } + + void noclients() + { + bannedips.shrink(0); + aiman::clearai(); + } + + void localconnect(int n) + { + clientinfo *ci = getinfo(n); + ci->clientnum = ci->ownernum = n; + ci->connectmillis = totalmillis; + ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; + ci->local = true; + + connects.add(ci); + sendservinfo(ci); + } + + void localdisconnect(int n) + { + if(m_demo) enddemoplayback(); + clientdisconnect(n); + } + + int clientconnect(int n) + { + clientinfo *ci = getinfo(n); + ci->clientnum = ci->ownernum = n; + ci->connectmillis = totalmillis; + ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; + + connects.add(ci); + if(!m_mp(gamemode)) return DISC_LOCAL; + sendservinfo(ci); + return DISC_NONE; + } + + void clientdisconnect(int n) + { + clientinfo *ci = getinfo(n); + loopv(clients) if(clients[i]->authkickvictim == ci->clientnum) clients[i]->cleanauth(); + if(ci->connected) + { + if(ci->privilege) setmaster(ci, false); + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + savescore(ci); + sendf(-1, 1, "ri2", N_CDIS, n); + clients.removeobj(ci); + aiman::removeai(ci); + if(!numclients(-1, false, true)) noclients(); // bans clear when server empties + if(ci->local) checkpausegame(); + } + else connects.removeobj(ci); + } + + int reserveclients() { return 3; } + + extern void verifybans(); + + struct banlist + { + vector bans; + + void clear() { bans.shrink(0); } + + bool check(uint ip) + { + loopv(bans) if(bans[i].check(ip)) return true; + return false; + } + + void add(const char *ipname) + { + ipmask ban; + ban.parse(ipname); + bans.add(ban); + + verifybans(); + } + } ipbans, gbans; + + bool checkbans(uint ip) + { + loopv(bannedips) if(bannedips[i].ip==ip) return true; + return ipbans.check(ip) || gbans.check(ip); + } + + void verifybans() + { + loopvrev(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.aitype != AI_NONE || ci->local || ci->privilege >= PRIV_ADMIN) continue; + //~if(checkbans(getclientip(ci->clientnum))) disconnect_client(ci->clientnum, DISC_IPBAN); + } + } + + ICOMMAND(clearipbans, "", (), ipbans.clear()); + ICOMMAND(ipban, "s", (const char *ipname), ipbans.add(ipname)); + + int allowconnect(clientinfo *ci, const char *pwd = "") + { + if(ci->local) return DISC_NONE; + if(!m_mp(gamemode)) return DISC_LOCAL; + if(serverpass[0]) + { + if(!checkpassword(ci, serverpass, pwd)) return DISC_PASSWORD; + return DISC_NONE; + } + if(adminpass[0] && checkpassword(ci, adminpass, pwd)) return DISC_NONE; + if(numclients(-1, false, true)>=maxclients) return DISC_MAXCLIENTS; + uint ip = getclientip(ci->clientnum); + if(checkbans(ip)) return DISC_IPBAN; + if(mastermode>=MM_PRIVATE && allowedips.find(ip)<0) return DISC_PRIVATE; + return DISC_NONE; + } + + bool allowbroadcast(int n) + { + clientinfo *ci = getinfo(n); + return ci && ci->connected; + } + + clientinfo *findauth(uint id) + { + loopv(clients) if(clients[i]->authreq == id) return clients[i]; + return NULL; + } + + + void authfailed(clientinfo *ci) + { + if(!ci) return; + ci->cleanauth(); + //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); + } + + void authfailed(uint id) + { + authfailed(findauth(id)); + } + + void authsucceeded(uint id) + { + clientinfo *ci = findauth(id); + if(!ci) return; + ci->cleanauth(ci->connectauth!=0); + if(ci->connectauth) connected(ci); + if(ci->authkickvictim >= 0) + { + if(setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH, false, true)) + trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH); + ci->cleanauthkick(); + } + else setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH); + } + + void authchallenged(uint id, const char *val, const char *desc = "") + { + clientinfo *ci = findauth(id); + if(!ci) return; + sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val); + } + + uint nextauthreq = 0; + + bool tryauth(clientinfo *ci, const char *user, const char *desc) + { + ci->cleanauth(); + if(!nextauthreq) nextauthreq = 1; + ci->authreq = nextauthreq++; + filtertext(ci->authname, user, false, false, 100); + copystring(ci->authdesc, desc); + if(ci->authdesc[0]) + { + userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); + if(u) + { + uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() }; + vector buf; + ci->authchallenge = genchallenge(u->pubkey, seed, sizeof(seed), buf); + sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, ci->authreq, buf.getbuf()); + } + else ci->cleanauth(); + } + else if(!requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname)) + { + ci->cleanauth(); + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); + } + if(ci->authreq) return true; + //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); + return false; + } + + bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc) + { + if(ci->authreq != id || strcmp(ci->authdesc, desc)) + { + ci->cleanauth(); + return !ci->connectauth; + } + for(char *s = val; *s; s++) + { + if(!isxdigit(*s)) { *s = '\0'; break; } + } + if(desc[0]) + { + if(ci->authchallenge && checkchallenge(val, ci->authchallenge)) + { + userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); + if(u) + { + if(ci->connectauth) connected(ci); + if(ci->authkickvictim >= 0) + { + if(setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege, false, true)) + trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, u->privilege); + } + else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege); + } + } + ci->cleanauth(); + } + else if(!requestmasterf("confauth %u %s\n", id, val)) + { + ci->cleanauth(); + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); + } + return ci->authreq || !ci->connectauth; + } + + void masterconnected() + { + } + + void masterdisconnected() + { + loopvrev(clients) + { + clientinfo *ci = clients[i]; + if(ci->authreq) authfailed(ci); + } + } + + void processmasterinput(const char *cmd, int cmdlen, const char *args) + { + uint id; + string val; + if(sscanf(cmd, "failauth %u", &id) == 1) + authfailed(id); + else if(sscanf(cmd, "succauth %u", &id) == 1) + authsucceeded(id); + else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2) + authchallenged(id, val); + else if(matchstring(cmd, cmdlen, "cleargbans")) + gbans.clear(); + else if(sscanf(cmd, "addgban %100s", val) == 1) + gbans.add(val); + } + + void receivefile(int sender, uchar *data, int len) + { + if(!m_edit || len <= 0 || len > 4*1024*1024) return; + clientinfo *ci = getinfo(sender); + if(ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) return; + if(mapdata) DELETEP(mapdata); + mapdata = opentempfile("mapdata", "w+b"); + if(!mapdata) { sendf(sender, 1, "ris", N_SERVMSG, "failed to open temporary file for map"); return; } + mapdata->write(data, len); + sendservmsgf("[%s sent a map to server, \"/getmap\" to receive it]", colorname(ci)); + } + + void sendclipboard(clientinfo *ci) + { + if(!ci->lastclipboard || !ci->clipboard) return; + bool flushed = false; + loopv(clients) + { + clientinfo &e = *clients[i]; + if(e.clientnum != ci->clientnum && e.needclipboard - ci->lastclipboard >= 0) + { + if(!flushed) { flushserver(true); flushed = true; } + sendpacket(e.clientnum, 1, ci->clipboard); + } + } + } + + void connected(clientinfo *ci) + { + if(m_demo) enddemoplayback(); + + if(!hasmap(ci)) rotatemap(false); + + shouldstep = true; + + connects.removeobj(ci); + clients.add(ci); + + ci->connectauth = 0; + ci->connected = true; + ci->needclipboard = totalmillis ? totalmillis : 1; + if(mastermode>=MM_LOCKED) ci->state.state = CS_SPECTATOR; + ci->state.lasttimeplayed = lastmillis; + + const char *worst = m_teammode ? chooseworstteam(NULL, ci) : NULL; + copystring(ci->team, worst ? worst : "good", MAXTEAMLEN+1); + + sendwelcome(ci); + if(restorescore(ci)) sendresume(ci); + sendinitclient(ci); + + aiman::addclient(ci); + + if(m_demo) setupdemoplayback(); + + if(servermotd[0]) sendf(ci->clientnum, 1, "ris", N_SERVMSG, servermotd); + } + + void parsepacket(int sender, int chan, packetbuf &p) // has to parse exactly each byte of the packet + { + if(sender<0 || p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED || chan > 2) return; + char text[MAXTRANS]; + int type; + clientinfo *ci = sender>=0 ? getinfo(sender) : NULL, *cq = ci, *cm = ci; + if(ci && !ci->connected) + { + if(chan==0) return; + //~else if(chan!=1) { disconnect_client(sender, DISC_MSGERR); return; } + else while(p.length() < p.maxlen) switch(checktype(getint(p), ci)) + { + case N_CONNECT: + { + getstring(text, p); + filtertext(text, text, false, false, MAXNAMELEN); + if(!text[0]) copystring(text, "Anonymous"); + copystring(ci->name, text, MAXNAMELEN+1); + ci->playermodel = 0; + + string password, authdesc, authname; + getstring(password, p, sizeof(password)); + getstring(authdesc, p, sizeof(authdesc)); + getstring(authname, p, sizeof(authname)); + int disc = allowconnect(ci, password); + if(disc) + { + if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !tryauth(ci, authname, authdesc)) + { + //~disconnect_client(sender, disc); + return; + } + ci->connectauth = disc; + } + else connected(ci); + break; + } + + case N_AUTHANS: + { + string desc, ans; + getstring(desc, p, sizeof(desc)); + uint id = (uint)getint(p); + getstring(ans, p, sizeof(ans)); + if(!answerchallenge(ci, id, ans, desc)) + { + //~disconnect_client(sender, ci->connectauth); + return; + } + break; + } + + case N_PING: + getint(p); + break; + + default: + //~disconnect_client(sender, DISC_MSGERR); + return; + } + return; + } + else if(chan==2) + { + receivefile(sender, p.buf, p.maxlen); + return; + } + + if(p.packet->flags&ENET_PACKET_FLAG_RELIABLE) reliablemessages = true; + #define QUEUE_AI clientinfo *cm = cq; + #define QUEUE_MSG { if(cm && (!cm->local || demorecord || hasnonlocalclients())) while(curmsgmessages.add(p.buf[curmsg++]); } + #define QUEUE_BUF(body) { \ + if(cm && (!cm->local || demorecord || hasnonlocalclients())) \ + { \ + curmsg = p.length(); \ + { body; } \ + } \ + } + #define QUEUE_INT(n) QUEUE_BUF(putint(cm->messages, n)) + #define QUEUE_UINT(n) QUEUE_BUF(putuint(cm->messages, n)) + #define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cm->messages)) + int curmsg; + while((curmsg = p.length()) < p.maxlen) switch(type = checktype(getint(p), ci)) + { + case N_POS: + { + int pcn = getuint(p); + p.get(); + uint flags = getuint(p); + clientinfo *cp = getinfo(pcn); + if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; + vec pos; + loopk(3) + { + int n = p.get(); n |= p.get()<<8; if(flags&(1<local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) + { + if(!ci->local && !m_edit && max(vel.magnitude2(), (float)fabs(vel.z)) >= 180) + cp->setexceeded(); + cp->position.setsize(0); + while(curmsgposition.add(p.buf[curmsg++]); + } + cp->state.o = pos; + cp->gameclip = (flags&0x80)!=0; + } + break; + } + + case N_TELEPORT: + { + int pcn = getint(p), teleport = getint(p), teledest = getint(p); + clientinfo *cp = getinfo(pcn); + if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; + if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) + { + flushclientposition(*cp); + sendf(-1, 0, "ri4x", N_TELEPORT, pcn, teleport, teledest, cp->ownernum); + } + break; + } + + case N_JUMPPAD: + { + int pcn = getint(p), jumppad = getint(p); + clientinfo *cp = getinfo(pcn); + if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; + if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) + { + cp->setpushed(); + flushclientposition(*cp); + sendf(-1, 0, "ri3x", N_JUMPPAD, pcn, jumppad, cp->ownernum); + } + break; + } + + case N_FROMAI: + { + int qcn = getint(p); + if(qcn < 0) cq = ci; + else + { + cq = getinfo(qcn); + if(cq && qcn != sender && cq->ownernum != sender) cq = NULL; + } + break; + } + + case N_EDITMODE: + { + int val = getint(p); + if(!ci->local && !m_edit) break; + if(val ? ci->state.state!=CS_ALIVE && ci->state.state!=CS_DEAD : ci->state.state!=CS_EDITING) break; + if(val) + { + ci->state.editstate = ci->state.state; + ci->state.state = CS_EDITING; + ci->events.setsize(0); + ci->state.rockets.reset(); + ci->state.grenades.reset(); + } + else ci->state.state = ci->state.editstate; + QUEUE_MSG; + break; + } + + case N_MAPCRC: + { + getstring(text, p); + int crc = getint(p); + if(!ci) break; + if(strcmp(text, smapname)) + { + if(ci->clientmap[0]) + { + ci->clientmap[0] = '\0'; + ci->mapcrc = 0; + } + else if(ci->mapcrc > 0) ci->mapcrc = 0; + break; + } + copystring(ci->clientmap, text); + ci->mapcrc = text[0] ? crc : 1; + checkmaps(); + if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; + break; + } + + case N_CHECKMAPS: + checkmaps(sender); + break; + + case N_TRYSPAWN: + if(!ci || !cq || cq->state.state!=CS_DEAD || cq->state.lastspawn>=0) break; + if(!ci->clientmap[0] && !ci->mapcrc) + { + ci->mapcrc = -1; + checkmaps(); + if(ci == cq) { if(ci->state.state != CS_DEAD) break; } + else if(cq->ownernum != ci->clientnum) { cq = NULL; break; } + } + if(cq->state.deadflush) + { + flushevents(cq, cq->state.deadflush); + cq->state.respawn(); + } + cleartimedevents(cq); + sendspawn(cq); + break; + + case N_GUNSELECT: + { + int gunselect = getint(p); + if(!cq || cq->state.state!=CS_ALIVE) break; + cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; + QUEUE_AI; + QUEUE_MSG; + break; + } + + case N_SPAWN: + { + int ls = getint(p), gunselect = getint(p); + if(!cq || (cq->state.state!=CS_ALIVE && cq->state.state!=CS_DEAD && cq->state.state!=CS_EDITING) || ls!=cq->state.lifesequence || cq->state.lastspawn<0) break; + cq->state.lastspawn = -1; + cq->state.state = CS_ALIVE; + cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; + cq->exceeded = 0; + QUEUE_AI; + QUEUE_BUF({ + putint(cm->messages, N_SPAWN); + sendstate(cq->state, cm->messages); + }); + break; + } + + case N_SUICIDE: + { + if(cq) cq->addevent(new suicideevent); + break; + } + + case N_SHOOT: + { + shotevent *shot = new shotevent; + shot->id = getint(p); + shot->millis = cq ? cq->geteventmillis(gamemillis, shot->id) : 0; + shot->gun = getint(p); + loopk(3) shot->from[k] = getint(p)/DMF; + loopk(3) shot->to[k] = getint(p)/DMF; + int hits = getint(p); + loopk(hits) + { + if(p.overread()) break; + hitinfo &hit = shot->hits.add(); + hit.target = getint(p); + hit.lifesequence = getint(p); + hit.dist = getint(p)/DMF; + hit.rays = getint(p); + loopk(3) hit.dir[k] = getint(p)/DNF; + } + if(cq) + { + cq->addevent(shot); + cq->setpushed(); + } + else delete shot; + break; + } + + case N_EXPLODE: + { + explodeevent *exp = new explodeevent; + int cmillis = getint(p); + exp->millis = cq ? cq->geteventmillis(gamemillis, cmillis) : 0; + exp->gun = getint(p); + exp->id = getint(p); + int hits = getint(p); + loopk(hits) + { + if(p.overread()) break; + hitinfo &hit = exp->hits.add(); + hit.target = getint(p); + hit.lifesequence = getint(p); + hit.dist = getint(p)/DMF; + hit.rays = getint(p); + loopk(3) hit.dir[k] = getint(p)/DNF; + } + if(cq) cq->addevent(exp); + else delete exp; + break; + } + + case N_ITEMPICKUP: + { + int n = getint(p); + if(!cq) break; + pickupevent *pickup = new pickupevent; + pickup->ent = n; + cq->addevent(pickup); + break; + } + + case N_TEXT: + { + QUEUE_AI; + QUEUE_MSG; + getstring(text, p); + filtertext(text, text, true, true); + QUEUE_STR(text); + if(isdedicatedserver() && cq) logoutf("%s: %s", colorname(cq), text); + break; + } + + case N_SAYTEAM: + { + getstring(text, p); + if(!ci || !cq || (ci->state.state==CS_SPECTATOR && !ci->local && !ci->privilege) || !m_teammode || !cq->team[0]) break; + filtertext(text, text, true, true); + loopv(clients) + { + clientinfo *t = clients[i]; + if(t==cq || t->state.state==CS_SPECTATOR || t->state.aitype != AI_NONE || strcmp(cq->team, t->team)) continue; + sendf(t->clientnum, 1, "riis", N_SAYTEAM, cq->clientnum, text); + } + if(isdedicatedserver() && cq) logoutf("%s <%s>: %s", colorname(cq), cq->team, text); + break; + } + + case N_SWITCHNAME: + { + QUEUE_MSG; + getstring(text, p); + filtertext(ci->name, text, false, false, MAXNAMELEN); + if(!ci->name[0]) copystring(ci->name, "Anonymous"); + QUEUE_STR(ci->name); + break; + } + + case N_SWITCHMODEL: + { + ci->playermodel = 0; + QUEUE_MSG; + break; + } + + case N_SWITCHTEAM: + { + getstring(text, p); + filtertext(text, text, false, false, MAXTEAMLEN); + if(m_teammode && text[0] && strcmp(ci->team, text) && addteaminfo(text)) + { + if(ci->state.state==CS_ALIVE) suicide(ci); + copystring(ci->team, text); + aiman::changeteam(ci); + sendf(-1, 1, "riisi", N_SETTEAM, sender, ci->team, ci->state.state==CS_SPECTATOR ? -1 : 0); + } + break; + } + + case N_MAPVOTE: + { + getstring(text, p); + filtertext(text, text, false); + fixmapname(text); + int reqmode = getint(p); + vote(text, reqmode, sender); + break; + } + + case N_ITEMLIST: + { + if((ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || !notgotitems || strcmp(ci->clientmap, smapname)) { while(getint(p)>=0 && !p.overread()) getint(p); break; } + int n; + while((n = getint(p))>=0 && nstate.state==CS_SPECTATOR) break; + QUEUE_MSG; + bool canspawn = canspawnitem(type); + if(istate.state!=CS_SPECTATOR) QUEUE_MSG; + break; + } + + case N_PING: + sendf(sender, 1, "i2", N_PONG, getint(p)); + break; + + case N_CLIENTPING: + { + int ping = getint(p); + if(ci) + { + ci->ping = ping; + loopv(ci->bots) ci->bots[i]->ping = ping; + } + QUEUE_MSG; + break; + } + + case N_MASTERMODE: + { + int mm = getint(p); + if((ci->privilege || ci->local) && mm>=MM_OPEN && mm<=MM_PRIVATE) + { + if((ci->privilege>=PRIV_ADMIN || ci->local) || (mastermask&(1<=MM_PRIVATE) + { + loopv(clients) allowedips.add(getclientip(clients[i]->clientnum)); + } + sendf(-1, 1, "rii", N_MASTERMODE, mastermode); + //sendservmsgf("mastermode is now %s (%d)", mastermodename(mastermode), mastermode); + } + else + { + defformatstring(s, "mastermode %d is disabled on this server", mm); + sendf(sender, 1, "ris", N_SERVMSG, s); + } + } + break; + } + + case N_CLEARBANS: + { + if(ci->privilege || ci->local) + { + bannedips.shrink(0); + sendservmsg("cleared all bans"); + } + break; + } + + case N_KICK: + { + int victim = getint(p); + getstring(text, p); + filtertext(text, text); + trykick(ci, victim, text); + break; + } + + case N_SPECTATOR: + { + int spectator = getint(p), val = getint(p); + if(!ci->privilege && !ci->local && (spectator!=sender || (ci->state.state==CS_SPECTATOR && mastermode>=MM_LOCKED))) break; + clientinfo *spinfo = (clientinfo *)getclientinfo(spectator); // no bots + if(!spinfo || !spinfo->connected || (spinfo->state.state==CS_SPECTATOR ? val : !val)) break; + + if(spinfo->state.state!=CS_SPECTATOR && val) forcespectator(spinfo); + else if(spinfo->state.state==CS_SPECTATOR && !val) unspectate(spinfo); + + if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; + break; + } + + case N_SETTEAM: + { + int who = getint(p); + getstring(text, p); + filtertext(text, text, false, false, MAXTEAMLEN); + if(!ci->privilege && !ci->local) break; + clientinfo *wi = getinfo(who); + if(!m_teammode || !text[0] || !wi || !wi->connected || !strcmp(wi->team, text)) break; + if(addteaminfo(text)) + { + if(wi->state.state==CS_ALIVE) suicide(wi); + copystring(wi->team, text, MAXTEAMLEN+1); + } + aiman::changeteam(wi); + sendf(-1, 1, "riisi", N_SETTEAM, who, wi->team, 1); + break; + } + + case N_FORCEINTERMISSION: + if(ci->local && !hasnonlocalclients()) startintermission(); + break; + + case N_RECORDDEMO: + { + int val = getint(p); + if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + if(!maxdemos || !maxdemosize) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "the server has disabled demo recording"); + break; + } + demonextmatch = val!=0; + sendservmsgf("demo recording is %s for next match", demonextmatch ? "enabled" : "disabled"); + break; + } + + case N_STOPDEMO: + { + if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + stopdemo(); + break; + } + + case N_CLEARDEMOS: + { + int demo = getint(p); + if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + cleardemos(demo); + break; + } + + case N_LISTDEMOS: + if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; + listdemos(sender); + break; + + case N_GETDEMO: + { + int n = getint(p), tag = getint(p); + if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; + senddemo(ci, n, tag); + break; + } + + case N_GETMAP: + if(!mapdata) sendf(sender, 1, "ris", N_SERVMSG, "no map to send"); + else if(ci->getmap) sendf(sender, 1, "ris", N_SERVMSG, "already sending map"); + else + { + sendservmsgf("[%s is getting the map]", colorname(ci)); + if((ci->getmap = sendfile(sender, 2, mapdata, "ri", N_SENDMAP))) + ci->getmap->freeCallback = freegetmap; + ci->needclipboard = totalmillis ? totalmillis : 1; + } + break; + + case N_NEWMAP: + { + int size = getint(p); + if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; + if(size>=0) + { + smapname[0] = '\0'; + resetitems(); + notgotitems = false; + } + QUEUE_MSG; + break; + } + + case N_SETMASTER: + { + int mn = getint(p), val = getint(p); + getstring(text, p); + if(mn != ci->clientnum) + { + if(!ci->privilege && !ci->local) break; + clientinfo *minfo = (clientinfo *)getclientinfo(mn); + if(!minfo || !minfo->connected || (!ci->local && minfo->privilege >= ci->privilege) || (val && minfo->privilege)) break; + setmaster(minfo, val!=0, "", NULL, NULL, PRIV_MASTER, true); + } + else setmaster(ci, val!=0, text); + // don't broadcast the master password + break; + } + + case N_ADDBOT: + { + aiman::reqadd(ci, getint(p)); + break; + } + + case N_DELBOT: + { + aiman::reqdel(ci); + break; + } + + case N_BOTLIMIT: + { + int limit = getint(p); + if(ci) aiman::setbotlimit(ci, limit); + break; + } + + case N_BOTBALANCE: + { + int balance = getint(p); + if(ci) aiman::setbotbalance(ci, balance!=0); + break; + } + + case N_AUTHTRY: + { + string desc, name; + getstring(desc, p, sizeof(desc)); + getstring(name, p, sizeof(name)); + tryauth(ci, name, desc); + break; + } + + case N_AUTHKICK: + { + string desc, name; + getstring(desc, p, sizeof(desc)); + getstring(name, p, sizeof(name)); + int victim = getint(p); + getstring(text, p); + filtertext(text, text); + int authpriv = PRIV_AUTH; + if(desc[0]) + { + userinfo *u = users.access(userkey(name, desc)); + if(u) authpriv = u->privilege; else break; + } + if(ci->local || ci->privilege >= authpriv) trykick(ci, victim, text); + else if(trykick(ci, victim, text, name, desc, authpriv, true) && tryauth(ci, name, desc)) + { + ci->authkickvictim = victim; + ci->authkickreason = newstring(text); + } + break; + } + + case N_AUTHANS: + { + string desc, ans; + getstring(desc, p, sizeof(desc)); + uint id = (uint)getint(p); + getstring(ans, p, sizeof(ans)); + answerchallenge(ci, id, ans, desc); + break; + } + + case N_PAUSEGAME: + { + int val = getint(p); + if(ci->privilege < (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + pausegame(val > 0, ci); + break; + } + + case N_GAMESPEED: + { + int val = getint(p); + if(ci->privilege < (restrictgamespeed ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + changegamespeed(val, ci); + break; + } + + case N_COPY: + ci->cleanclipboard(); + ci->lastclipboard = totalmillis ? totalmillis : 1; + goto genericmsg; + + case N_PASTE: + if(ci->state.state!=CS_SPECTATOR) sendclipboard(ci); + goto genericmsg; + + case N_CLIPBOARD: + { + int unpacklen = getint(p), packlen = getint(p); + ci->cleanclipboard(false); + if(ci->state.state==CS_SPECTATOR) + { + if(packlen > 0) p.subbuf(packlen); + break; + } + if(packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) + { + if(packlen > 0) p.subbuf(packlen); + packlen = unpacklen = 0; + } + packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); + putint(q, N_CLIPBOARD); + putint(q, ci->clientnum); + putint(q, unpacklen); + putint(q, packlen); + if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); + ci->clipboard = q.finalize(); + ci->clipboard->referenceCount++; + break; + } + + case N_EDITT: + case N_REPLACE: + case N_EDITVSLOT: + { + int size = server::msgsizelookup(type); + //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } + loopi(size-1) getint(p); + //~if(p.remaining() < 2) { disconnect_client(sender, DISC_MSGERR); return; } + int extra = lilswap(*(const ushort *)p.pad(2)); + //~if(p.remaining() < extra) { disconnect_client(sender, DISC_MSGERR); return; } + p.pad(extra); + if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG; + break; + } + + case N_UNDO: + case N_REDO: + { + int unpacklen = getint(p), packlen = getint(p); + if(!ci || ci->state.state==CS_SPECTATOR || packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) + { + if(packlen > 0) p.subbuf(packlen); + break; + } + //~if(p.remaining() < packlen) { disconnect_client(sender, DISC_MSGERR); return; } + packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); + putint(q, type); + putint(q, ci->clientnum); + putint(q, unpacklen); + putint(q, packlen); + if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); + sendpacket(-1, 1, q.finalize(), ci->clientnum); + break; + } + + case N_SERVCMD: + getstring(text, p); + break; + + + case -1: + //~disconnect_client(sender, DISC_MSGERR); + return; + + case -2: + //~disconnect_client(sender, DISC_OVERFLOW); + return; + + default: genericmsg: + { + int size = server::msgsizelookup(type); + //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } + loopi(size-1) getint(p); + if(ci) switch(msgfilter[type]) + { + case 2: case 3: if(ci->state.state != CS_SPECTATOR) QUEUE_MSG; break; + default: if(cq && (ci != cq || ci->state.state!=CS_SPECTATOR)) { QUEUE_AI; QUEUE_MSG; } break; + } + break; + } + } + } + + int laninfoport() { return SAUERBRATEN_LANINFO_PORT; } + int serverinfoport(int servport) { return servport < 0 ? SAUERBRATEN_SERVINFO_PORT : servport+1; } + int serverport(int infoport) { return infoport < 0 ? SAUERBRATEN_SERVER_PORT : infoport-1; } + const char *defaultmaster() { return "master.sauerbraten.org"; } + int masterport() { return SAUERBRATEN_MASTER_PORT; } + int numchannels() { return 3; } + + #include "extinfo.h" + + void serverinforeply(ucharbuf &req, ucharbuf &p) + { + if(req.remaining() && !getint(req)) + { + extserverinforeply(req, p); + return; + } + + putint(p, numclients(-1, false, true)); + putint(p, gamepaused || gamespeed != 100 ? 7 : 5); // number of attrs following + putint(p, PROTOCOL_VERSION); // generic attributes, passed back below + putint(p, gamemode); + putint(p, m_timed ? max((gamelimit - gamemillis)/1000, 0) : 0); + putint(p, maxclients); + putint(p, serverpass[0] ? MM_PASSWORD : (!m_mp(gamemode) ? MM_PRIVATE : (mastermode || mastermask&MM_AUTOAPPROVE ? mastermode : MM_AUTH))); + if(gamepaused || gamespeed != 100) + { + putint(p, gamepaused ? 1 : 0); + putint(p, gamespeed); + } + sendstring(smapname, p); + sendstring(serverdesc, p); + sendserverinforeply(p); + } + + #include "aiman.h" } -- cgit v1.2.3