#include "game.hh" namespace game { void parseoptions(vector &args) { loopv(args) #ifndef STANDALONE if(!game::clientoption(args[i])) #endif if(!server::serveroption(args[i])) conoutf(CON_ERROR, "unknown command-line option: %s", args[i]); } const char *gameident() { return "Tesseract"; } } 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, atk; vec from, to; vector hits; void process(clientinfo *ci); }; struct explodeevent : timedevent { int id, atk; 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 servstate : gamestate { vec o; int state, editstate; int lastdeath, deadflush, lastspawn, lifesequence; int lastshot; projectilestate<8> projs; int frags, flags, deaths, teamkills, shotdamage, damage; int lasttimeplayed, timeplayed; float effectiveness; servstate() : 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 = 1; projs.reset(); timeplayed = 0; effectiveness = 0; frags = flags = deaths = teamkills = shotdamage = damage = 0; lastdeath = 0; respawn(); } void respawn() { gamestate::respawn(); o = vec(-1e10f, -1e10f, -1e10f); deadflush = 0; lastspawn = -1; lastshot = 0; } void reassign() { respawn(); projs.reset(); } }; struct savedscore { uint ip; string name; int frags, flags, deaths, teamkills, shotdamage, damage; int timeplayed; float effectiveness; void save(servstate &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(servstate &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, mapvote; int team, playermodel, playercolor; int modevote; int privilege; bool connected, local, timesync; int gameoffset, lastevent, pushed, exceeded; servstate 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 ? peer->roundTripTime + peer->roundTripTimeVariance : 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] = 0; team = 0; playermodel = -1; playercolor = 0; 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, demomillis = 0; VAR(maxdemos, 0, 5, 25); VAR(maxdemosize, 0, 16, 31); VAR(restrictdemos, 0, 1, 1); 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; case 1: mastermask = MM_PUBSERV; break; 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 *modeprettyname(int n, const char *unknown) { if(m_valid(n)) return gamemodes[n - STARTGAMEMODE].prettyname; 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]; } struct servmode { virtual ~servmode() {} virtual void entergame(clientinfo *ci) {} virtual void leavegame(clientinfo *ci, bool disconnecting = false) {} virtual void moved(clientinfo *ci, const vec &oldpos, bool oldclip, const vec &newpos, bool newclip) {} virtual bool canspawn(clientinfo *ci, bool connecting = false) { return true; } virtual void spawned(clientinfo *ci) {} virtual int fragvalue(clientinfo *victim, clientinfo *actor) { if(victim==actor || isteam(victim->team, actor->team)) return -1; return 1; } virtual void died(clientinfo *victim, clientinfo *actor) {} virtual bool canchangeteam(clientinfo *ci, int oldteam, int newteam) { return true; } virtual void changeteam(clientinfo *ci, int oldteam, int newteam) {} virtual void initclient(clientinfo *ci, packetbuf &p, bool connecting) {} virtual void update() {} virtual void cleanup() {} virtual void setup() {} virtual void newmap() {} virtual void intermission() {} virtual bool hidefrags() { return false; } virtual int getteamscore(int team) { return 0; } virtual void getteamscores(vector &scores) {} virtual bool extinfoteam(int team, ucharbuf &p) { return false; } }; #define SERVMODE 1 #include "ctf.hh" ctfservmode ctfmode; servmode *smode = NULL; bool canspawnitem(int type) { return validitem(type); } 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) { } return sec*1000; } bool delayspawn(int type) { switch(type) { 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 || (!ci->local && !ci->state.canpickup(sents[i].type))) 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 teaminfo teaminfos[MAXTEAMS]; void clearteaminfo() { loopi(MAXTEAMS) teaminfos[i].reset(); } 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; } void autoteam() { vector team[MAXTEAMS]; float teamrank[MAXTEAMS] = {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(smode && smode->hidefrags()) rank = 1; else 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(MAXTEAMS) loopvj(team[i]) { clientinfo *ci = team[i][j]; if(ci->team == 1+i) continue; ci->team = 1+i; sendf(-1, 1, "riiii", N_SETTEAM, ci->clientnum, ci->team, -1); } } struct teamrank { float rank; int clients; teamrank() : rank(0), clients(0) {} }; int chooseworstteam(clientinfo *exclude = NULL) { teamrank teamranks[MAXTEAMS]; loopv(clients) { clientinfo *ci = clients[i]; if(ci==exclude || ci->state.aitype!=AI_NONE || ci->state.state==CS_SPECTATOR || !validteam(ci->team)) continue; ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; ci->state.lasttimeplayed = lastmillis; teamrank &ts = teamranks[ci->team-1]; ts.rank += ci->state.effectiveness/max(ci->state.timeplayed, 1); ts.clients++; } teamrank *worst = &teamranks[0]; for(int i = 1; i < MAXTEAMS; i++) { teamrank &ts = teamranks[i]; if(smode && smode->hidefrags()) { if(ts.clients < worst->clients || (ts.clients == worst->clients && ts.rank < worst->rank)) worst = &ts; } else if(ts.rank < worst->rank || (ts.rank == worst->rank && ts.clients < worst->clients)) worst = &ts; } return 1+int(worst-teamranks); } 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, modeprettyname(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) { 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, "rim", N_SENDDEMO, 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]); } void setupdemoplayback() { if(demoplayback) return; demoheader hdr; string msg; msg[0] = '\0'; defformatstring(file, "%s.dmo", smapname); 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 Tesseract", file, hdr.versionread(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) { enddemoplayback(); return; } lilswap(&nextplayback, 1); } void readdemo() { if(!demoplayback) return; demomillis += curtime; while(demomillis>=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 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 %s", cn, sessionid, pwd); 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_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_SENDMAP, N_DROPFLAG, N_SCOREFLAG, N_RETURNFLAG, N_RESETFLAG, N_CLIENT, N_AUTHCHAL, N_INITAI, N_DEMOPACKET, -2, N_CALCLIGHT, 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<40 && !force) return false; bool flush = buildworldstate(); lastsend += curtime - (curtime%40); return flush; } template void sendstate(servstate &gs, T &p) { putint(p, gs.lifesequence); putint(p, gs.health); putint(p, gs.maxhealth); putint(p, gs.gunselect); loopi(NUMGUNS) putint(p, gs.ammo[i]); } void spawnstate(clientinfo *ci) { servstate &gs = ci->state; gs.spawnstate(gamemode); gs.lifesequence = (gs.lifesequence + 1)&0x7F; } void sendspawn(clientinfo *ci) { servstate &gs = ci->state; spawnstate(ci); sendf(ci->ownernum, 1, "rii5v", N_SPAWNSTATE, ci->clientnum, gs.lifesequence, gs.health, gs.maxhealth, gs.gunselect, NUMGUNS, gs.ammo); 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); putint(p, ci->playercolor); putint(p, ci->team); sendstring(ci->name, p); } else { putint(p, N_INITCLIENT); putint(p, ci->clientnum); sendstring(ci->name, p); putint(p, ci->team); putint(p, ci->playermodel); putint(p, ci->playercolor); } } 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); loopi(MAXTEAMS) { teaminfo &t = teaminfos[i]; putint(p, t.frags); } } if(ci) { putint(p, N_SETTEAM); putint(p, ci->clientnum); putint(p, ci->team); putint(p, -1); } if(ci && (m_demo || m_mp(gamemode)) && ci->state.state!=CS_SPECTATOR) { if(smode && !smode->canspawn(ci, true)) { ci->state.state = CS_DEAD; putint(p, N_FORCEDEATH); putint(p, ci->clientnum); sendf(-1, 1, "ri2x", N_FORCEDEATH, ci->clientnum, ci->clientnum); } else { servstate &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); sendstate(oi->state, p); } putint(p, -1); welcomeinitclient(p, ci ? ci->clientnum : -1); } if(smode) smode->initclient(ci, p, true); 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) { servstate &gs = ci->state; sendf(-1, 1, "ri3i7vi", N_RESUME, ci->clientnum, gs.state, gs.frags, gs.flags, gs.deaths, gs.lifesequence, gs.health, gs.maxhealth, gs.gunselect, NUMGUNS, gs.ammo, -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); if(smode) smode->cleanup(); aiman::clearai(); gamemode = mode; gamemillis = 0; gamelimit = (m_overtime ? 15 : 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_ctf) smode = &ctfmode; else smode = NULL; 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) { demonextmatch = false; setupdemorecord(); } if(smode) smode->setup(); } void rotatemap(bool next) { if(!maprotations.inrange(curmaprotation)) { changemap("", 0); 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)) { 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", modeprettyname(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)) { if(demorecord) enddemorecord(); if(!ci->local || hasnonlocalclients()) sendservmsgf("%s forced %s on map %s", colorname(ci), modeprettyname(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), modeprettyname(reqmode), map[0] ? map : "[new map]"); checkvotes(); } } void checkintermission() { if(gamemillis >= gamelimit && !interm) { sendf(-1, 1, "ri2", N_TIMEUP, 0); if(smode) smode->intermission(); changegamespeed(100); interm = gamemillis + 10000; } } void startintermission() { gamelimit = min(gamelimit, gamemillis); checkintermission(); } void dodamage(clientinfo *target, clientinfo *actor, int damage, int atk, const vec &hitpush = vec(0, 0, 0)) { servstate &ts = target->state; ts.dodamage(damage); if(target!=actor && !isteam(target->team, actor->team)) actor->state.damage += damage; sendf(-1, 1, "ri5", N_DAMAGE, target->clientnum, actor->clientnum, damage, 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, atk, damage, v.x, v.y, v.z); target->setpushed(); } if(ts.health<=0) { target->state.deaths++; int fragvalue = smode ? smode->fragvalue(target, actor) : (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(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 && validteam(actor->team) ? &teaminfos[actor->team-1] : 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); if(smode) smode->died(target, actor); ts.state = CS_DEAD; ts.lastdeath = gamemillis; if(actor!=target && m_teammode && 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) { servstate &gs = ci->state; if(gs.state!=CS_ALIVE) return; int fragvalue = smode ? smode->fragvalue(ci, ci) : -1; ci->state.frags += fragvalue; ci->state.deaths++; teaminfo *t = m_teammode && validteam(ci->team) ? &teaminfos[ci->team-1] : 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); if(smode) smode->died(ci, NULL); gs.state = CS_DEAD; gs.lastdeath = gamemillis; gs.respawn(); } void suicideevent::process(clientinfo *ci) { suicide(ci); } void explodeevent::process(clientinfo *ci) { servstate &gs = ci->state; switch(atk) { case ATK_PULSE_SHOOT: if(!gs.projs.remove(id)) return; break; default: return; } sendf(-1, 1, "ri4x", N_EXPLODEFX, ci->clientnum, atk, 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>attacks[atk].exprad) continue; bool dup = false; loopj(i) if(hits[j].target==h.target) { dup = true; break; } if(dup) continue; float damage = attacks[atk].damage*(1-h.dist/EXP_DISTSCALE/attacks[atk].exprad); if(target==ci) damage /= EXP_SELFDAMDIV; if(damage > 0) dodamage(target, ci, max(int(damage), 1), atk, h.dir); } } void shotevent::process(clientinfo *ci) { servstate &gs = ci->state; int wait = millis - gs.lastshot; if(!gs.isalive(gamemillis) || wait attacks[atk].range + 1)) return; gs.ammo[gun] -= attacks[atk].use; gs.lastshot = millis; gs.gunwait = attacks[atk].attackdelay; sendf(-1, 1, "rii9x", N_SHOTFX, ci->clientnum, atk, 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 += attacks[atk].damage*attacks[atk].rays; switch(atk) { case ATK_PULSE_SHOOT: gs.projs.add(id); break; default: { int totalrays = 0, maxrays = attacks[atk].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 > attacks[atk].range + 1) continue; totalrays += h.rays; if(totalrays>maxrays) continue; int damage = h.rays*attacks[atk].damage; dodamage(target, ci, damage, atk, h.dir); } break; } } } void pickupevent::process(clientinfo *ci) { servstate &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]; 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 { sents[i].spawntime -= curtime; if(sents[i].spawntime<=0) { sents[i].spawntime = 0; sents[i].spawned = true; sendf(-1, 1, "ri2", N_ITEMSPAWN, i); } } } aiman::checkai(); if(smode) smode->update(); } } 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); if(smode) smode->leavegame(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, uint ip) { 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); if(smode) smode->leavegame(ci, true); 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; ci->team = m_teammode ? chooseworstteam(ci) : 0; 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, "unnamed"); copystring(ci->name, text, MAXNAMELEN+1); ci->playermodel = getint(p); ci->playercolor = getint(p); 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++]); } if(smode && cp->state.state==CS_ALIVE) smode->moved(cp, cp->state.o, cp->gameclip, pos, (flags&0x80)!=0); 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(smode) { if(val) smode->leavegame(ci); else smode->entergame(ci); } if(val) { ci->state.editstate = ci->state.state; ci->state.state = CS_EDITING; ci->events.setsize(0); ci->state.projs.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 || (smode && !smode->canspawn(cq))) 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 || !validgun(gunselect)) break; cq->state.gunselect = gunselect; 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 || !validgun(gunselect)) break; cq->state.lastspawn = -1; cq->state.state = CS_ALIVE; cq->state.gunselect = gunselect; cq->exceeded = 0; if(smode) smode->spawned(cq); 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->atk = 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->atk = 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 || !validteam(cq->team)) 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 || cq->team != t->team) continue; sendf(t->clientnum, 1, "riis", N_SAYTEAM, cq->clientnum, text); } if(isdedicatedserver() && cq) logoutf("%s <%s>: %s", colorname(cq), teamnames[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, "unnamed"); QUEUE_STR(ci->name); break; } case N_SWITCHMODEL: { ci->playermodel = getint(p); QUEUE_MSG; break; } case N_SWITCHCOLOR: { ci->playercolor = getint(p); QUEUE_MSG; break; } case N_SWITCHTEAM: { int team = getint(p); if(m_teammode && validteam(team) && ci->team != team && (!smode || smode->canchangeteam(ci, ci->team, team))) { if(ci->state.state==CS_ALIVE) suicide(ci); ci->team = team; aiman::changeteam(ci); sendf(-1, 1, "riiii", 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 { sendf(sender, 1, "ris", N_SERVMSG, tempformatstring("mastermode %d is disabled on this server", mm)); } } 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), team = getint(p); if(!ci->privilege && !ci->local) break; clientinfo *wi = getinfo(who); if(!m_teammode || !validteam(team) || !wi || !wi->connected || wi->team == team) break; if(!smode || smode->canchangeteam(wi, wi->team, team)) { if(wi->state.state==CS_ALIVE) suicide(wi); wi->team = team; } aiman::changeteam(wi); sendf(-1, 1, "riiii", 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); if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; senddemo(ci, n); 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; if(smode) smode->newmap(); } 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; #define PARSEMESSAGES 1 #include "ctf.hh" #undef PARSEMESSAGES 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 TESSERACT_LANINFO_PORT; } int serverport() { return TESSERACT_SERVER_PORT; } const char *defaultmaster() { return "master.tesseract.gg"; } int masterport() { return TESSERACT_MASTER_PORT; } int numchannels() { return 3; } #include "extinfo.hh" void serverinforeply(ucharbuf &req, ucharbuf &p) { if(req.remaining() && !getint(req)) { extserverinforeply(req, p); return; } putint(p, PROTOCOL_VERSION); putint(p, numclients(-1, false, true)); putint(p, maxclients); putint(p, gamepaused || gamespeed != 100 ? 5 : 3); // number of attrs following putint(p, gamemode); putint(p, m_timed ? max((gamelimit - gamemillis)/1000, 0) : 0); 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); } int protocolversion() { return PROTOCOL_VERSION; } #include "aiman.hh" }