#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, 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 = 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(); } }; extern int gamemillis, nextexceeded; struct clientinfo { int clientnum, ownernum, connectmillis, sessionid, overflow; string name; int team, playermodel, playercolor; bool connected, local, timesync; int gameoffset, lastevent, pushed, exceeded; servstate state; vector events; vector position, messages; uchar *wsdata; int wslen; int ping, aireinit; string clientmap; int mapcrc; bool warned, gameclip; ENetPacket *getmap, *clipboard; int lastclipboard, needclipboard; clientinfo() : getmap(NULL), clipboard(NULL) { reset(); } ~clientinfo() { events.deletecontents(); cleanclipboard(); } 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() { 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 reset() { name[0] = 0; team = 0; playermodel = -1; playercolor = 0; connected = local = false; position.setsize(0); messages.setsize(0); ping = 0; aireinit = 0; needclipboard = 0; cleanclipboard(); 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; }; #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; 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< ments; vector sents; 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->local))) 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)) return name; static string cname[3]; static int cidx = 0; cidx = (cidx+1)%3; formatstring(cname[cidx], "%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; } }; 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) 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 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.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); } int welcomepacket(packetbuf &p, clientinfo *ci); void sendwelcome(clientinfo *ci); 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() { } 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; } 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_PONG, N_RESUME, N_SENDMAP, N_CLIENT, -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); int checktype(int type, clientinfo *ci) { if(ci) { 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()) 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(); wsbuf.put(wsbuf.buf, wslen); loopv(clients) { clientinfo &ci = *clients[i]; 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(); wsbuf.put(wsbuf.buf, wslen); loopv(clients) { clientinfo &ci = *clients[i]; 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]; addposition(ws, wsbuf, mtu, ci, ci); } sendpositions(ws, wsbuf); loopv(clients) { clientinfo &ci = *clients[i]; addmessages(ws, wsbuf, mtu, ci, 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()) 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) { 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->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); } 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_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; } 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) { pausegame(false); changegamespeed(100); if(smode) smode->cleanup(); gamemode = mode; gamemillis = 0; gamelimit = (m_overtime ? 15 : 10)*60000; interm = 0; nextexceeded = 0; copystring(smapname, s); loaditems(); 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(); 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); } if(smode) smode->setup(); } void forcemap(const char *map, int mode) { changemap(map, mode); } 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; 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_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); } } } 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.checkexceeded()) disconnect_client(c.clientnum, DISC_MSGERR); else c.scheduleexceeded(); } } if(shouldstep && !gamepaused) { if(m_timed && smapname[0] && gamemillis-curtime>0) checkintermission(); if(interm > 0 && gamemillis>interm) { interm = -1; } } 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; 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) 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->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->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; sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 0); if(ci->clientmap[0] || ci->mapcrc) checkmaps(); } void sendservinfo(clientinfo *ci) { sendf(ci->clientnum, 1, "ri4", N_SERVINFO, ci->clientnum, PROTOCOL_VERSION, ci->sessionid); } void noclients() { bannedips.shrink(0); } 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) { 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); if(ci->connected) { if(smode) smode->leavegame(ci, true); ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; sendf(-1, 1, "ri2", N_CDIS, n); clients.removeobj(ci); if(!numclients(-1, false, true)) noclients(); // bans clear when server empties if(ci->local) checkpausegame(); } else connects.removeobj(ci); } int reserveclients() { return 3; } bool allowbroadcast(int n) { clientinfo *ci = getinfo(n); return ci && ci->connected; } void masterconnected() { } void masterdisconnected() { } void processmasterinput(const char *cmd, int cmdlen, const char *args) { } 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->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) { shouldstep = true; connects.removeobj(ci); clients.add(ci); 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); sendinitclient(ci); } 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); connected(ci); 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 || hasnonlocalclients())) while(curmsgmessages.add(p.buf[curmsg++]); } #define QUEUE_BUF(body) { \ if(cm && (!cm->local || 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 || 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_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_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_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) || !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 || 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_EDITENT: { int i = getint(p); loopk(3) getint(p); int type = getint(p); loopk(5) getint(p); if(!ci || ci->state.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; } QUEUE_MSG; break; } case N_FORCEINTERMISSION: if(ci->local && !hasnonlocalclients()) startintermission(); break; case N_NEWMAP: { int size = getint(p); if(!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_PAUSEGAME: { int val = getint(p); pausegame(val > 0, ci); break; } case N_GAMESPEED: { int val = getint(p); 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 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; } void serverinforeply(ucharbuf &req, ucharbuf &p) { 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, MM_PRIVATE); if(gamepaused || gamespeed != 100) { putint(p, gamepaused ? 1 : 0); putint(p, gamespeed); } sendstring(smapname, p); sendserverinforeply(p); } int protocolversion() { return PROTOCOL_VERSION; } }