// server.cpp: little more than enhanced multicaster
// runs dedicated or as client coroutine
#include "cube.h"
#define DEBUGCOND (true)
#include "server.h"
#include "servercontroller.h"
#include "serverfiles.h"
// 2011feb05:ft: quitproc
#include "signal.h"
// config
servercontroller *svcctrl = NULL;
servercommandline scl;
servermaprot maprot;
serveripblacklist ipblacklist;
servernickblacklist nickblacklist;
serverforbiddenlist forbiddenlist;
serverpasswords passwords;
serverinfofile infofiles;
// server state
bool isdedicated = false;
ENetHost *serverhost = NULL;
int nextstatus = 0, servmillis = 0, lastfillup = 0;
vector<client *> clients;
vector<worldstate *> worldstates;
vector<savedscore> savedscores;
vector<savedlimit> savedlimits;
struct steamscore : teamscore
{
bool valid;
steamscore(int team) : teamscore(team), valid(true) { }
};
steamscore steamscores[2] = { steamscore(TEAM_CLA), steamscore(TEAM_RVSF) };
inline steamscore &getsteamscore(int team){ return steamscores[(m_team(gamemode, mutators) && team == 1) ? 1 : 0]; }
inline steamscore &usesteamscore(int team){ getsteamscore(team).valid = false; return getsteamscore(team); }
vector<ban> bans;
vector<demofile> demofiles;
int mastermode = MM_OPEN, botbalance = -1, progressiveround = 1, zombiebalance = 1, zombiesremain = 1;
static bool autoteam = true;
#define autobalance_mode (!m_zombie(gamemode) && !m_convert(gamemode, mutators))
#define autobalance (autoteam && autobalance_mode)
int matchteamsize = 0;
long int incoming_size = 0;
static bool forceintermission = false, nokills = false;
string servdesc_current;
ENetAddress servdesc_caller;
bool custom_servdesc = false;
// current game
string smapname, nextmapname;
int smode = G_DM, nextgamemode, smuts = G_M_TEAM, nextmutators;
int interm = 0;
static int minremain = 0, gamemillis = 0, gamelimit = 0, /*lmsitemtype = 0,*/ nextsendscore = 0;
mapstats smapstats;
vector<entity> sents;
ssqr *maplayout = NULL, *testlayout = NULL;
int maplayout_factor, testlayout_factor, maplayoutssize;
persistent_entity *mapents = NULL;
servermapbuffer mapbuffer;
vector<sconfirm> sconfirms;
int confirmseq = 0;
vector<sknife> sknives;
int knifeseq = 0;
void purgesconfirms()
{
loopv(sconfirms)
sendf(NULL, 1, "ri2", SV_CONFIRMREMOVE, sconfirms[i].id);
}
void purgesknives()
{
loopv(sknives)
sendf(NULL, 1, "ri2", SV_KNIFEREMOVE, sknives[i].id);
sknives.setsize(0);
}
// cmod
int totalclients = 0;
int servertime = 0, serverlagged = 0;
#include "serverworld.h"
bool valid_client(int cn)
{
return clients.inrange(cn) && clients[cn]->type != ST_EMPTY;
}
const char *client::formatname()
{
static string cname[3];
static int idx = 0;
if (idx >= 3) idx %= 3;
if (type == ST_AI) formatstring(cname[idx])("%s [%d-%d]", name, clientnum, ownernum);
else formatstring(cname[idx])("%s (%d)", name, clientnum);
return cname[idx++];
}
const char *client::gethostname()
{
if (ownernum < 0)
return hostname;
return clients[ownernum]->hostname;
}
bool client::hasclient(int cn)
{
if(!valid_client(cn)) return false;
return clientnum == cn || clients[cn]->ownernum == clientnum;
}
void client::removeexplosives()
{
state.grenades.reset(); // remove active/flying nades
state.knives.reset(); // remove active/flying knives (usually useless, since knives are fast)
}
void client::cheat(const char *reason)
{
logline(ACLOG_INFO, "[%s] %s cheat detected (%s)", this->gethostname(), this->formatname(), reason);
defformatstring(cheatstr)("\f2%s \fs\f6(%d) \f3cheat detected \f4(%s)", this->name, this->clientnum, reason);
sendservmsg(cheatstr);
this->suicide(this->type == ST_AI ? OBIT_BOT : OBIT_CHEAT, FRAG_GIB);
}
void clientstate::addwound(int owner, const vec &woundloc)
{
wound &w = wounds.length() >= 8 ? wounds[0] : wounds.add();
w.inflictor = owner;
w.lastdealt = gamemillis;
w.offset = woundloc;
w.offset.sub(o);
}
void cleanworldstate(ENetPacket *packet)
{
loopv(worldstates)
{
worldstate *ws = worldstates[i];
if(ws->positions.inbuf(packet->data) || ws->messages.inbuf(packet->data)) ws->uses--;
else continue;
if(!ws->uses)
{
delete ws;
worldstates.remove(i);
}
break;
}
}
void sendpacket(client *cl, int chan, ENetPacket *packet, int exclude, bool demopacket)
{
// fix exclude
if(valid_client(exclude) && clients[exclude]->type == ST_AI)
exclude = clients[exclude]->ownernum;
if(!cl)
{
// broadcast
recordpacket(chan, packet->data, (int)packet->dataLength);
loopv(clients)
if(i!=exclude && clients[i]->type != ST_EMPTY && clients[i]->type != ST_AI && (clients[i]->type!=ST_TCPIP || clients[i]->isauthed))
sendpacket(clients[i], chan, packet, -1, demopacket);
return;
}
if(cl->type == ST_AI)
{
// reroute packets
if (!valid_client(cl->ownernum) || cl->ownernum == exclude)
return;
cl = clients[cl->ownernum];
//if(cl->type == ST_AI)
// return;
}
switch(cl->type)
{
case ST_TCPIP:
{
enet_peer_send(cl->peer, chan, packet);
break;
}
case ST_LOCAL:
localservertoclient(chan, packet->data, (int)packet->dataLength, demopacket);
break;
}
}
static bool reliablemessages = false;
bool buildworldstate()
{
static struct { int posoff, poslen, msgoff, msglen; } pkt[MAXCLIENTS];
worldstate &ws = *new worldstate;
loopvj(clients)
{
if(clients[j]->type!=ST_TCPIP || !clients[j]->isauthed) continue;
pkt[j].posoff = ws.positions.length();
pkt[j].msgoff = ws.messages.length();
loopv(clients)
{
client &c = *clients[i];
if(i != j && (c.type!=ST_AI || c.ownernum != j)) continue;
c.overflow = 0;
if(!c.position.empty())
{
ws.positions.put(c.position.getbuf(), c.position.length());
c.position.setsize(0);
}
if(!c.messages.empty())
{
putint(ws.messages, SV_CLIENT);
putint(ws.messages, i);
ws.messages.put(c.messages.getbuf(), c.messages.length());
c.messages.setsize(0);
}
}
pkt[j].poslen = ws.positions.length() - pkt[j].posoff;
pkt[j].msglen = ws.messages.length() - pkt[j].msgoff;
}
int psize = ws.positions.length(), msize = ws.messages.length();
if(psize)
{
recordpacket(0, ws.positions.getbuf(), psize);
ucharbuf p = ws.positions.reserve(psize);
p.put(ws.positions.getbuf(), psize);
ws.positions.addbuf(p);
}
if(msize)
{
recordpacket(1, ws.messages.getbuf(), msize);
ucharbuf p = ws.messages.reserve(msize);
p.put(ws.messages.getbuf(), msize);
ws.messages.addbuf(p);
}
ws.uses = 0;
if(psize || msize)
loopv(clients)
{
client &c = *clients[i];
if(c.type!=ST_TCPIP || !c.isauthed) continue;
ENetPacket *packet;
if(psize && psize>pkt[i].poslen)
{
packet = enet_packet_create(&ws.positions[!pkt[i].poslen ? 0 : pkt[i].posoff+pkt[i].poslen],
psize-pkt[i].poslen,
ENET_PACKET_FLAG_NO_ALLOCATE);
sendpacket(&c, 0, packet);
if(!packet->referenceCount) enet_packet_destroy(packet);
else { ++ws.uses; packet->freeCallback = cleanworldstate; }
}
if(msize && msize>pkt[i].msglen)
{
packet = enet_packet_create(&ws.messages[!pkt[i].msglen ? 0 : pkt[i].msgoff+pkt[i].msglen],
msize-pkt[i].msglen,
(reliablemessages ? ENET_PACKET_FLAG_RELIABLE : 0) | ENET_PACKET_FLAG_NO_ALLOCATE);
sendpacket(&c, 1, packet);
if(!packet->referenceCount) enet_packet_destroy(packet);
else { ++ws.uses; packet->freeCallback = cleanworldstate; }
}
}
reliablemessages = false;
if(!ws.uses)
{
delete &ws;
return false;
}
else
{
worldstates.add(&ws);
return true;
}
}
int countclients(int type, bool exclude = false)
{
int num = 0;
loopv(clients) if((clients[i]->type!=type)==exclude) num++;
return num;
}
int numclients() { return countclients(ST_EMPTY, true); }
int numlocalclients() { return countclients(ST_LOCAL); }
int numnonlocalclients() { return countclients(ST_TCPIP); }
int numauthedclients()
{
int num = 0;
loopv(clients) if(clients[i]->type!=ST_EMPTY && clients[i]->isauthed) num++;
return num;
}
int numactiveclients()
{
int num = 0;
loopv(clients) if(clients[i]->type!=ST_EMPTY && clients[i]->isauthed && clients[i]->isonrightmap && team_isactive(clients[i]->team)) num++;
return num;
}
int *numteamclients(int exclude = -1, bool include_bots = false)
{
static int num[TEAM_NUM];
loopi(TEAM_NUM) num[i] = 0;
loopv(clients) if(i != exclude && clients[i]->type!=ST_EMPTY && (include_bots || clients[i]->type!=ST_AI) && clients[i]->isauthed && clients[i]->isonrightmap && team_isvalid(clients[i]->team)) num[clients[i]->team]++;
return num;
}
int sendservermode(bool send = true)
{
int sm = (autoteam & 1) | ((mastermode & MM_MASK) << 2) | (matchteamsize << 4);
if(send) sendf(NULL, 1, "ri2", SV_SERVERMODE, sm);
return sm;
}
void changematchteamsize(int newteamsize)
{
if(newteamsize < 0) return;
if(matchteamsize != newteamsize)
{
matchteamsize = newteamsize;
sendservermode();
}
if(mastermode == MM_MATCH && matchteamsize && m_team(gamemode, mutators))
{
int size[2] = { 0 };
loopv(clients)
{
client &cl = *clients[i];
if (cl.type != ST_EMPTY && cl.isauthed && cl.isonrightmap && team_isactive(cl.team))
{
if (++size[cl.team] > matchteamsize) updateclientteam(cl, team_tospec(cl.team), FTR_SILENT);
}
}
}
}
void changemastermode(int newmode)
{
if(mastermode != newmode)
{
mastermode = newmode;
senddisconnectedscores();
if(mastermode != MM_MATCH)
{
loopv(clients)
{
client &cl = *clients[i];
if (cl.type != ST_EMPTY && cl.isauthed && (cl.team == TEAM_CLA_SPECT || cl.team == TEAM_RVSF_SPECT))
updateclientteam(cl, TEAM_SPECT, FTR_SILENT);
}
}
else if(matchteamsize) changematchteamsize(matchteamsize);
sendservermode();
}
}
savedscore *findscore(client &c, bool insert)
{
if(c.type!=ST_TCPIP) return NULL;
enet_uint32 mask = ENET_HOST_TO_NET_32(mastermode == MM_MATCH ? 0xFFFF0000 : 0xFFFFFFFF); // in match mode, reconnecting from /16 subnet is allowed
if(!insert)
{
loopv(clients)
{
client &o = *clients[i];
if(o.type!=ST_TCPIP || !o.isauthed) continue;
if(o.clientnum!=c.clientnum && o.peer->address.host==c.peer->address.host && !strcmp(o.name, c.name))
{
static savedscore curscore;
curscore.save(o.state, o.team);
return &curscore;
}
}
}
loopv(savedscores)
{
savedscore &sc = savedscores[i];
if(!strcmp(sc.name, c.name) && (sc.ip & mask) == (c.peer->address.host & mask)) return ≻
}
if(!insert) return NULL;
savedscore &sc = savedscores.add();
copystring(sc.name, c.name);
sc.ip = c.peer->address.host;
return ≻
}
bool findlimit(client &c, bool insert)
{
if (c.type != ST_TCPIP) return false;
if (insert)
{
if (savedlimits.length() >= 32) savedlimits.remove(0, 16); // halve the saved limits before it reaches 33
savedlimit &sl = savedlimits.add();
sl.ip = c.peer->address.host;
sl.save(c);
return true;
}
loopv(savedlimits)
{
savedlimit &sl = savedlimits[i];
if (sl.ip == c.peer->address.host)
{
sl.restore(c);
return true;
}
}
return false;
}
void sendf(client *cl, int chan, const char *format, ...)
{
int exclude = -1;
bool reliable = false;
if(*format=='r') { reliable = true; ++format; }
packetbuf p(MAXTRANS, reliable ? ENET_PACKET_FLAG_RELIABLE : 0);
va_list args;
va_start(args, format);
while(*format) switch(*format++)
{
case 'x':
exclude = va_arg(args, int);
break;
case 'v':
{
int n = va_arg(args, int);
int *v = va_arg(args, int *);
loopi(n) putint(p, v[i]);
break;
}
case 'i':
{
int n = isdigit(*format) ? *format++-'0' : 1;
loopi(n) putint(p, va_arg(args, int));
break;
}
case 's': sendstring(va_arg(args, const char *), p); break;
case 'm':
{
int n = va_arg(args, int);
p.put(va_arg(args, uchar *), n);
break;
}
}
va_end(args);
sendpacket(cl, chan, p.finalize(), exclude);
}
void sendservmsg(const char *msg, client *cl)
{
sendf(cl, 1, "ris", SV_SERVMSG, msg);
}
void streakready(client &c, int streak)
{
if (streak < 0 || streak >= STREAK_NUM) return;
if (streak == STREAK_AIRSTRIKE) ++c.state.airstrikes;
sendf(NULL, 1, "ri3", SV_STREAKREADY, c.clientnum, streak);
}
void usestreak(client &c, int streak, client *actor = NULL, const vec *o = NULL)
{
if (streak < 0 || streak >= STREAK_NUM) return;
int info = 0;
switch (streak)
{
case STREAK_AIRSTRIKE:
{
if (!o) break;
sendf(NULL, 1, "ri9", SV_RICOCHET, c.clientnum, GUN_RPG, (int)(c.state.o.x*DMF), (int)(c.state.o.y*DMF), (int)(c.state.o.z*DMF), (int)(o->x*DMF), (int)(o->y*DMF), (int)(o->z*DMF));
sendf(NULL, 1, "ri7", SV_EXPLODE, c.clientnum, GUN_RPG, 0, (int)(o->x*DMF), (int)(o->y*DMF), (int)(o->z*DMF));
int airmillis = gamemillis + 1000;
loopi(5)
{
airmillis += rnd(200) + 50;
vec airo = *o;
airo.add(vec(rnd(7) - 3, rnd(7) - 3, rnd(7) - 3));
extern bool checkpos(vec &p, bool alter = true);
checkpos(airo);
c.addtimer(new airstrikeevent(airmillis, airo));
}
info = --c.state.airstrikes;
break;
}
case STREAK_RADAR:
c.state.radarearned = gamemillis + (info = 15000);
break;
case STREAK_NUKE:
c.state.nukemillis = gamemillis + (info = 30000);
break;
case STREAK_JUG:
info = (c.state.health += min(2000 * HEALTHSCALE, c.state.health * 7 + 200 * HEALTHSCALE));
break;
case STREAK_REVENGE:
c.addtimer(new suicidebomberevent(actor ? actor->clientnum : -1));
// fallthrough
case STREAK_DROPNADE:
info = randomMT();
c.state.grenades.add(info);
break;
}
sendf(NULL, 1, "ri4", SV_STREAKUSE, c.clientnum, streak, info);
}
#define SECURESPAWNDIST 15
int spawncycle = -1;
int fixspawn = 2;
int findspawn(int index)
{
for (int i = index; i<smapstats.hdr.numents; i++) if (mapents[i].type == PLAYERSTART) return i;
loopj(index) if (mapents[j].type == PLAYERSTART) return j;
return -1;
}
int findspawn(int index, uchar attr2)
{
for (int i = index; i<smapstats.hdr.numents; i++) if (mapents[i].type == PLAYERSTART && mapents[i].attr2 == attr2) return i;
loopj(index) if (mapents[j].type == PLAYERSTART && mapents[j].attr2 == attr2) return j;
return -1;
}
// returns -1 for a free place, else dist to the nearest enemy
float nearestenemy(vec place, int team)
{
float nearestenemydist = -1;
loopv(clients)
{
client &other = *clients[i];
if (other.type == ST_EMPTY || other.team == TEAM_SPECT || (m_team(gamemode, mutators) && team == other.team)) continue;
float dist = place.dist(other.state.o);
if (dist < nearestenemydist || nearestenemydist == -1) nearestenemydist = dist;
}
if (nearestenemydist >= SECURESPAWNDIST || nearestenemydist == -1) return -1;
else return nearestenemydist;
}
void sendspawn(client &c)
{
if(team_isspect(c.team)) return;
clientstate &gs = c.state;
if (gs.lastdeath) gs.respawn();
// spawnstate
if(c.type == ST_AI)
{
// random loadout settings
const int weap1[] = {
// insta/sniping
GUN_BOLT,
GUN_SNIPER2,
GUN_SNIPER, // only for sniping
// non-sniping below
GUN_SHOTGUN,
GUN_SUBGUN,
GUN_ASSAULT,
GUN_SWORD,
GUN_ASSAULT2,
GUN_SNIPER3,
//GUN_ASSAULT_PRO,
//GUN_ACR_PRO,
}, weap2[] = {
GUN_PISTOL,
GUN_HEAL,
GUN_RPG,
GUN_PISTOL2,
//GUN_SHOTGUN_PRO,
};
gs.nextprimary = weap1[rnd(m_insta(gamemode, mutators) ? 2 : m_sniper(gamemode, mutators) ? 3 : sizeof(weap1) / sizeof(int))];
gs.nextsecondary = weap2[rnd(sizeof(weap2) / sizeof(int))];
gs.nextperk1 = PERK_NONE;
gs.nextperk2 = (gs.nextprimary == GUN_BOLT || m_sniper(gamemode, mutators)) ? PERK2_STEADY : PERK2_NONE;
}
#if (SERVER_BUILTIN_MOD & 8)
gs.nextprimary = gs.nextsecondary = gungame[gs.gungame];
#endif
gs.spawnstate(c.team, smode, smuts);
gs.lifesequence++;
gs.state = CS_DEAD;
// spawnpos
persistent_entity *spawn_ent = NULL;
int r = fixspawn-->0 ? 4 : rnd(10) + 1;
const int type = m_spawn_team(gamemode, mutators) ? (c.team ^ ((m_spawn_reversals(gamemode, mutators) && gamemillis > gamelimit / 2) ? 1 : 0)) : 100;
if (m_duke(gamemode, mutators) && c.spawnindex >= 0)
{
int x = -1;
loopi(c.spawnindex + 1) x = findspawn(x + 1, type);
if (x >= 0) spawn_ent = &mapents[x];
}
else if (m_team(gamemode, mutators) || m_duke(gamemode, mutators))
{
loopi(r) spawncycle = findspawn(spawncycle + 1, type);
if (spawncycle >= 0) spawn_ent = &mapents[spawncycle];
}
else
{
float bestdist = -1;
loopi(r)
{
spawncycle = !m_spawn_team(gamemode, mutators) && smapstats.spawns[2] > 5 ? findspawn(spawncycle + 1, 100) : findspawn(spawncycle + 1);
if (spawncycle < 0) continue;
float dist = nearestenemy(vec(mapents[spawncycle].x, mapents[spawncycle].y, mapents[spawncycle].z), c.team);
if (!spawn_ent || dist < 0 || (bestdist >= 0 && dist > bestdist)) { spawn_ent = &mapents[spawncycle]; bestdist = dist; }
}
}
if (spawn_ent)
{
gs.o.x = spawn_ent->x;
gs.o.y = spawn_ent->y;
c.y = spawn_ent->attr1; // yaw
}
else
{
// try to spawn in a random place (might be solid)
gs.o.x = rnd((1 << maplayout_factor) - MINBORD) + MINBORD;
gs.o.y = rnd((1 << maplayout_factor) - MINBORD) + MINBORD;
c.y = 0; // yaw
}
extern float getblockfloor(int id, bool check_vdelta = true);
gs.o.z = getblockfloor(getmaplayoutid(gs.o.x, gs.o.y));
extern bool checkpos(vec &p, bool alter = true);
checkpos(gs.o); // fix spawn being stuck
gs.o.z += PLAYERHEIGHT;
checkpos(gs.o); // fix spawn being too high
c.p = 0; // pitch
// send spawn state
sendf(&c, 1, "ri9vvi4", SV_SPAWNSTATE, c.clientnum, gs.lifesequence,
gs.health, gs.armour, gs.perk1, gs.perk2, gs.primary, gs.secondary,
NUMGUNS, gs.ammo, NUMGUNS, gs.mag,
(int)(gs.o.x*DMF), (int)(gs.o.y*DMF), (int)(gs.o.z*DMF), c.y);
gs.lastspawn = gamemillis;
int dstreak = gs.deathstreak + (gs.perk2 == PERK2_STREAK ? 1 : 0);
if (dstreak >= 8) gs.streakondeath = STREAK_REVENGE;
else if (dstreak >= 5 && (c.type != ST_AI || m_progressive(gamemode, mutators))) gs.streakondeath = STREAK_DROPNADE;
else gs.streakondeath = -1;
streakready(c, gs.streakondeath);
}
// demo
stream *demotmp = NULL, *demorecord = NULL, *demoplayback = NULL;
bool recordpackets = false;
int nextplayback = 0;
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);
}
void recordpacket(int chan, void *data, int len)
{
if(recordpackets) writedemo(chan, data, len);
}
void recordpacket(int chan, ENetPacket *packet)
{
if(recordpackets) writedemo(chan, packet->data, (int)packet->dataLength);
}
#ifdef STANDALONE
const char *currentserver(int i)
{
static string curSRVinfo;
string r;
r[0] = '\0';
switch(i)
{
case 1: { copystring(r, scl.ip[0] ? scl.ip : "local"); break; } // IP
case 2: { copystring(r, scl.logident[0] ? scl.logident : "local"); break; } // HOST
case 3: { formatstring(r)("%d", scl.serverport); break; } // PORT
// the following are used by a client, a server will simply return empty strings for them
case 4:
case 5:
case 6:
case 7:
case 8:
{
break;
}
default:
{
formatstring(r)("%s %d", scl.ip[0] ? scl.ip : "local", scl.serverport);
break;
}
}
copystring(curSRVinfo, r);
return curSRVinfo;
}
#endif
// these are actually the values used by the client, the server ones are in "scl".
string demofilenameformat = DEFDEMOFILEFMT;
string demotimestampformat = DEFDEMOTIMEFMT;
int demotimelocal = 0;
#ifdef STANDALONE
#define DEMOFORMAT scl.demofilenameformat
#define DEMOTSFORMAT scl.demotimestampformat
#else
#define DEMOFORMAT demofilenameformat
#define DEMOTSFORMAT demotimestampformat
#endif
const char *getDemoFilename(int gmode, int gmuts, int mplay, int mdrop, int tstamp, char *srvmap)
{
// we use the following internal mapping of formatchars:
// %g : gamemode (int) %G : gamemode (chr) %F : gamemode (full)
// %m : minutes remaining %M : minutes played
// %s : seconds remaining %S : seconds played
// %h : IP of server %H : hostname of server
// %n : mapName
// %w : timestamp "when"
static string dmofn;
copystring(dmofn, "");
int cc = 0;
int mc = strlen(DEMOFORMAT);
while(cc<mc)
{
switch(DEMOFORMAT[cc])
{
case '%':
{
if(cc<(mc-1))
{
string cfspp;
switch(DEMOFORMAT[cc+1])
{
case 'F': formatstring(cfspp)("%s", modestr(gmode, gmuts, false)); break;
case 'g': formatstring(cfspp)("%d-%d", gmode, gmuts); break;
case 'G': formatstring(cfspp)("%s", modestr(gmode, gmuts, true)); break;
case 'h': formatstring(cfspp)("%s", currentserver(1)); break; // client/server have different implementations
case 'H': formatstring(cfspp)("%s", currentserver(2)); break; // client/server have different implementations
case 'm': formatstring(cfspp)("%d", mdrop/60); break;
case 'M': formatstring(cfspp)("%d", mplay/60); break;
case 'n': formatstring(cfspp)("%s", srvmap); break;
case 's': formatstring(cfspp)("%d", mdrop); break;
case 'S': formatstring(cfspp)("%d", mplay); break;
case 'w':
{
time_t t = tstamp;
struct tm * timeinfo;
timeinfo = demotimelocal ? localtime(&t) : gmtime (&t);
strftime(cfspp, sizeof(string) - 1, DEMOTSFORMAT, timeinfo);
break;
}
default: logline(ACLOG_INFO, "bad formatstring: demonameformat @ %d", cc); cc-=1; break; // don't drop the bad char
}
concatstring(dmofn, cfspp);
}
else
{
logline(ACLOG_INFO, "trailing %%-sign in demonameformat");
}
cc+=1;
break;
}
default:
{
defformatstring(fsbuf)("%s%c", dmofn, DEMOFORMAT[cc]);
copystring(dmofn, fsbuf);
break;
}
}
cc+=1;
}
return dmofn;
}
#undef DEMOFORMAT
#undef DEMOTSFORMAT
void enddemorecord()
{
if(!demorecord) return;
delete demorecord;
recordpackets = false;
demorecord = NULL;
if(!demotmp) return;
if(gamemillis < DEMO_MINTIME)
{
delete demotmp;
demotmp = NULL;
logline(ACLOG_INFO, "Demo discarded.");
return;
}
int len = demotmp->size();
demotmp->seek(0, SEEK_SET);
if(demofiles.length() >= scl.maxdemos)
{
delete[] demofiles[0].data;
demofiles.remove(0);
}
int mr = gamemillis >= gamelimit ? 0 : (gamelimit - gamemillis + 60000 - 1)/60000;
demofile &d = demofiles.add();
//2010oct10:ft: suggests : formatstring(d.info)("%s, %s, %.2f%s", modestr(gamemode, mutators), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB"); // the datetime bit is pretty useless in the servmesg, no?!
formatstring(d.info)("%s: %s, %s, %.2f%s", asctime(), modestr(gamemode, mutators), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB");
if(mr) { concatformatstring(d.info, ", %d mr", mr); concatformatstring(d.file, "_%dmr", mr); }
defformatstring(msg)("Demo \"%s\" recorded\nPress F10 to download it from the server..", d.info);
sendservmsg(msg);
logline(ACLOG_INFO, "Demo \"%s\" recorded.", d.info);
// 2011feb05:ft: previously these two static formatstrings were used ..
//formatstring(d.file)("%s_%s_%s", timestring(), behindpath(smapname), modestr(gamemode, mutators, true)); // 20100522_10.08.48_ac_mines_DM.dmo
//formatstring(d.file)("%s_%s_%s", modestr(gamemode, mutators, true), behindpath(smapname), timestring( true, "%Y.%m.%d_%H%M")); // DM_ac_mines.2010.05.22_1008.dmo
// .. now we use client-side parseable fileattribs
int mPLAY = gamemillis >= gamelimit ? gamelimit/1000 : gamemillis/1000;
int mDROP = gamemillis >= gamelimit ? 0 : (gamelimit - gamemillis)/1000;
int iTIME = time(NULL);
const char *mTIME = numtime();
const char *sMAPN = behindpath(smapname);
string iMAPN;
copystring(iMAPN, sMAPN);
formatstring(d.file)( "%d:%d:%d:%s:%s", gamemode, mPLAY, mDROP, mTIME, iMAPN);
d.data = new uchar[len];
d.len = len;
demotmp->read(d.data, len);
delete demotmp;
demotmp = NULL;
if(scl.demopath[0])
{
formatstring(msg)("%s%s.dmo", scl.demopath, getDemoFilename(gamemode, mutators, mPLAY, mDROP, iTIME, iMAPN)); //d.file);
path(msg);
stream *demo = openfile(msg, "wb");
if(demo)
{
int wlen = (int) demo->write(d.data, d.len);
delete demo;
logline(ACLOG_INFO, "demo written to file \"%s\" (%d bytes)", msg, wlen);
}
else
{
logline(ACLOG_INFO, "failed to write demo to file \"%s\"", msg);
}
}
}
void setupdemorecord()
{
if(numlocalclients() || m_edit(gamemode)) return;
defformatstring(demotmppath)("demos/demorecord_%s_%d", scl.ip[0] ? scl.ip : "local", scl.serverport);
demotmp = opentempfile(demotmppath, "w+b");
if(!demotmp) return;
stream *f = opengzfile(NULL, "wb", demotmp);
if(!f)
{
delete demotmp;
demotmp = NULL;
return;
}
sendservmsg("recording demo");
logline(ACLOG_INFO, "Demo recording started.");
demorecord = f;
recordpackets = false;
demoheader hdr;
memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic));
hdr.version = DEMO_VERSION;
hdr.protocol = SERVER_PROTOCOL_VERSION;
lilswap(&hdr.version, 1);
lilswap(&hdr.protocol, 1);
memset(hdr.desc, 0, DHDR_DESCCHARS);
defformatstring(desc)("%s, %s, %s %s", modestr(gamemode, mutators, false), behindpath(smapname), asctime(), servdesc_current);
if(strlen(desc) > DHDR_DESCCHARS)
formatstring(desc)("%s, %s, %s %s", modestr(gamemode, mutators, true), behindpath(smapname), asctime(), servdesc_current);
desc[DHDR_DESCCHARS - 1] = '\0';
strcpy(hdr.desc, desc);
memset(hdr.plist, 0, DHDR_PLISTCHARS);
const char *bl = "";
loopv(clients)
{
client *ci = clients[i];
if(ci->type==ST_EMPTY) continue;
if(strlen(hdr.plist) + strlen(ci->name) < DHDR_PLISTCHARS - 2) { strcat(hdr.plist, bl); strcat(hdr.plist, ci->name); }
bl = " ";
}
demorecord->write(&hdr, sizeof(demoheader));
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
welcomepacket(p, NULL);
writedemo(1, p.buf, p.len);
}
void listdemos(client *cl)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putint(p, SV_LISTDEMOS);
putint(p, demofiles.length());
loopv(demofiles) sendstring(demofiles[i].info, p);
sendpacket(cl, 1, p.finalize());
}
static void cleardemos(int n)
{
if(!n)
{
loopv(demofiles) delete[] demofiles[i].data;
demofiles.shrink(0);
sendservmsg("cleared all demos");
}
else if(demofiles.inrange(n-1))
{
delete[] demofiles[n-1].data;
demofiles.remove(n-1);
defformatstring(msg)("cleared demo %d", n);
sendservmsg(msg);
}
}
bool sending_demo = false;
void senddemo(client &cl, int num)
{
bool is_admin = cl.role >= CR_ADMIN;
if(scl.demo_interm && (!interm || totalclients > 2) && !is_admin)
{
sendservmsg("\f3sorry, but this server only sends demos at intermission.\n wait for the end of this game, please", &cl);
return;
}
if(!num) num = demofiles.length();
if(!demofiles.inrange(num-1))
{
if (demofiles.empty()) sendservmsg("no demos available", &cl);
else
{
defformatstring(msg)("no demo %d available", num);
sendservmsg(msg, &cl);
}
return;
}
demofile &d = demofiles[num-1];
loopv(d.clientssent) if(d.clientssent[i].ip == cl.peer->address.host && d.clientssent[i].clientnum == cl.clientnum)
{
sendservmsg("\f3Sorry, you have already downloaded this demo.", &cl);
return;
}
clientidentity &ci = d.clientssent.add();
ci.ip = cl.peer->address.host;
ci.clientnum = cl.clientnum;
if (interm) sending_demo = true;
packetbuf p(MAXTRANS + d.len, ENET_PACKET_FLAG_RELIABLE);
putint(p, SV_GETDEMO);
sendstring(d.file, p);
putint(p, d.len);
p.put(d.data, d.len);
sendpacket(&cl, 2, p.finalize());
}
int demoprotocol;
bool watchingdemo = false;
void enddemoplayback()
{
if(!demoplayback) return;
delete demoplayback;
demoplayback = NULL;
watchingdemo = false;
loopv(clients) sendf(clients[i], 1, "risi", SV_DEMOPLAYBACK, "", i);
sendservmsg("demo playback finished");
loopv(clients) sendwelcome(*clients[i]);
}
void setupdemoplayback()
{
demoheader hdr;
string msg;
msg[0] = '\0';
defformatstring(file)("demos/%s.dmo", smapname);
path(file);
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, 1);
lilswap(&hdr.protocol, 1);
if(hdr.version!=DEMO_VERSION) formatstring(msg)("demo \"%s\" requires an %s version of AssaultCube", file, hdr.version<DEMO_VERSION ? "older" : "newer");
else if(hdr.protocol != PROTOCOL_VERSION && !(hdr.protocol < 0 && hdr.protocol == -PROTOCOL_VERSION)) formatstring(msg)("demo \"%s\" requires an %s version of AssaultCube", file, hdr.protocol<PROTOCOL_VERSION ? "older" : "newer");
demoprotocol = hdr.protocol;
}
if(msg[0])
{
if(demoplayback) { delete demoplayback; demoplayback = NULL; }
sendservmsg(msg);
return;
}
formatstring(msg)("playing demo \"%s\"", file);
sendservmsg(msg);
sendf(NULL, 1, "risi", SV_DEMOPLAYBACK, smapname, -1);
watchingdemo = true;
if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback))
{
enddemoplayback();
return;
}
lilswap(&nextplayback, 1);
}
void readdemo()
{
if(!demoplayback) return;
while(gamemillis>=nextplayback)
{
int chan, len;
if(demoplayback->read(&chan, sizeof(chan))!=sizeof(chan) ||
demoplayback->read(&len, sizeof(len))!=sizeof(len))
{
enddemoplayback();
return;
}
lilswap(&chan, 1);
lilswap(&len, 1);
ENetPacket *packet = enet_packet_create(NULL, len, 0);
if(!packet || demoplayback->read(packet->data, len)!=len)
{
if(packet) enet_packet_destroy(packet);
enddemoplayback();
return;
}
sendpacket(NULL, chan, packet, -1, true);
if(!packet->referenceCount) enet_packet_destroy(packet);
if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback))
{
enddemoplayback();
return;
}
lilswap(&nextplayback, 1);
}
}
void putflaginfo(packetbuf &p, int flag)
{
sflaginfo &f = sflaginfos[flag];
putint(p, SV_FLAGINFO);
putint(p, flag);
putint(p, f.state);
switch(f.state)
{
case CTFF_STOLEN:
putint(p, f.actor_cn);
break;
case CTFF_DROPPED:
loopi(3) putuint(p, (int)(f.pos[i]*DMF));
break;
}
if (m_overload(gamemode))
{
putint(p, SV_FLAGOVERLOAD);
putint(p, flag);
putint(p, 255 - f.damage / 1000);
}
}
void putsecureflaginfo(ucharbuf &p, ssecure &s)
{
putint(p, SV_FLAGSECURE);
putint(p, s.id);
putint(p, s.team);
putint(p, s.enemy);
putint(p, s.overthrown);
}
#include "serverchecks.h"
void sendflaginfo(int flag = -1, client *cl = NULL)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
if(flag >= 0) putflaginfo(p, flag);
else loopi(2) putflaginfo(p, i);
sendpacket(cl, 1, p.finalize());
}
void sendsecureflaginfo(ssecure *s = NULL, client *cl = NULL)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
if (s) putsecureflaginfo(p, *s);
else loopv(ssecures) putsecureflaginfo(p, ssecures[i]);
sendpacket(cl, 1, p.finalize());
}
void flagmessage(int flag, int message, int actor)
{
if(message == FA_KTFSCORE)
sendf(NULL, 1, "ri5", SV_FLAGMSG, flag, message, actor, (gamemillis - sflaginfos[flag].stolentime) / 1000);
else
sendf(NULL, 1, "ri4", SV_FLAGMSG, flag, message, actor);
}
void flagaction(int flag, int action, int actor)
{
if(!valid_flag(flag)) return;
sflaginfo &f = sflaginfos[flag];
sflaginfo &of = sflaginfos[team_opposite(flag)];
int score = 0;
int message = action;
if (m_capture(gamemode) || m_hunt(gamemode) || m_ktf2(gamemode, mutators) || m_bomber(gamemode) || m_overload(gamemode))
{
switch(action)
{
case FA_PICKUP:
case FA_STEAL:
{
f.state = CTFF_STOLEN;
f.actor_cn = actor;
f.stolentime = gamemillis; // needed for KTF2
break;
}
case FA_LOST:
case FA_DROP:
if (actor == -1) actor = f.actor_cn;
f.state = CTFF_DROPPED;
loopi(3) f.pos[i] = clients[actor]->state.o[i];
//if(f.pos[2] < smapstats.hdr.waterlevel) f.pos[2] = smapstats.hdr.waterlevel; // float to top of water
break;
case FA_RETURN:
f.state = CTFF_INBASE;
break;
case FA_SCORE: // ctf: f = carried by actor flag, htf: f = hunted flag (run over by actor)
if (m_capture(gamemode) || m_overload(gamemode)) score = 1;
else if (m_bomber(gamemode)) score = of.state == CTFF_INBASE ? 3 : of.state == CTFF_DROPPED ? 2 : 1;
else if (m_ktf2(gamemode, mutators))
{
if (valid_client(f.actor_cn) && clients[f.actor_cn]->state.state == CS_ALIVE)
{
actor = f.actor_cn;
score = 1;
message = FA_KTFSCORE;
break; // do not set to INBASE
}
}
else if (m_hunt(gamemode))
{
// strict: must have flag to score
if (!m_gsp1(gamemode, mutators) || of.state == CTFF_STOLEN)
{
if (!m_gsp1(gamemode, mutators))
++score;
if (of.state == CTFF_STOLEN)
{
++score;
if (of.actor_cn == actor) ++score;
}
}
if (!score) message = FA_SCOREFAIL;
}
f.state = CTFF_INBASE;
break;
case FA_RESET:
f.state = CTFF_INBASE;
break;
}
}
else if(m_keep(gamemode)) // f: active flag, of: idle flag
{
switch(action)
{
case FA_PICKUP:
case FA_STEAL:
f.state = CTFF_STOLEN;
f.actor_cn = actor;
f.stolentime = gamemillis;
break;
case FA_SCORE: // f = carried by actor flag
if(valid_client(f.actor_cn) && clients[f.actor_cn]->state.state == CS_ALIVE && !team_isspect(clients[f.actor_cn]->team))
{
actor = f.actor_cn;
score = 1;
message = FA_KTFSCORE;
break;
}
case FA_LOST:
case FA_DROP:
if (actor == -1) actor = f.actor_cn;
// fallthrough
case FA_RESET:
if(f.state == CTFF_STOLEN)
actor = f.actor_cn;
f.state = CTFF_IDLE;
of.state = CTFF_INBASE;
sendflaginfo(team_opposite(flag));
break;
}
}
if (valid_client(actor))
{
client &c = *clients[actor];
if (score)
{
c.state.invalidate().flagscore += score;
usesteamscore(c.team).flagscore += score;
}
usesteamscore(c.team).points += max(0, flagpoints(c, message));
switch (message)
{
case FA_PICKUP:
case FA_STEAL:
logline(ACLOG_INFO, "[%s] %s %s the flag", c.gethostname(), c.formatname(), action == FA_STEAL ? "stole" : "took");
break;
case FA_DROP:
f.drop_cn = actor;
f.dropmillis = servmillis;
// fallthrough
case FA_LOST:
logline(ACLOG_INFO, "[%s] %s %s the flag", c.gethostname(), c.formatname(), message == FA_LOST ? "lost" : "dropped");
break;
case FA_RETURN:
logline(ACLOG_INFO, "[%s] %s returned the flag", c.gethostname(), c.formatname());
break;
case FA_SCORE:
if (m_hunt(gamemode))
logline(ACLOG_INFO, "[%s] %s hunted the flag for %s, new score %d", c.gethostname(), c.formatname(), team_string(c.team), c.state.flagscore);
else
logline(ACLOG_INFO, "[%s] %s scored with the flag for %s, new score %d", c.gethostname(), c.formatname(), team_string(c.team), c.state.flagscore);
break;
case FA_KTFSCORE:
logline(ACLOG_INFO, "[%s] %s scored, carrying for %d seconds, new score %d", c.gethostname(), c.formatname(), (gamemillis - f.stolentime) / 1000, c.state.flagscore);
break;
case FA_SCOREFAIL:
logline(ACLOG_INFO, "[%s] %s failed to score", c.gethostname(), c.formatname());
break;
default:
logline(ACLOG_INFO, "flagaction %d, actor %d, flag %d, message %d", action, actor, flag, message);
break;
}
}
else if (message == FA_RESET) logline(ACLOG_INFO, "the server reset the flag for team %s", team_string(flag));
else logline(ACLOG_INFO, "flagaction %d, actor %d, flag %d, message %d", action, actor, flag, message);
f.lastupdate = gamemillis;
sendflaginfo(flag);
flagmessage(flag, message, actor);
}
int clienthasflag(int cn)
{
if (m_flags(gamemode) && !m_secure(gamemode) && !m_overload(gamemode) && valid_client(cn))
{
loopi(2) { if(sflaginfos[i].state==CTFF_STOLEN && sflaginfos[i].actor_cn==cn) return i; }
}
return -1;
}
void ctfreset()
{
int idleflag = m_keep(gamemode) && !m_ktf2(gamemode, mutators) ? rnd(2) : -1;
loopi(2)
{
sflaginfos[i].actor_cn = -1;
sflaginfos[i].state = i == idleflag ? CTFF_IDLE : CTFF_INBASE;
sflaginfos[i].lastupdate = -1;
}
}
void sdropflag(int cn)
{
int fl = clienthasflag(cn);
if(fl >= 0) flagaction(fl, FA_LOST, cn);
}
void resetflag(int cn)
{
int fl = clienthasflag(cn);
if(fl >= 0) flagaction(fl, FA_RESET, -1);
}
void htf_forceflag(int flag)
{
sflaginfo &f = sflaginfos[flag];
int besthealth = 0;
vector<int> clientnumbers;
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
if(clients[i]->state.state == CS_ALIVE && team_base(clients[i]->team) == flag)
{
if(clients[i]->state.health == besthealth)
clientnumbers.add(i);
else
{
if(clients[i]->state.health > besthealth)
{
besthealth = clients[i]->state.health;
clientnumbers.shrink(0);
clientnumbers.add(i);
}
}
}
}
if(clientnumbers.length())
{
int pick = rnd(clientnumbers.length());
client *cl = clients[clientnumbers[pick]];
f.state = CTFF_STOLEN;
f.actor_cn = cl->clientnum;
sendflaginfo(flag);
flagmessage(flag, FA_PICKUP, cl->clientnum);
logline(ACLOG_INFO, "[%s] %s got forced to pickup the flag", cl->gethostname(), cl->formatname());
}
f.lastupdate = gamemillis;
}
int arenaround = 0, arenaroundstartmillis = 0;
struct twoint { int index, value; };
int cmpscore(const int *a, const int *b) { return clients[*a]->at3_score - clients[*b]->at3_score; }
int cmptwoint(const struct twoint *a, const struct twoint *b) { return a->value - b->value; }
vector<int> tdistrib;
vector<twoint> sdistrib;
void distributeteam(int team)
{
int numsp = team == 100 ? smapstats.spawns[2] : smapstats.spawns[team];
if(!numsp) numsp = 30; // no spawns: try to distribute anyway
twoint ti;
tdistrib.shrink(0);
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
if(team == 100 || team == clients[i]->team)
{
tdistrib.add(i);
clients[i]->at3_score = rnd(0x1000000);
}
}
tdistrib.sort(cmpscore); // random player order
sdistrib.shrink(0);
loopi(numsp)
{
ti.index = i;
ti.value = rnd(0x1000000);
sdistrib.add(ti);
}
sdistrib.sort(cmptwoint); // random spawn order
int x = 0;
loopv(tdistrib)
{
clients[tdistrib[i]]->spawnindex = sdistrib[x++].index;
x %= sdistrib.length();
}
}
void distributespawns()
{
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
clients[i]->spawnindex = -1;
}
if(m_team(gamemode, mutators))
{
distributeteam(0);
distributeteam(1);
}
else
{
distributeteam(100);
}
}
void checkitemspawns(int);
void arenanext(bool forcespawn = true)
{
// start new arena round
arenaround = 0;
arenaroundstartmillis = gamemillis;
distributespawns();
purgesknives();
checkitemspawns(60 * 1000); // the server will respawn all items now
loopi(2) if (sflaginfos[i].state == CTFF_DROPPED || sflaginfos[i].state == CTFF_STOLEN) flagaction(i, FA_RESET, -1);
loopv(clients) if (clients[i]->type != ST_EMPTY && clients[i]->isauthed)
{
// prevent grenades from going into the next round
clients[i]->removeexplosives();
if (clients[i]->isonrightmap && team_isactive(clients[i]->team))
{
clientstate &cs = clients[i]->state;
if (forcespawn || cs.state == CS_DEAD)
{
cs.lastdeath = 1;
sendspawn(*clients[i]);
}
// Refill humans' health/ammo for the next zombie round
else if (m_progressive(gamemode, mutators) && clients[i]->team == TEAM_RVSF)
{
if (cs.canpickup(I_HEALTH, false))
cs.pickup(I_HEALTH);
sendf(NULL, 1, "ri3", SV_REGEN, i, cs.health);
if (cs.canpickup(I_AMMO, false))
cs.pickup(I_AMMO);
sendf(NULL, 1, "ri5", SV_RELOAD, i, cs.primary, cs.mag[cs.primary], cs.ammo[cs.primary]);
}
}
}
nokills = true;
}
void arenacheck()
{
if(!m_duke(gamemode, mutators) || interm || gamemillis<arenaround || !numactiveclients()) return;
if(arenaround)
{
if (m_progressive(gamemode, mutators) && progressiveround <= MAXZOMBIEROUND)
{
defformatstring(zombiemsg)("\f1Wave #\f0%d \f3has started\f2!", progressiveround);
sendservmsg(zombiemsg);
return arenanext(false); // bypass forced spawning
}
return arenanext();
}
client *alive = NULL;
bool dead = false;
int lastdeath = 0;
bool found = false; int ha = 0, hd = 0; // found a match to keep the round / humans alive / humans dead
loopv(clients)
{
client &c = *clients[i];
if(c.type==ST_EMPTY || !c.isauthed || !c.isonrightmap || team_isspect(c.team)) continue;
if (c.state.lastspawn < 0 && c.state.state==CS_DEAD)
{
if (c.type != ST_AI) ++hd;
dead = true;
lastdeath = max(lastdeath, c.state.lastdeath);
}
else if(c.state.state==CS_ALIVE)
{
if (c.type != ST_AI) ++ha;
if(!alive) alive = &c;
else if(!m_team(gamemode, mutators) || alive->team != c.team) found = true;
}
}
if ((found && (ha || !hd)) || !dead || gamemillis < lastdeath + 500) return;
// what happened?
if (m_progressive(gamemode, mutators))
{
const bool humanswin = !alive || alive->team == TEAM_RVSF;
progressiveround += humanswin ? 1 : -1;
if (progressiveround < 1) progressiveround = 1; // epic fail
else if (progressiveround > MAXZOMBIEROUND)
{
sendservmsg("\f0Good work! \f1The zombies have been defeated!");
forceintermission = true;
}
checkai(); // progressive zombies
// convertcheck();
sendf(NULL, 1, "ri2", SV_ZOMBIESWIN, (progressiveround << 1) | (humanswin ? 1 : 0));
loopv(clients) if (clients[i]->type != ST_EMPTY && clients[i]->isauthed && !team_isspect(clients[i]->team))
{
if (clients[i]->team == TEAM_CLA || progressiveround == MAXZOMBIEROUND)
{
// give humans time to prepare, except wave 30
clients[i]->removeexplosives();
if (clients[i]->state.state != CS_DEAD)
forcedeath(*clients[i]);
}
else if (clients[i]->isonrightmap && clients[i]->state.state == CS_DEAD)
{
// early repawn for humans, except wave 30
clients[i]->state.lastdeath = 1;
sendspawn(*clients[i]);
}
int pts = 0, pr = -1;
if ((clients[i]->team == TEAM_CLA) == humanswin)
{
// he died
pts = ARENALOSEPT;
pr = PR_ARENA_LOSE;
}
else
{
// he survives
pts = ARENAWINPT;
pr = PR_ARENA_WIN;
}
addpt(*clients[i], pts, pr);
}
}
else // duke
{
const int cn = !ha && found ? -2 : // bots win
alive ? alive->clientnum : // someone/a team wins
-1 // everyone died
;
// send message
sendf(NULL, 1, "ri2", SV_ARENAWIN, cn);
// award points
loopv(clients) if (clients[i]->type != ST_EMPTY && clients[i]->isauthed && team_isactive(clients[i]->team))
{
int pts = ARENALOSEPT, pr = PR_ARENA_LOSE; // he died with this team, or bots win
if (clients[i]->state.state == CS_ALIVE) // he survives
{
pts = ARENAWINPT;
pr = PR_ARENA_WIN;
}
else if (alive && isteam(alive, clients[i])) // his team wins, but he is dead
{
pts = ARENAWINDPT;
pr = PR_ARENA_WIND;
}
addpt(*clients[i], pts, pr);
}
}
// arena intermission
arenaround = gamemillis+5000;
// check team
if(autobalance && m_team(gamemode, mutators))
refillteams(true);
}
void convertcheck(bool quick)
{
if (!m_convert(gamemode, mutators) || interm || gamemillis < arenaround || !numactiveclients()) return;
if (arenaround)
{
// start new convert round
shuffleteams(FTR_SILENT);
return arenanext(true);
}
if (quick) return;
// check if converted
int bigteam = -1, found = 0;
loopv(clients) if (clients[i]->type != ST_EMPTY && team_isactive(clients[i]->team))
{
if (!team_isvalid(bigteam)) bigteam = clients[i]->team;
if (clients[i]->team == bigteam) ++found;
else return; // nope
}
// game ends if not arena, and all enemies are converted
if (found >= 2)
{
sendf(NULL, 1, "ri", SV_CONVERTWIN);
arenaround = gamemillis + 5000;
}
}
#define SPAMREPEATINTERVAL 20 // detect doubled lines only if interval < 20 seconds
#define SPAMMAXREPEAT 3 // 4th time is SPAM
#define SPAMCHARPERMINUTE 220 // good typist
#define SPAMCHARINTERVAL 30 // allow 20 seconds typing at maxspeed
bool spamdetect(client &cl, char *text) // checks doubled lines and average typing speed
{
if(cl.type != ST_TCPIP || cl.role >= CR_ADMIN) return false;
bool spam = false;
int pause = servmillis - cl.lastsay;
if(pause < 0 || pause > 90*1000) pause = 90*1000;
cl.saychars -= (SPAMCHARPERMINUTE * pause) / (60*1000);
cl.saychars += (int)strlen(text);
if(cl.saychars < 0) cl.saychars = 0;
if(text[0] && !strcmp(text, cl.lastsaytext) && servmillis - cl.lastsay < SPAMREPEATINTERVAL*1000)
{
spam = ++cl.spamcount > SPAMMAXREPEAT;
}
else
{
copystring(cl.lastsaytext, text);
cl.spamcount = 0;
}
cl.lastsay = servmillis;
if(cl.saychars > (SPAMCHARPERMINUTE * SPAMCHARINTERVAL) / 60)
spam = true;
return spam;
}
// chat message distribution matrix:
//
// /------------------------ common chat C c C c c C C c C
// |/----------------------- RVSF chat T
// ||/---------------------- CLA chat T
// |||/--------------------- spect chat t t t T t T
// |||| | | | | | | | | |
// |||| | | | | | | | | | C: normal chat
// |||| team modes: chat goes to | | | | | | | | | T: team chat
// XX --> RVSF players >-----/ | | | | | | | | c: normal chat in all mastermodes except 'match'
// XX X --> RVSF spect players >-------/ | | | | | | | t: all chat in mastermode 'match', otherwise only team chat
// X X --> CLA players >---------/ | | | | | |
// X XX --> CLA spect players >-----------/ | | | | |
// X X --> SPECTATORs >-------------/ | | | |
// XXXX --> SPECTATORs (admin) >---------------/ | | |
// ffa modes: | | |
// X --> any player (ffa mode) >-----------------/ | |
// X X --> any spectator (ffa mode) >-------------------/ |
// X X --> admin spectator >---------------------/
// --> any admin (ACR addition) reads all
// purpose:
// a) give spects a possibility to chat without annoying the players (even in ffa),
// b) no hidden messages from spects to active teams,
// c) no spect talk to players during 'match'
void sendtext(char *text, client &cl, int flags, int voice, int targ)
{
client *target;
if (targ == -1)
target = NULL;
else if (valid_client(targ))
{
target = clients[targ];
if(target == &cl || target->type == ST_AI)
return;
}
else
return;
// voicecom spam filter
if(voice >= S_AFFIRMATIVE && voice <= S_AWESOME2 && servmillis > cl.mute) // valid and client is not muted
{
if ( cl.lastvc + 4000 < servmillis ) { if ( cl.spam > 0 ) cl.spam -= (servmillis - cl.lastvc) / 4000; } // no vc in the last 4 seconds
else cl.spam++; // the guy is spamming
if ( cl.spam < 0 ) cl.spam = 0;
cl.lastvc = servmillis; // register
if ( cl.spam > 4 ) { cl.mute = servmillis + 10000; voice = 0; } // 5 vcs in less than 20 seconds... shut up please
}
else voice = 0;
string logmsg;
if (flags & SAY_ACTION) formatstring(logmsg)("[%s] * %s '%s' ", cl.gethostname(), cl.formatname(), text);
else formatstring(logmsg)("[%s] <%s> '%s' ", cl.gethostname(), cl.formatname(), text);
// Check team flag
if(cl.team == TEAM_VOID || (!m_team(gamemode, mutators) && cl.team != TEAM_SPECT))
flags &= ~SAY_TEAM; // forced common chat
else if(mastermode != MM_MATCH || !matchteamsize || team_isactive(cl.team) || (cl.team == TEAM_SPECT && cl.role >= CR_ADMIN));
else // forced team chat
flags |= SAY_TEAM;
if(flags & SAY_TEAM)
concatformatstring(logmsg, "(%s) ", team_basestring(cl.team));
if(voice)
concatformatstring(logmsg, "[%d] ", voice);
if(cl.type == ST_TCPIP && cl.role < CR_ADMIN)
{
extern int roleconf(int key);
if(const char *forbidden = forbiddenlist.forbidden(text))
{
logline(ACLOG_VERBOSE, "%s, forbidden speech (%s)", logmsg, forbidden);
defformatstring(forbiddenmessage)("\f2forbidden speech: \f3%s \f2was detected", forbidden);
sendservmsg(forbiddenmessage, &cl);
sendf(&cl, 1, "ri4s", SV_TEXT, cl.clientnum, 0, flags | SAY_FORBIDDEN, text);
return;
}
else if(spamdetect(cl, text))
{
logline(ACLOG_VERBOSE, "%s, SPAM detected", logmsg);
sendf(&cl, 1, "ri4s", SV_TEXT, cl.clientnum, 0, flags | SAY_SPAM, text);
return;
}
else if (target && ((mastermode == MM_MATCH && cl.team != target->team) || cl.role < roleconf('t')))
{
logline(ACLOG_VERBOSE, "%s, disallowed", logmsg);
sendf(&cl, 1, "ri4s", SV_TEXT, cl.clientnum, 0, flags | SAY_DISALLOW, text);
return;
}
}
logline(ACLOG_INFO, "%s", logmsg);
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putint(p, SV_TEXT);
putint(p, cl.clientnum);
putint(p, voice);
putint(p, flags);
sendstring(text, p);
ENetPacket *packet = p.finalize();
if (target)
{
sendpacket(target, 1, packet);
sendpacket(&cl, 1, packet);
return;
}
recordpacket(1, packet);
const int &st = cl.team;
loopv(clients)
{
if (clients[i]->type == ST_EMPTY || clients[i]->type == ST_AI)
continue;
const int &rt = clients[i]->team;
if( !(flags & SAY_TEAM) || (i == cl.clientnum) || // common chat or same player
(clients[i]->role >= CR_ADMIN) || // admin reads all
(team_isactive(st) && st == team_group(rt)) || // player to own team + own spects
(team_isspect(st) && team_isspect(rt))) // spectator to other spectators
sendpacket(clients[i], 1, packet);
}
}
int numplayers(bool include_bots = true)
{
// Count every client
if(include_bots)
return numclients();
// Count every client that is not a bot
int count = 0;
loopv(clients)
if(clients[i]->type != ST_EMPTY && clients[i]->type != ST_AI)
++count;
return count;
}
int spawntime(int type)
{
int np = numplayers();
np = np<3 ? 4 : (np>4 ? 2 : 3); // Some spawn times are dependent on the number of players.
int sec = 0;
switch(type)
{
// Please update ./ac_website/htdocs/docs/introduction.html if these times change.
case I_CLIPS:
case I_AMMO: sec = np*2; break;
case I_GRENADE: sec = np + 5; break;
case I_HEALTH: sec = np*5; break;
case I_HELMET:
case I_ARMOUR: sec = 25; break;
case I_AKIMBO: sec = 60; break;
}
return sec*1000;
}
void checkitemspawns(int diff)
{
if(!diff) return;
loopv(sents) if(sents[i].spawntime)
{
sents[i].spawntime -= diff;
if(sents[i].spawntime<=0)
{
sents[i].spawntime = 0;
sents[i].spawned = true;
sendf(NULL, 1, "ri2", SV_ITEMSPAWN, i);
}
}
}
void serverdied(client &target, client &actor_, int damage, int gun, int style, const vec &source, float killdist)
{
client *actor = &actor_;
clientstate &ts = target.state;
const bool suic = (&target == actor);
const bool tk = !suic && isteam(&target, actor);
int targethasflag = clienthasflag(target.clientnum);
ts.damagelog.removeobj(target.clientnum);
// detect assisted suicide
if (suic && ts.damagelog.length() && gun != OBIT_NUKE)
{
loopv(ts.damagelog)
if (valid_client(ts.damagelog[i]) && !isteam(&target, clients[ts.damagelog[i]]))
{
actor = clients[ts.damagelog[i]];
style = isheadshot(gun, style) ? FRAG_GIB : FRAG_NONE;
gun = OBIT_ASSIST;
ts.damagelog.remove(i/*--*/);
break;
}
}
clientstate &as = actor->state;
// only things on target team that changes
if (!m_confirm(gamemode, mutators)) ++usesteamscore(target.team).deaths;
// apply to individual
++ts.invalidate().deaths;
addpt(target, DEATHPT);
const int kills = (suic || tk) ? -1 : ((style & FRAG_GIB) ? 2 : 1);
as.invalidate().frags += kills;
if (!suic)
{
// revenge
if (as.revengelog.find(target.clientnum) >= 0)
{
style |= FRAG_REVENGE;
as.revengelog.removeobj(target.clientnum);
}
ts.revengelog.add(actor->clientnum);
// first blood (not for AI)
if (actor->type != ST_AI && nokills)
{
style |= FRAG_FIRST;
nokills = false;
}
// type of scoping
const int zoomtime = ADSTIME(as.perk2 == PERK_TIME), scopeelapsed = gamemillis - as.scopemillis;
if (as.scoping)
{
// quick/recent/full
if (scopeelapsed >= zoomtime)
{
style |= FRAG_SCOPE_FULL;
if (scopeelapsed < zoomtime + 750)
style |= FRAG_SCOPE_NONE; // recent, not hard
}
}
else
{
// no/quick
if (scopeelapsed >= zoomtime) style |= FRAG_SCOPE_NONE;
}
// buzzkill check
bool buzzkilled = false;
int kstreak_next = ts.pointstreak / 5 + 1;
const int kstreaks_checked[] = { 7, 9, 11, 17 }; // it goes forever, but we only check the first juggernaut
loopi(sizeof(kstreaks_checked) / sizeof(*kstreaks_checked))
if (ts.streakused < kstreaks_checked[i] * 5 && kstreaks_checked[i] == kstreak_next)
{
buzzkilled = true;
break;
}
if (buzzkilled)
{
addptreason(*actor, PR_BUZZKILL);
addptreason(target, PR_BUZZKILLED);
}
// streak
if (gun != OBIT_NUKE)
{
int pointstreak_gain = 5;
if(gun == GUN_ACR_PRO)
pointstreak_gain = 17; // 3.4 kills
else if (gun == GUN_ASSAULT_PRO || gun == GUN_SHOTGUN_PRO)
pointstreak_gain = 8; // 1.6 kills
as.pointstreak += pointstreak_gain;
}
++ts.deathstreak;
actor->state.deathstreak = ts.streakused = 0;
// teamkilling a flag carrier is bad
if ((m_hunt(gamemode) || m_keep(gamemode)) && tk && targethasflag >= 0)
--as.flagscore;
}
ts.pointstreak = 0;
ts.wounds.shrink(0);
ts.damagelog.removeobj(actor->clientnum);
target.invalidateheals();
// assists
loopv(ts.damagelog)
{
if (valid_client(ts.damagelog[i]))
{
const int factor = isteam(clients[ts.damagelog[i]], &target) ? -1 : 1;
clientstate &cs = clients[ts.damagelog[i]]->state;
cs.invalidate();
cs.assists += factor;
if (factor > 0)
usesteamscore(actor->team).assists += factor; // add to assists
cs.pointstreak += factor * 2;
}
else ts.damagelog.remove(i--);
}
// killstreak rewards
int effectivestreak = actor->state.pointstreak + (actor->state.perk2 == PERK2_STREAK ? 5 : 0);
#define checkstreak(n) if(effectivestreak >= n * 5 && actor->state.streakused < n * 5)
checkstreak(7) streakready(*actor, STREAK_AIRSTRIKE);
checkstreak(9) usestreak(*actor, STREAK_RADAR);
checkstreak(11) usestreak(*actor, STREAK_NUKE);
int jugcheck = max(0, (actor->state.streakused / 5 - 17) / 5);
// 17, 22, 27, 32, 37 ...
if (gun != OBIT_NUKE)
while (effectivestreak >= (17 + jugcheck * 5) * 5)
{
if (actor->state.streakused < (17 + jugcheck * 5) * 5) usestreak(*actor, STREAK_JUG);
++jugcheck;
}
#undef checkstreak
// restart streak
// actor->state.pointstreak %= 11 * 5;
actor->state.streakused = effectivestreak;
// combo reset check
if (gamemillis >= as.lastkill + COMBOTIME) as.combo = 0;
as.lastkill = gamemillis;
// team points
int earnedpts = killpoints(target, *actor, gun, style);
if (m_confirm(gamemode, mutators))
{
// create confirm object if necessary
if (earnedpts > 0 || kills > 0)
{
sconfirm &c = sconfirms.add();
c.o = ts.o;
sendf(NULL, 1, "ri6", SV_CONFIRMADD, c.id = ++confirmseq, c.team = actor->team, (int)(c.o.x*DMF), (int)(c.o.y*DMF), (int)(c.o.z*DMF));
c.actor = actor->clientnum;
c.target = target.clientnum;
c.points = max(0, earnedpts);
c.frag = max(0, kills);
c.death = target.team;
}
}
else
{
if (earnedpts > 0) usesteamscore(actor->team).points += earnedpts;
if (kills > 0) usesteamscore(actor->team).frags += kills;
}
// pro kills
if(gun == GUN_ACR_PRO || gun == GUN_ASSAULT_PRO || gun == GUN_SHOTGUN_PRO)
{
addptreason(*actor, PR_PRO_KILL);
addptreason(target, PR_PRO_DEATH);
}
// automatic zombie count
if (m_zombie(gamemode) && !m_progressive(gamemode, mutators) && (botbalance == 0 || botbalance == -1) && target.team != actor->team)
{
if (target.team == TEAM_CLA)
{
// zombie was killed
--zombiesremain;
if (zombiesremain <= 0)
{
zombiesremain = (++zombiebalance + 1) >> 1;
checkai();
}
}
// human died
else if (++zombiesremain > zombiebalance)
zombiesremain = zombiebalance;
}
// send message
sendf(NULL, 1, "ri9i3", SV_KILL, target.clientnum, actor->clientnum, gun, style, damage, ++as.combo, ts.damagelog.length(),
(int)(source.x*DMF), (int)(source.y*DMF), (int)(source.z*DMF), (int)(killdist*DMF));
target.position.setsize(0);
ts.state = CS_DEAD;
ts.lastdeath = gamemillis;
ts.lastspawn = -1;
// don't issue respawn yet until DEATHMILLIS has elapsed
// ts.respawn();
// log message
const int logtype = actor->type == ST_AI && target.type == ST_AI ? ACLOG_VERBOSE : ACLOG_INFO;
if (suic)
logline(logtype, "[%s] %s [%s] (%.2f m)", actor->gethostname(), actor->formatname(), suicname(gun), killdist / CUBES_PER_METER);
else
logline(logtype, "[%s] %s [%s] %s (%.2f m)", actor->gethostname(), actor->formatname(), killname(gun, style), target.formatname(), killdist / CUBES_PER_METER);
// drop flags
if (targethasflag >= 0 && m_flags(gamemode) && !m_secure(gamemode) && !m_overload(gamemode))
{
if (m_ktf2(gamemode, mutators) && // KTF2 only
sflaginfos[team_opposite(targethasflag)].state != CTFF_INBASE) // other flag is not in base
{
if (sflaginfos[0].actor_cn == sflaginfos[1].actor_cn) // he has both
{
// reset the far one
const int farflag = ts.o.distxy(vec(sflaginfos[0].x, sflaginfos[0].y, 0)) > ts.o.distxy(vec(sflaginfos[1].x, sflaginfos[1].y, 0)) ? 0 : 1;
flagaction(farflag, FA_RESET, -1);
// drop the close one
targethasflag = team_opposite(farflag);
}
else // he only has this one
{
// reset this one
flagaction(targethasflag, FA_RESET, -1);
targethasflag = -1;
}
}
// drop all flags
while (targethasflag >= 0)
{
flagaction(targethasflag, (tk && m_capture(gamemode)) ? FA_RESET : FA_LOST, -1);
targethasflag = clienthasflag(target.clientnum);
}
}
// target streaks
if (ts.nukemillis)
{
// nuke cancelled!
ts.nukemillis = 0;
sendf(NULL, 1, "ri4", SV_STREAKUSE, target.clientnum, STREAK_NUKE, -2);
}
// deathstreaks MUST be processed after setting to CS_DEAD
if ((explosive_weap(gun) || isheadshot(gun, style)) && ts.streakondeath == STREAK_REVENGE)
ts.streakondeath = STREAK_DROPNADE;
usestreak(target, ts.streakondeath, m_zombie(gamemode) ? actor : NULL);
if (!suic)
{
#if (SERVER_BUILTIN_MOD & 8)
// gungame advance
if (gun != OBIT_NUKE)
{
// gungame maxed out
if (++actor->state.gungame >= GUNGAME_MAX)
{
actor->state.nukemillis = gamemillis; // deploy a nuke
actor->state.gungame = 0; // restart gungame
}
const int newprimary = actor->state.primary = actor->state.secondary = actor->state.gunselect = gungame[actor->state.gungame];
sendf(NULL, 1, "ri5", SV_RELOAD, actor->clientnum, newprimary, actor->state.mag[newprimary] = magsize(newprimary), actor->state.ammo[newprimary] = (ammostats[newprimary].start - 1));
sendf(NULL, 1, "ri3", SV_WEAPCHANGE, actor->clientnum, newprimary);
}
#endif
// conversions
if (m_convert(gamemode, mutators) && target.team != actor->team)
{
updateclientteam(target, actor->team, FTR_SILENT);
// checkai(); // DO NOT balance bots here
convertcheck(true);
}
}
}
void client::suicide(int gun, int style)
{
if (state.state != CS_DEAD)
serverdied(*this, *this, 0, gun, style, state.o);
}
void serverdamage(client &target_, client &actor, int damage, int gun, int style, const vec &source, float dist)
{
// moon jump mario = no damage during gib
#if (SERVER_BUILTIN_MOD & 4)
#if !(SERVER_BUILTIN_MOD & 2)
if (m_gib(gamemode, mutators))
#endif
return;
#endif
if (!damage) return;
client *target = &target_;
if (target != &actor)
{
if (isteam(&actor, target))
{
// for hardcore modes only
if (actor.state.protect(gamemillis, gamemode, mutators))
return;
damage /= 2;
target = &actor;
}
else if (m_vampire(gamemode, mutators) && actor.state.health < VAMPIREMAX)
{
int hpadd = damage / (rnd(3) + 3);
// cap at 300 HP
if (actor.state.health + hpadd > VAMPIREMAX)
hpadd = VAMPIREMAX - actor.state.health;
sendf(NULL, 1, "ri3", SV_REGEN, actor.clientnum, actor.state.health += hpadd);
}
}
clientstate &ts = target->state;
if (ts.state != CS_ALIVE) return;
// damage changes
if (gun == GUN_ASSAULT_PRO || gun == GUN_SHOTGUN_PRO || gun == GUN_ACR_PRO)
{
if (m_classic(gamemode, mutators) && gun != GUN_ACR_PRO)
damage /= 2;
}
else
{
if (m_expert(gamemode, mutators))
{
if (gun == GUN_RPG)
damage /= ((style & (FRAG_GIB | FRAG_FLAG)) == (FRAG_GIB | FRAG_FLAG)) ? 1 : 3;
else if ((gun == GUN_GRENADE && (style & FRAG_FLAG)) || (style & FRAG_GIB) || melee_weap(gun))
damage *= 2;
else if (gun == GUN_GRENADE) damage /= 2;
else damage /= 8;
}
else if (m_real(gamemode, mutators))
{
if (gun == GUN_HEAL && target == &actor) damage /= 2;
else damage *= 2;
}
else if (m_classic(gamemode, mutators)) damage /= 2;
}
ts.dodamage(damage, gun, actor.state.perk1 == PERK_POWER);
ts.lastregen = gamemillis + REGENDELAY - REGENINT;
//ts.allowspeeding(gamemillis, 2000);
if (ts.health <= 0)
serverdied(*target, actor, damage, gun, style, source, dist);
else
{
if (ts.damagelog.find(actor.clientnum) < 0)
ts.damagelog.add(actor.clientnum);
sendf(NULL, 1, "ri8i3", SV_DAMAGE, target->clientnum, actor.clientnum, damage, ts.armour, ts.health, gun, style,
(int)(source.x*DMF), (int)(source.y*DMF), (int)(source.z*DMF));
}
}
#include "serverevents.h"
bool updatedescallowed(void) { return scl.servdesc_pre[0] || scl.servdesc_suf[0]; }
void updatesdesc(const char *newdesc, ENetAddress *caller = NULL)
{
if(!newdesc || !newdesc[0] || !updatedescallowed())
{
copystring(servdesc_current, scl.servdesc_full);
custom_servdesc = false;
}
else
{
formatstring(servdesc_current)("%s%s%s", scl.servdesc_pre, newdesc, scl.servdesc_suf);
custom_servdesc = true;
if(caller) servdesc_caller = *caller;
}
}
inline bool canspawn(client &c, bool connecting)
{
if (!maplayout)
return false;
if (c.team == TEAM_SPECT || (team_isspect(c.team) && !m_team(gamemode, mutators)))
return false; // SP_SPECT
if (m_duke(gamemode, mutators))
return (connecting && totalclients <= 2) || (arenaround && m_zombie(gamemode) && c.team == TEAM_RVSF && progressiveround != MAXZOMBIEROUND);
return true;
}
int chooseteam(client &cl, int suggest = -1)
{
// zombies override
if (m_zombie(gamemode) && !m_convert(gamemode, mutators))
return cl.type == ST_AI ? TEAM_CLA : TEAM_RVSF;
// match base team
if(mastermode == MM_MATCH && cl.team < TEAM_SPECT)
return team_base(cl.team);
// team sizes
int *teamsizes = numteamclients(cl.clientnum, cl.type == ST_AI);
if (botbalance < -1)
{
const int target_CLA = botbalance / -100;
const int target_RVSF = -botbalance % 100;
if (teamsizes[TEAM_CLA] < target_CLA || teamsizes[TEAM_RVSF] < target_RVSF)
return teamsizes[TEAM_CLA] * target_RVSF < teamsizes[TEAM_RVSF] * target_CLA ? TEAM_CLA : TEAM_RVSF;
return TEAM_SPECT;
}
else if(autobalance && teamsizes[TEAM_CLA] != teamsizes[TEAM_RVSF])
return teamsizes[TEAM_CLA] < teamsizes[TEAM_RVSF] ? TEAM_CLA : TEAM_RVSF;
else
{ // join weaker team
int teamscore[2] = {0, 0}, sum = calcscores();
loopv(clients) if(clients[i]->type!=ST_EMPTY && i != cl.clientnum && clients[i]->isauthed && clients[i]->team != TEAM_SPECT)
{
teamscore[team_base(clients[i]->team)] += clients[i]->at3_score;
}
if (sum > 200 && teamscore[TEAM_CLA] != teamscore[TEAM_RVSF])
return teamscore[TEAM_CLA] < teamscore[TEAM_RVSF] ? TEAM_CLA : TEAM_RVSF;
return team_isvalid(suggest) ? suggest : rnd(2);
}
}
bool updateclientteam(client &cl, int newteam, int ftr)
{
if (!team_isvalid(newteam)) return false;
// zombies override
if (m_zombie(gamemode) && !m_convert(gamemode, mutators) && newteam != TEAM_SPECT) newteam = cl.type == ST_AI ? TEAM_CLA : TEAM_RVSF;
if (cl.team == newteam)
{
// only allow to notify
if (ftr != FTR_AUTO) return false;
}
else cl.removeexplosives(); // no nade switch
// if (cl.team == TEAM_SPECT) cl.state.lastdeath = gamemillis;
// log message
logline(ftr == FTR_SILENT ? ACLOG_DEBUG : ACLOG_INFO, "[%s] %s is now on team %s", cl.gethostname(), cl.formatname(), team_string(newteam));
// send message
sendf(NULL, 1, "ri3", SV_SETTEAM, cl.clientnum, (cl.team = newteam) | (ftr << 4));
// force a death if necessary
if (cl.state.state != CS_DEAD && (m_team(gamemode, mutators) || newteam == TEAM_SPECT))
{
if (ftr == FTR_PLAYERWISH) cl.suicide(team_isspect(newteam) ? OBIT_SPECT : OBIT_TEAM, FRAG_NONE);
else forcedeath(cl);
}
return true;
}
int calcscores() // skill eval
{
int sum = 0;
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
clientstate &cs = clients[i]->state;
sum += clients[i]->at3_score = cs.points > 0 ? ufSqrt((float)cs.points) : -ufSqrt((float)-cs.points);
}
return sum;
}
vector<int> shuffle;
void shuffleteams(int ftr)
{
int numplayers = numclients();
int team, sums = calcscores();
if(gamemillis < 2 * 60 *1000)
{ // random
int teamsize[2] = {0, 0};
loopv(clients) if(clients[i]->type!=ST_EMPTY && clients[i]->isonrightmap && !team_isspect(clients[i]->team)) // only shuffle active players
{
sums += rnd(1000);
team = sums & 1;
if(teamsize[team] >= numplayers/2) team = team_opposite(team);
updateclientteam(*clients[i], team, ftr);
teamsize[team]++;
sums >>= 1;
}
}
else
{ // skill sorted
shuffle.shrink(0);
sums /= 4 * numplayers + 2;
team = rnd(2);
loopv(clients) if(clients[i]->type!=ST_EMPTY && clients[i]->isonrightmap && !team_isspect(clients[i]->team))
{
clients[i]->at3_score += rnd(sums | 1);
shuffle.add(i);
}
shuffle.sort(cmpscore);
loopi(shuffle.length())
{
updateclientteam(*clients[shuffle[i]], team, ftr);
team = !team;
}
}
checkai(); // end of shuffle
// convertcheck();
}
bool balanceteams(int ftr) // pro vs noobs never more
{
if(mastermode != MM_OPEN || totalclients < 3 ) return true;
int tsize[2] = {0, 0}, tscore[2] = {0, 0};
int totalscore = 0, nplayers = 0;
int flagmult = (m_capture(gamemode) ? 50 : (m_hunt(gamemode) ? 25 : 12));
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
client *c = clients[i];
if(c->isauthed && team_isactive(c->team))
{
int time = servmillis - c->connectmillis + 5000;
if ( time > gamemillis ) time = gamemillis + 5000;
tsize[c->team]++;
// effective score per minute, thanks to wtfthisgame for the nice idea
// in a normal game, normal players will do 500 points in 10 minutes
c->eff_score = c->state.points * 60 * 1000 / time + c->state.points / 6 + c->state.flagscore * flagmult;
tscore[c->team] += c->eff_score;
nplayers++;
totalscore += c->state.points;
}
}
int h = 0, l = 1;
if ( tscore[1] > tscore[0] ) { h = 1; l = 0; }
if ( 2 * tscore[h] < 3 * tscore[l] || totalscore < nplayers * 100 ) return true;
if ( tscore[h] > 3 * tscore[l] && tscore[h] > 150 * nplayers )
{
shuffleteams();
return true;
}
float diffscore = tscore[h] - tscore[l];
int besth = 0, hid = -1;
int bestdiff = 0;
client *bestpair[2] = { NULL, NULL };
if ( tsize[h] - tsize[l] > 0 ) // the h team has more players, so we will force only one player
{
loopv(clients) if( clients[i]->type!=ST_EMPTY )
{
client *c = clients[i]; // loop for h
// client from the h team, not forced in this game, and without the flag
if( c->isauthed && c->team == h && !c->state.forced && clienthasflag(i) < 0 )
{
// do not exchange in the way that weaker team becomes the stronger or the change is less than 20% effective
if ( 2 * c->eff_score <= diffscore && 10 * c->eff_score >= diffscore && c->eff_score > besth )
{
besth = c->eff_score;
hid = i;
}
}
}
if ( hid >= 0 )
{
updateclientteam(*clients[hid], l, ftr);
// checkai(); // balance big to small
// convertcheck();
clients[hid]->at3_lastforce = gamemillis;
clients[hid]->state.forced = true;
return true;
}
} else { // the h score team has less or the same player number, so, lets exchange
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
client &ci = *clients[i]; // loop for h
if( ci.isauthed && ci.team == h && !ci.state.forced && clienthasflag(i) < 0 )
{
loopvj(clients) if(clients[j]->type!=ST_EMPTY && j != i )
{
client &cj = *clients[j]; // loop for l
if( cj.isauthed && cj.team == l && !cj.state.forced && clienthasflag(j) < 0 )
{
int pairdiff = 2 * (ci.eff_score - cj.eff_score);
if ( pairdiff <= diffscore && 5 * pairdiff >= diffscore && pairdiff > bestdiff )
{
bestdiff = pairdiff;
bestpair[h] = &ci;
bestpair[l] = &cj;
}
}
}
}
}
if ( bestpair[h] && bestpair[l] )
{
updateclientteam(*bestpair[h], l, ftr);
updateclientteam(*bestpair[l], h, ftr);
bestpair[h]->at3_lastforce = bestpair[l]->at3_lastforce = gamemillis;
bestpair[h]->state.forced = bestpair[l]->state.forced = true;
checkai(); // balance switch
// convertcheck();
return true;
}
}
return false;
}
int lastbalance = 0, waitbalance = 2 * 60 * 1000;
bool refillteams(bool now, int ftr) // force only minimal amounts of players
{
if (m_zombie(gamemode) && !m_convert(gamemode, mutators))
{
// force to zombie teams
loopv(clients)
if (clients[i]->type != ST_EMPTY && !team_isspect(clients[i]->team))
updateclientteam(*clients[i], clients[i]->type == ST_AI ? TEAM_CLA : TEAM_RVSF, ftr);
return false;
}
if(mastermode == MM_MATCH) return false;
static int lasttime_eventeams = 0;
int teamsize[2] = {0, 0}, teamscore[2] = {0, 0}, moveable[2] = {0, 0};
bool switched = false;
calcscores();
loopv(clients) if(clients[i]->type!=ST_EMPTY) // playerlist stocktaking
{
client *c = clients[i];
c->at3_dontmove = true;
if(c->isauthed)
{
if(team_isactive(c->team)) // only active players count
{
teamsize[c->team]++;
teamscore[c->team] += c->at3_score;
if(clienthasflag(i) < 0)
{
c->at3_dontmove = false;
moveable[c->team]++;
}
}
}
}
int bigteam = teamsize[1] > teamsize[0];
int allplayers = teamsize[0] + teamsize[1];
int diffnum = teamsize[bigteam] - teamsize[!bigteam];
int diffscore = teamscore[bigteam] - teamscore[!bigteam];
if(lasttime_eventeams > gamemillis) lasttime_eventeams = 0;
if(diffnum > 1)
{
if(now || gamemillis - lasttime_eventeams > 8000 + allplayers * 1000 || diffnum > 2 + allplayers / 10)
{
// time to even out teams
loopv(clients) if(clients[i]->type!=ST_EMPTY && clients[i]->team != bigteam) clients[i]->at3_dontmove = true; // dont move small team players
while(diffnum > 1 && moveable[bigteam] > 0)
{
// pick best fitting cn
client *pick = NULL;
int bestfit = 1000000000;
int targetscore = diffscore / (diffnum & ~1);
loopv(clients) if(clients[i]->type!=ST_EMPTY && !clients[i]->at3_dontmove) // try all still movable players
{
int fit = targetscore - clients[i]->at3_score;
if(fit < 0 ) fit = -(fit * 15) / 10; // avoid too good players
int forcedelay = clients[i]->at3_lastforce ? (1000 - (gamemillis - clients[i]->at3_lastforce) / (5 * 60)) : 0;
if(forcedelay > 0) fit += (fit * forcedelay) / 600; // avoid lately forced players
if(fit < bestfit + fit * rnd(100) / 400) // search 'almost' best fit
{
bestfit = fit;
pick = clients[i];
}
}
if(!pick) break; // should really never happen
// move picked player
pick->at3_dontmove = true;
moveable[bigteam]--;
if(updateclientteam(*pick, !bigteam, ftr))
{
diffnum -= 2;
diffscore -= 2 * pick->at3_score;
pick->at3_lastforce = gamemillis; // try not to force this player again for the next 5 minutes
switched = true;
// checkai(); // refill
// convertcheck();
}
}
}
}
if(diffnum < 2)
{
if ( ( gamemillis - lastbalance ) > waitbalance && ( gamelimit - gamemillis ) > 4*60*1000 )
{
if ( balanceteams (ftr) )
{
waitbalance = 2 * 60 * 1000 + gamemillis / 3;
switched = true;
}
else waitbalance = 20 * 1000;
lastbalance = gamemillis;
}
else if ( lastbalance > gamemillis )
{
lastbalance = 0;
waitbalance = 2 * 60 * 1000;
}
lasttime_eventeams = gamemillis;
}
return switched;
}
void resetserver(const char *newname, int newmode, int newmuts, int newtime)
{
if(m_demo(gamemode)) enddemoplayback();
else enddemorecord();
smode = newmode;
copystring(smapname, newname);
smuts = newmuts;
minremain = newtime > 0 ? newtime : defaultgamelimit(newmode, newmuts);
gamemillis = 0;
gamelimit = minremain*60000;
arenaround = arenaroundstartmillis = 0;
memset(&smapstats, 0, sizeof(smapstats));
interm = nextsendscore = 0;
lastfillup = servmillis;
sents.shrink(0);
if(mastermode == MM_PRIVATE)
{
loopv(savedscores) savedscores[i].valid = false;
}
else savedscores.shrink(0);
ctfreset();
nextmapname[0] = '\0';
forceintermission = false;
}
void startdemoplayback(const char *newname)
{
if(isdedicated) return;
resetserver(newname, G_DEMO, G_M_NONE, -1);
setupdemoplayback();
}
inline void putmap(ucharbuf &p)
{
putint(p, SV_MAPCHANGE);
sendstring(smapname, p);
putint(p, smode);
putint(p, smuts);
putint(p, mapbuffer.available());
putint(p, mapbuffer.revision);
loopv(sents)
if (sents[i].spawned)
putint(p, i);
putint(p, -1);
putint(p, sknives.length());
loopv(sknives)
{
putint(p, sknives[i].id);
putint(p, KNIFETTL + sknives[i].millis - gamemillis);
putint(p, (int)(sknives[i].o.x*DMF));
putint(p, (int)(sknives[i].o.y*DMF));
putint(p, (int)(sknives[i].o.z*DMF));
}
putint(p, sconfirms.length());
loopv(sconfirms)
{
putint(p, sconfirms[i].id);
putint(p, sconfirms[i].team);
putint(p, (int)(sconfirms[i].o.x*DMF));
putint(p, (int)(sconfirms[i].o.y*DMF));
putint(p, (int)(sconfirms[i].o.z*DMF));
}
}
void startgame(const char *newname, int newmode, int newmuts, int newtime, bool notify)
{
if(!newname || !*newname || (newmode == G_DEMO && isdedicated)) fatal("startgame() abused");
if(newmode == G_DEMO)
{
startdemoplayback(newname);
}
else
{
bool lastteammode = m_team(gamemode, mutators);
resetserver(newname, newmode, newmuts, newtime); // beware: may clear *newname
int maploc = MAP_VOID;
mapstats *ms = getservermapstats(smapname, true, &maploc);
mapbuffer.clear();
if(isdedicated && distributablemap(maploc)) mapbuffer.load();
if(ms)
{
smapstats = *ms;
loopi(2)
{
sflaginfo &f = sflaginfos[i];
if(smapstats.flags[i] == 1) // don't check flag positions, if there is more than one flag per team
{
f.x = mapents[smapstats.flagents[i]].x;
f.y = mapents[smapstats.flagents[i]].y;
if (m_overload(gamemode))
f.damage = f.damagetime = f.lastupdate = 0;
}
else f.x = f.y = -1;
}
if (smapstats.flags[0] == 1 && smapstats.flags[1] == 1)
{
sflaginfo &f0 = sflaginfos[0], &f1 = sflaginfos[1];
FlagFlag = pow2(f0.x - f1.x) + pow2(f0.y - f1.y);
}
ssecures.shrink(0);
entity e;
loopi(smapstats.hdr.numents)
{
entity &e = sents.add();
persistent_entity &pe = mapents[i];
e.type = pe.type;
e.transformtype(smode, smuts);
e.x = pe.x;
e.y = pe.y;
e.z = pe.z;
e.attr1 = pe.attr1;
e.attr2 = pe.attr2;
e.attr3 = pe.attr3;
e.attr4 = pe.attr4;
e.spawned = e.fitsmode(smode, smuts);
e.spawntime = 0;
if (m_secure(newmode) && e.type == CTF_FLAG && e.attr2 >= 2)
{
ssecure &s = ssecures.add();
s.id = i;
s.team = TEAM_SPECT;
s.enemy = TEAM_SPECT;
s.overthrown = 0;
s.o = vec(e.x, e.y, getblockfloor(getmaplayoutid(e.x, e.y), false));
s.last_service = 0;
}
}
mapbuffer.setrevision();
logline(ACLOG_INFO, "Map height density information for %s: H = %.2f V = %d, A = %d and MA = %d", smapname, Mheight, Mvolume, Marea, Mopen);
}
else if(isdedicated) sendservmsg("\f3server error: map not found - please start another map or send this map to the server");
if(notify)
{
// change map
// sknives.setsize(0);
packetbuf q(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putmap(q);
sendpacket(NULL, 1, q.finalize());
// time remaining
if(smode>1 || (smode==0 && numnonlocalclients()>0)) sendf(NULL, 1, "ri3", SV_TIMEUP, gamemillis, gamelimit);
}
defformatstring(gsmsg)("Game start: %s on %s, %d players, %d minutes, mastermode %d, ", modestr(smode, smuts), smapname, numclients(), minremain, mastermode);
if(mastermode == MM_MATCH) concatformatstring(gsmsg, "teamsize %d, ", matchteamsize);
if(ms) concatformatstring(gsmsg, "(map rev %d/%d, %s, 'getmap' %sprepared)", smapstats.hdr.maprevision, smapstats.cgzsize, maplocstr[maploc], mapbuffer.available() ? "" : "not ");
else concatformatstring(gsmsg, "error: failed to preload map (map: %s)", maplocstr[maploc]);
logline(ACLOG_INFO, "\n%s", gsmsg);
arenaround = 0;
nokills = true;
if(m_duke(gamemode, mutators)) distributespawns();
if (m_progressive(gamemode, mutators)) progressiveround = 1;
else if (m_zombie(gamemode)) zombiebalance = zombiesremain = 1;
if(notify)
{
// shuffle if previous mode wasn't a team-mode
if(m_team(gamemode, mutators))
{
if (m_zombie(gamemode) || (lastteammode && autoteam))
refillteams(true, FTR_SILENT); // force teams for zombies
else if (!lastteammode)
shuffleteams(FTR_SILENT);
}
// prepare spawns; players will spawn, once they've loaded the correct map
loopv(clients) if(clients[i]->type!=ST_EMPTY)
{
client &c = *clients[i];
c.mapchange();
forcedeath(c);
}
}
checkai(); // re-init ai (init)
// convertcheck();
// reset team scores
loopi(2) steamscores[i] = steamscore(i);
purgesknives();
purgesconfirms(); // but leave the confirms for team modes in arena
if(numnonlocalclients() > 0) setupdemorecord();
if (notify)
{
if (m_keep(gamemode) || m_overload(gamemode)) sendflaginfo();
else if (m_secure(gamemode)) sendsecureflaginfo();
senddisconnectedscores();
}
}
}
inline void addban(client &cl, int reason, int type)
{
ban b = { cl.peer->address, servmillis+scl.ban_time, type };
bans.add(b);
disconnect_client(cl, reason);
}
int getbantype(client &c)
{
if(c.type==ST_LOCAL) return BAN_NONE;
if(ipblacklist.check(c.peer->address.host)) return BAN_BLACKLIST;
loopv(bans)
{
ban &b = bans[i];
if(b.millis < servmillis) { bans.remove(i--); }
if(b.address.host == c.peer->address.host) return b.type;
}
return BAN_NONE;
}
void sendserveropinfo(client *receiver = NULL)
{
loopv(clients)
if(clients[i]->type!=ST_EMPTY)
sendf(receiver, 1, "ri3", SV_SETPRIV, i, clients[i]->role);
}
#include "serveractions.h"
struct voteinfo
{
int owner, callmillis, result, type;
serveraction *action;
voteinfo() : owner(0), callmillis(0), result(VOTE_NEUTRAL), type(SA_NUM), action(NULL) {}
~voteinfo() { delete action; }
void end(int result, int veto)
{
if(action && !action->isvalid()) result = VOTE_NO; // don't perform() invalid votes
if (valid_client(veto)) logline(ACLOG_INFO, "[%s] vote %s, forced by %s (%d)", clients[owner]->gethostname(), result == VOTE_YES ? "passed" : "failed", clients[veto]->formatname(), veto);
else logline(ACLOG_INFO, "[%s] vote %s (%s)", clients[owner]->gethostname(), result == VOTE_YES ? "passed" : "failed", veto == -2 ? "enough votes" : veto == -3 ? "expiry" : "unknown");
sendf(NULL, 1, "ri3", SV_VOTERESULT, result, veto);
this->result = result;
if(result == VOTE_YES)
{
if(valid_client(owner)) clients[owner]->lastvotecall = 0;
if(action) action->perform();
}
loopv(clients) clients[i]->vote = VOTE_NEUTRAL;
}
bool isvalid() const { return valid_client(owner) && action != NULL && action->isvalid(); }
bool isalive() const { return servmillis - callmillis < action->length; }
void evaluate(bool forceend = false, int veto = VOTE_NEUTRAL, int vetoowner = -1)
{
if(result!=VOTE_NEUTRAL) return; // block double action
if(action && !action->isvalid()) end(VOTE_NO, -1);
int stats[VOTE_NUM] = {0}, total_votes = 0;
loopv(clients)
{
if (clients[i]->type == ST_EMPTY || clients[i]->type == ST_AI) continue;
++stats[clients[i]->vote];
++total_votes;
}
const int required_votes_yes = total_votes * action->passratio,
required_votes_no = total_votes * (1 - action->passratio),
expireresult = stats[VOTE_YES] > (int)((stats[VOTE_NO] + stats[VOTE_YES]) * action->passratio) ? VOTE_YES : VOTE_NO;
sendf(NULL, 1, "riv", SV_VOTESTATUS, VOTE_NUM, stats);
// can it end?
if (forceend)
{
if (veto == VOTE_NEUTRAL) end(expireresult, -3);
else end(veto, vetoowner);
}
else if (stats[VOTE_YES] > required_votes_yes || (!isdedicated && clients[owner]->type == ST_LOCAL))
end(VOTE_YES, -2);
else if (stats[VOTE_NO] > required_votes_no)
end(VOTE_NO, -2);
}
};
static voteinfo *curvote = NULL;
void sendcallvote(client *cl = NULL);
bool scallvote(voteinfo &v) // true if a regular vote was called
{
int area = isdedicated ? EE_DED_SERV : EE_LOCAL_SERV;
int error = -1;
client *c = clients[v.owner];
int time = servmillis - c->lastvotecall;
if ( c->nvotes > 0 && time > 4*60*1000 ) c->nvotes -= time/(4*60*1000);
if ( c->nvotes < 0 || c->role >= CR_ADMIN ) c->nvotes = 0;
c->nvotes++;
if( !v.isvalid() ) error = VOTEE_INVALID;
else if( v.action->reqcall > c->role ) error = VOTEE_PERMISSION;
else if( !(area & v.action->area) ) error = VOTEE_AREA;
else if( curvote && curvote->result==VOTE_NEUTRAL ) error = VOTEE_CUR;
else if( c->role == CR_DEFAULT && v.action->isdisabled() ) error = VOTEE_DISABLED;
else if( (c->lastvotecall && servmillis - c->lastvotecall < 60*1000 && c->role < CR_ADMIN && numclients()>1) || c->nvotes > 3 ) error = VOTEE_MAX;
if(error>=0)
{
sendf(c, 1, "ri2", SV_CALLVOTEERR, error);
logline(ACLOG_INFO, "[%s] %s failed to call a vote: %s (%s)", c->gethostname(), c->formatname(), v.action && v.action->desc[0] ? v.action->desc : "[unknown]", voteerrorstr(error));
return false;
}
else
{
DELETEP(curvote);
curvote = &v;
c->lastvotecall = servmillis;
c->nvotes--; // successful votes do not count as abuse
logline(ACLOG_INFO, "[%s] %s called a vote: %s", c->gethostname(), c->formatname(), v.action && v.action->desc[0] ? v.action->desc : "[unknown]");
sendcallvote();
// owner auto votes yes
sendf(NULL, 1, "ri3", SV_VOTE, v.owner, (c->vote = VOTE_YES));
curvote->evaluate();
return true;
}
}
void sendcallvote(client *cl)
{
if (!curvote || curvote->result != VOTE_NEUTRAL)
return;
packetbuf q(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putint(q, SV_CALLVOTE);
putint(q, curvote->owner);
putint(q, curvote->type);
putint(q, curvote->action->length - servmillis + curvote->callmillis);
putint(q, (int)(curvote->action->passratio * 32000));
switch (curvote->type)
{
case SA_KICK:
putint(q, ((kickaction *)curvote->action)->cn);
sendstring(((kickaction *)curvote->action)->reason, q);
break;
case SA_BAN:
putint(q, ((banaction *)curvote->action)->minutes);
putint(q, ((banaction *)curvote->action)->cn);
sendstring(((banaction *)curvote->action)->reason, q);
break;
case SA_MASTERMODE:
putint(q, ((mastermodeaction *)curvote->action)->mode);
break;
case SA_AUTOTEAM:
putint(q, ((autoteamaction *)curvote->action)->enable ? 1 : 0);
break;
case SA_FORCETEAM:
putint(q, ((forceteamaction *)curvote->action)->team);
putint(q, ((forceteamaction *)curvote->action)->cn);
break;
case SA_GIVEADMIN:
putint(q, ((giveadminaction *)curvote->action)->give);
putint(q, ((giveadminaction *)curvote->action)->cn);
break;
case SA_MAP:
putint(q, ((mapaction *)curvote->action)->muts);
putint(q, ((mapaction *)curvote->action)->mode + (((mapaction *)curvote->action)->queue ? G_MAX : 0));
sendstring(((mapaction *)curvote->action)->map, q);
break;
case SA_RECORDDEMO:
putint(q, ((recorddemoaction *)curvote->action)->enable ? 1 : 0);
break;
case SA_CLEARDEMOS:
putint(q, ((cleardemosaction *)curvote->action)->demo);
break;
case SA_SERVERDESC:
sendstring(((serverdescaction *)curvote->action)->desc, q);
break;
case SA_BOTBALANCE:
putint(q, ((botbalanceaction *)curvote->action)->balance);
break;
case SA_SUBDUE:
putint(q, ((subdueaction *)curvote->action)->cn);
break;
case SA_REVOKE:
putint(q, ((revokeaction *)curvote->action)->cn);
break;
case SA_STOPDEMO:
// compatibility
default:
case SA_REMBANS:
case SA_SHUFFLETEAMS:
break;
}
// send vote states
loopv(clients) if (clients[i]->vote != VOTE_NEUTRAL)
{
putint(q, SV_VOTE);
putint(q, i);
putint(q, clients[i]->vote);
}
sendpacket(cl, 1, q.finalize());
}
void setpriv(client &cl, int priv)
{
if (!priv) // relinquish
{
if (!cl.role) return; // no privilege to relinquish
sendf(NULL, 1, "ri4", SV_CLAIMPRIV, cl.clientnum, cl.role, 1);
logline(ACLOG_INFO, "[%s] %s relinquished %s access", cl.gethostname(), cl.formatname(), privname(cl.role));
cl.role = CR_DEFAULT;
sendserveropinfo();
return;
}
else if (cl.role >= priv)
{
sendf(&cl, 1, "ri4", SV_CLAIMPRIV, cl.clientnum, priv, 2);
return;
}
/*
else if(priv >= PRIV_ADMIN)
{
loopv(clients)
if(clients[i]->type != ST_EMPTY && clients[i]->authpriv < PRIV_MASTER && clients[i]->priv == PRIV_MASTER)
setpriv(*clients[i], PRIV_NONE);
}
*/
cl.role = priv;
sendf(NULL, 1, "ri4", SV_CLAIMPRIV, cl.clientnum, cl.role, 0);
logline(ACLOG_INFO, "[%s] %s claimed %s access", cl.gethostname(), cl.formatname(), privname(cl.role));
sendserveropinfo();
//if(curvote) curvote->evaluate();
}
void senddisconnectedscores(client *cl)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putint(p, SV_DISCSCORES);
if(mastermode == MM_MATCH)
{
loopv(savedscores)
{
savedscore &sc = savedscores[i];
if(sc.valid)
{
putint(p, sc.team);
sendstring(sc.name, p);
putint(p, sc.flagscore);
putint(p, sc.frags);
putint(p, sc.assists);
putint(p, sc.deaths);
putint(p, sc.points);
}
}
}
putint(p, -1);
sendpacket(cl, 1, p.finalize());
}
const char *disc_reason(int reason)
{
static const char *disc_reasons[DISC_NUM] = {
"normal", "end of packet/overread", "vote-kicked", "vote-banned", "tag type", "connection refused - banned", "incorrect password", "failed login", "the server is FULL - try again later", "mastermode is \"private\" - must be \"open\"",
"bad nickname", "nickname is IP protected", "nickname requires password", "duplicate connection", "error - packet flood", "timeout",
"extension", "ext2", "ext3"
};
return reason >= 0 && (size_t)reason < sizeof(disc_reasons)/sizeof(disc_reasons[0]) ? disc_reasons[reason] : "unknown";
}
void clientdisconnect(client &cl)
{
const int n = cl.clientnum;
sdropflag(n);
// remove assists/revenge
loopv(clients) if(valid_client(i) && i != n)
{
clientstate &cs = clients[i]->state;
cs.damagelog.removeobj(n);
cs.revengelog.removeobj(n);
}
// delete kill confirmed references
loopv(sconfirms)
{
if(sconfirms[i].actor == n) sconfirms[i].actor = -1;
if(sconfirms[i].target == n) sconfirms[i].target = -1;
}
cl.zap();
}
#include "serverai.h"
void disconnect_client(client &c, int reason)
{
if(c.type!=ST_TCPIP) return;
sdropflag(c.clientnum);
// reassign/delete AI
loopv(clients)
if(clients[i]->ownernum == c.clientnum)
if(!shiftai(*clients[i], -1, c.clientnum))
deleteai(*clients[i]);
// remove privilege
if(c.role) setpriv(c, CR_DEFAULT);
c.state.lastdisc = servmillis;
const char *scoresaved = "";
if(c.haswelcome)
{
// save score
savedscore *sc = findscore(c, true);
if(sc)
{
sc->save(c.state, c.team);
scoresaved = ", score saved";
}
// save limits
findlimit(c, true);
}
int sp = (servmillis - c.connectmillis) / 1000;
if (reason >= 0) logline(ACLOG_INFO, "[%s] disconnecting client %s (%s) cn %d, %d seconds played%s", c.gethostname(), c.name, disc_reason(reason), c.clientnum, sp, scoresaved);
else logline(ACLOG_INFO, "[%s] disconnected client %s cn %d, %d seconds played%s", c.gethostname(), c.name, c.clientnum, sp, scoresaved);
totalclients--;
c.peer->data = (void *)-1;
if(reason>=0) enet_peer_disconnect(c.peer, reason);
sendf(NULL, 1, "ri3", SV_CDIS, c.clientnum, max(reason, 0));
if(curvote) curvote->evaluate();
// do cleanup
clientdisconnect(c);
extern void freeconnectcheck(int cn);
freeconnectcheck(c.clientnum); // disconnect - ms check is void
if(*scoresaved && mastermode == MM_MATCH) senddisconnectedscores();
checkai(); // disconnect
convertcheck();
}
#include "serverauth.h"
void sendresume(client &c, bool broadcast)
{
sendf(broadcast ? NULL : &c, 1, "ri9i7i5vvi", SV_RESUME,
c.clientnum,
c.state.state == CS_WAITING ? CS_DEAD : c.state.state,
c.state.lifesequence,
c.state.primary,
c.state.secondary,
c.state.perk1,
c.state.perk2,
c.state.gunselect,
c.state.flagscore,
c.state.frags,
c.state.points,
c.state.assists,
c.state.deaths,
c.state.health,
c.state.armour,
c.state.pointstreak,
c.state.deathstreak,
c.state.airstrikes,
c.state.radarearned,
c.state.nukemillis,
NUMGUNS, c.state.ammo,
NUMGUNS, c.state.mag,
-1);
}
bool restorescore(client &c)
{
//if(ci->local) return false;
savedscore *sc = findscore(c, false);
if(sc && sc->valid)
{
sc->restore(c.state);
sc->valid = false;
if ( c.connectmillis - c.state.lastdisc < 5000 ) c.state.reconnections++;
else if ( c.state.reconnections ) c.state.reconnections--;
return true;
}
return false;
}
void sendservinfo(client &c)
{
sendf(&c, 1, "ri5", SV_SERVINFO, c.clientnum, isdedicated ? SERVER_PROTOCOL_VERSION : PROTOCOL_VERSION, c.salt, scl.serverpassword[0] ? 1 : 0);
}
void putinitai(client &c, ucharbuf &p)
{
putint(p, SV_INITAI);
putint(p, c.clientnum);
putint(p, c.ownernum);
putint(p, c.bot_seed);
putint(p, c.skin[0]);
putint(p, c.skin[1]);
putint(p, c.team);
putint(p, c.level);
}
void putinitclient(client &c, packetbuf &p)
{
if(c.type == ST_AI) return putinitai(c, p);
putint(p, SV_INITCLIENT);
putint(p, c.clientnum);
sendstring(c.name, p);
putint(p, c.skin[TEAM_CLA]);
putint(p, c.skin[TEAM_RVSF]);
putint(p, c.level);
putint(p, c.team);
putint(p, c.acbuildtype | (c.authpriv != -1 ? 0x02 : 0));
putint(p, c.acthirdperson);
}
void sendinitclient(client &c)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putinitclient(c, p);
sendpacket(NULL, 1, p.finalize(), c.clientnum);
}
void putteamscore(int team, ucharbuf &p, bool set_valid)
{
if (team < 0 || team > 1) return;
steamscore &t = steamscores[team];
putint(p, SV_TEAMSCORE);
putint(p, team);
putint(p, t.points);
putint(p, t.flagscore);
putint(p, t.frags);
putint(p, t.assists);
putint(p, t.deaths);
if (set_valid) t.valid = true;
}
void sendteamscore(int team, client *cl = NULL)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
putteamscore(team, p, true);
sendpacket(cl, 1, p.finalize());
}
inline void welcomeinitclient(packetbuf &p, int exclude = -1)
{
loopv(clients)
{
client &c = *clients[i];
if(c.type==ST_EMPTY || !c.isauthed || c.clientnum == exclude) continue;
putinitclient(c, p);
}
}
void welcomepacket(packetbuf &p, client *c)
{
if(!smapname[0]) maprot.next(false);
const int n = c ? c->clientnum : -1;
const int numcl = numclients();
putint(p, SV_WELCOME);
putint(p, smapname[0] && !m_demo(gamemode) ? numcl : -1);
// Sync weapon info on connect
loopi(NUMGUNS)
{
putint(p, guns[i].reloadtime);
putint(p, guns[i].attackdelay);
//putint(p, guns[i].damage);
//putint(p, guns[i].projspeed);
//putint(p, guns[i].part);
putint(p, guns[i].spread);
putint(p, guns[i].spreadrem);
putint(p, guns[i].kick);
putint(p, guns[i].addsize);
putint(p, guns[i].magsize);
//putint(p, guns[i].mdl_kick_rot);
//putint(p, guns[i].mdl_kick_back);
putint(p, guns[i].recoil);
putint(p, guns[i].maxrecoil);
putint(p, guns[i].recoilangle);
putint(p, guns[i].pushfactor);
}
if(smapname[0] && !m_demo(gamemode))
{
putmap(p);
if(smode>1 || (smode==0 && numnonlocalclients()>0))
{
putint(p, SV_TIMEUP);
putint(p, (gamemillis>=gamelimit || forceintermission) ? gamelimit : gamemillis);
putint(p, gamelimit);
//putint(p, minremain*60);
}
if (m_flags(gamemode))
{
if (m_secure(gamemode))
{
loopv(ssecures) putsecureflaginfo(p, ssecures[i]);
}
else
{
loopi(2) putflaginfo(p, i);
}
}
}
savedscore *sc = NULL;
if(c)
{
if(c->type == ST_TCPIP) sendserveropinfo(c);
c->team = mastermode == MM_MATCH && sc ? team_tospec(sc->team) : TEAM_SPECT;
putint(p, SV_SETTEAM);
putint(p, n);
putint(p, c->team | (FTR_SILENT << 4));
putint(p, SV_FORCEDEATH);
putint(p, n);
sendf(NULL, 1, "ri2x", SV_FORCEDEATH, n, n);
}
if(!c || clients.length()>1)
{
putint(p, SV_RESUME);
loopv(clients)
{
client &c = *clients[i];
if(c.type!=ST_TCPIP || c.clientnum==n) continue;
putint(p, c.clientnum);
putint(p, c.state.state == CS_WAITING ? CS_DEAD : c.state.state);
putint(p, c.state.lifesequence);
putint(p, c.state.primary);
putint(p, c.state.secondary);
putint(p, c.state.perk1);
putint(p, c.state.perk2);
putint(p, c.state.gunselect);
putint(p, c.state.flagscore);
putint(p, c.state.frags);
putint(p, c.state.points);
putint(p, c.state.assists);
putint(p, c.state.deaths);
putint(p, c.state.health);
putint(p, c.state.armour);
putint(p, c.state.pointstreak);
putint(p, c.state.deathstreak);
putint(p, c.state.airstrikes);
putint(p, c.state.radarearned);
putint(p, c.state.nukemillis);
loopi(NUMGUNS) putint(p, c.state.ammo[i]);
loopi(NUMGUNS) putint(p, c.state.mag[i]);
}
putint(p, -1);
welcomeinitclient(p, n);
putteamscore(0, p, false);
if(m_team(gamemode, mutators)) putteamscore(1, p, false);
}
putint(p, SV_SERVERMODE);
putint(p, sendservermode(false));
const char *motd = scl.motd[0] ? scl.motd : infofiles.getmotd(c ? c->lang : "");
if(motd)
{
putint(p, SV_TEXT);
putint(p, -1);
putint(p, 0);
putint(p, 0);
sendstring(motd, p);
}
}
void sendwelcome(client &cl, int chan)
{
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
welcomepacket(p, &cl);
sendpacket(&cl, chan, p.finalize());
cl.haswelcome = true;
}
void forcedeath(client &cl)
{
sdropflag(cl.clientnum);
clientstate &cs = cl.state;
cs.state = CS_DEAD;
cs.respawn();
cs.lastspawn = -1;
cs.lastdeath = gamemillis;
if (cs.nukemillis)
{
// nuke cancelled!
cs.nukemillis = 0;
sendf(NULL, 1, "ri4", SV_STREAKUSE, cl.clientnum, STREAK_NUKE, -2);
}
sendf(NULL, 1, "ri2", SV_FORCEDEATH, cl.clientnum);
}
bool movechecks(client &cp, const vec &newo, const int newf, const int newg)
{
clientstate &cs = cp.state;
// Only check alive players (skip editmode users)
if(cs.state != CS_ALIVE) return true;
// new crouch and scope
const bool newcrouching = (newf >> 7) & 1;
if (cs.crouching != newcrouching)
{
cs.crouching = newcrouching;
cs.crouchmillis = gamemillis - CROUCHTIME + min(gamemillis - cs.crouchmillis, CROUCHTIME);
}
const bool newscoping = (newg >> 4) & 1;
if (newscoping != cs.scoping)
{
if (!newscoping || (ads_gun(cs.gunselect) && ads_classic_allowed(cs.gunselect)))
{
cs.scoping = newscoping;
cs.scopemillis = gamemillis - ADSTIME(cs.perk2 == PERK_TIME) + min(gamemillis - cs.scopemillis, ADSTIME(cs.perk2 == PERK_TIME));
}
// else: clear the scope from the packet? other clients can ignore it?
}
// deal damage from movements
if(!cs.protect(gamemillis, gamemode, mutators))
{
// medium transfer (falling damage)
const bool newonfloor = (newf>>4)&1, newonladder = (newf>>5)&1, newunderwater = newo.z + PLAYERHEIGHT * cs.crouchfactor(gamemillis) < smapstats.hdr.waterlevel;
if((newonfloor || newonladder || newunderwater) && !cs.onfloor)
{
const float dz = cs.fallz - cs.o.z;
if(newonfloor)
{ // air to solid
if (dz > 2.5 * CUBES_PER_METER)
{
bool hit = false;
// fall onto others
loopv(clients)
{
client &t = *clients[i];
clientstate &ts = t.state;
// basic checks
if (t.type == ST_EMPTY || ts.state != CS_ALIVE || i == cp.clientnum) continue;
// check from above
if(ts.o.distxy(cs.o) > 2.5f*PLAYERRADIUS) continue;
// check from side
const float dz2 = cs.o.z - ts.o.z;
if(dz2 > PLAYERABOVEEYE + 2 || -dz2 > PLAYERHEIGHT + 2) continue;
if(!isteam(&t, &cp) && !ts.protect(gamemillis, gamemode, mutators))
serverdied(t, cp, 0, OBIT_FALL, FRAG_NONE, cs.o);
hit = true;
}
if (!hit) // not cushioned by another player
{
// 4 meters without damage + 2/0.5 HP/meter
// int damage = ((cs.fallz - newo.z) - 4 * CUBES_PER_METER) * HEALTHSCALE / (cs.perk1 == PERK1_LIGHT ? 8 : 2);
// 2 meters without damage, then quadratic up to 100 @ 22m (52m with lightweight)
int damage = min((int)(powf((dz - 2 * CUBES_PER_METER) / (cs.perk1 == PERK1_LIGHT ? 20 : 8), 2.f) * HEALTHSCALE), 100 * HEALTHSCALE);
if (damage >= 1 * HEALTHSCALE) // don't heal the player
{
// maximum damage is 95 for balance purposes
serverdamage(cp, cp, min(damage, (m_classic(gamemode, mutators) ? 30 : 95) * HEALTHSCALE), OBIT_FALL, FRAG_NONE, cs.o); // max 95, "30" (15) for classic
}
}
}
}
else if (newunderwater && dz > 8 * CUBES_PER_METER) // air to liquid
serverdamage(cp, cp, (m_classic(gamemode, mutators) ? 20 : 35) * HEALTHSCALE, OBIT_FALL_WATER, FRAG_NONE, cs.o); // fixed damage @ 35, "20" (10) for classic
cs.onfloor = true;
}
else if(!newonfloor)
{ // airborne
if(cs.onfloor || cs.fallz < cs.o.z) cs.fallz = cs.o.z;
cs.onfloor = false;
}
// did we die?
if(cs.state != CS_ALIVE) return false;
}
// out of map check
vec checko(newo);
checko.z += PLAYERHEIGHT / 2; // because the positions are now at the feet
if (/*cp.type != ST_LOCAL &&*/ !m_edit(gamemode) && checkpos(checko, false))
{
if (cp.type == ST_AI)
cp.suicide(OBIT_BOT);
else
{
logline(ACLOG_INFO, "[%s] %s collides with the map (%d)", cp.gethostname(), cp.formatname(), ++cp.mapcollisions);
defformatstring(msg)("%s \f2collides with the map \f5- \f3forcing death", cp.formatname());
sendservmsg(msg);
sendf(&cp, 1, "ri", SV_MAPIDENT);
forcedeath(cp);
cp.isonrightmap = false; // cannot spawn until you get the right map
}
return false; // no pickups for you!
}
// the rest can proceed without killing
// item pickups
if(!m_zombie(gamemode) || cp.team != TEAM_CLA)
loopv(sents)
{
entity &e = sents[i];
const bool cantake = (e.spawned && cs.canpickup(e.type, cp.type == ST_AI)), canheal = (e.type == I_HEALTH && cs.wounds.length());
if(!cantake && !canheal) continue;
const int ls = (1 << maplayout_factor) - 2, maplayoutid = getmaplayoutid(e.x, e.y);
const bool getmapz = maplayout && e.x > 2 && e.y > 2 && e.x < ls && e.y < ls;
const char &mapz = getmapz ? getblockfloor(maplayoutid, false) : 0;
vec v(e.x, e.y, getmapz ? (mapz + e.attr1) : cs.o.z);
float dist = cs.o.dist(v);
if(dist > 3) continue;
if(canheal)
{
// healing station
addpt(cp, HEALWOUNDPT * cs.wounds.length(), PR_HEALWOUND);
cs.wounds.shrink(0);
}
if(cantake)
{
// server side item pickup, acknowledge first client that moves to the entity
e.spawned = false;
int spawntime(int type);
sendf(NULL, 1, "ri4", SV_ITEMACC, i, cp.clientnum, e.spawntime = spawntime(e.type));
cs.pickup(sents[i].type);
}
}
// flags
if (m_flags(gamemode) && !m_secure(gamemode) && !m_overload(gamemode)) loopi(2)
{
void flagaction(int flag, int action, int actor);
sflaginfo &f = sflaginfos[i];
sflaginfo &of = sflaginfos[team_opposite(i)];
bool forcez = false;
vec v(-1, -1, cs.o.z);
if (m_bomber(gamemode) && i == cp.team && f.state == CTFF_STOLEN && f.actor_cn == cp.clientnum)
{
v.x = of.x;
v.y = of.y;
}
else switch (f.state)
{
case CTFF_STOLEN:
if (!m_return(gamemode, mutators) || i != cp.team) break;
case CTFF_INBASE:
v.x = f.x; v.y = f.y;
break;
case CTFF_DROPPED:
if (f.drop_cn == cp.clientnum && f.dropmillis + 2000 > servmillis) break;
v.x = f.pos[0]; v.y = f.pos[1];
forcez = true;
break;
}
if (v.x < 0) continue;
if (forcez)
v.z = f.pos[2];
else
v.z = getsblock(getmaplayoutid((int)v.x, (int)v.y)).floor;
float dist = cs.o.dist(v);
if (dist > 2) continue;
if (m_capture(gamemode))
{
if (i == cp.team) // it's our flag
{
if (f.state == CTFF_DROPPED)
{
if (m_return(gamemode, mutators) /*&& (of.state != CTFF_STOLEN || of.actor_cn != sender)*/)
flagaction(i, FA_PICKUP, cp.clientnum);
else flagaction(i, FA_RETURN, cp.clientnum);
}
else if (f.state == CTFF_STOLEN && cp.clientnum == f.actor_cn)
flagaction(i, FA_RETURN, cp.clientnum);
else if (f.state == CTFF_INBASE && of.state == CTFF_STOLEN && of.actor_cn == cp.clientnum && gamemillis >= of.stolentime + 1000)
flagaction(team_opposite(i), FA_SCORE, cp.clientnum);
}
else
{
/*if(m_return && of.state == CTFF_STOLEN && of.actor_cn == sender) flagaction(team_opposite(i), FA_RETURN, sender);*/
flagaction(i, f.state == CTFF_INBASE ? FA_STEAL : FA_PICKUP, cp.clientnum);
}
}
else if (m_hunt(gamemode) || m_bomber(gamemode))
{
// BTF only: score their flag by bombing their base!
if (f.state == CTFF_STOLEN)
{
flagaction(i, FA_SCORE, cp.clientnum);
// nuke message + points
nuke(cp, !m_gsp1(gamemode, mutators), false); // no suicide for demolition, but suicide for bomber
/*
if(m_gsp1(gamemode, mutators))
{
// force round win
loopv(clients) if(valid_client(i) && clients[i]->state.state == CS_ALIVE && !isteam(clients[i], &cp))
forcedeath(clients[i]);
}
else explosion(cp, v, WEAP_GRENADE); // identical to self-nades, replace with something else?
*/
}
else if (i == cp.team)
{
if (m_hunt(gamemode)) f.drop_cn = -1; // force pickup
flagaction(i, FA_PICKUP, cp.clientnum);
}
else if (f.state == CTFF_DROPPED && gamemillis >= of.stolentime + 500)
flagaction(i, m_hunt(gamemode) ? FA_SCORE : FA_RETURN, cp.clientnum);
}
else if (m_keep(gamemode) && f.state == CTFF_INBASE)
flagaction(i, FA_PICKUP, cp.clientnum);
else if (m_ktf2(gamemode, mutators) && f.state != CTFF_STOLEN)
{
bool cantake = of.state != CTFF_STOLEN || of.actor_cn != cp.clientnum || !m_team(gamemode, mutators);
if (!cantake)
{
cantake = true;
loopv(clients)
if (i != cp.clientnum && valid_client(i) && clients[i]->type != ST_AI && clients[i]->team == cp.team)
{
cantake = false;
break;
}
}
if (cantake) flagaction(i, FA_PICKUP, cp.clientnum);
}
}
// kill confirmed
loopv(sconfirms) if (sconfirms[i].o.dist(cs.o) < 5.f)
{
if (cp.team == sconfirms[i].team)
{
addpt(cp, KCKILLPTS, PR_KC);
usesteamscore(sconfirms[i].team).points += sconfirms[i].points;
// the following line doesn't have to set the valid flag twice
getsteamscore(sconfirms[i].team).frags += sconfirms[i].frag;
++usesteamscore(sconfirms[i].death).deaths;
}
else
{
addpt(cp, KCDENYPTS, cp.clientnum == sconfirms[i].target ? PR_KD_SELF : PR_KD);
if (valid_client(sconfirms[i].actor) && clients[sconfirms[i].actor]->type != ST_AI)
addptreason(*clients[sconfirms[i].actor], PR_KD_ENEMY);
}
sendf(NULL, 1, "ri2", SV_CONFIRMREMOVE, sconfirms[i].id);
sconfirms.remove(i--);
}
// throwing knife pickup
if (cp.type != ST_AI) loopv(sknives)
{
const bool pickup = cs.o.dist(sknives[i].o) < 5 && cs.ammo[GUN_KNIFE] < ammostats[GUN_KNIFE].max;
const bool expired = gamemillis - sknives[i].millis > KNIFETTL;
if (pickup || expired)
{
if (pickup) sendf(NULL, 1, "ri5", SV_RELOAD, cp.clientnum, GUN_KNIFE, cs.mag[GUN_KNIFE], ++cs.ammo[GUN_KNIFE]);
sendf(NULL, 1, "ri2", SV_KNIFEREMOVE, sknives[i].id);
sknives.remove(i--);
}
}
return true;
}
int checktype(int type, client &cl)
{
if (type < 0 || type >= SV_NUM) return -1; // out of range
if (cl.type == ST_TCPIP)
{
// only allow edit messages in coop-edit mode
if (!m_edit(smode) && type >= SV_EDITH && type <= SV_NEWMAP) return -1; // SV_EDITMODE is handled
// overflow
static const int exempt[] = { SV_POS, SV_POSC, SV_SPAWN, SV_SHOOT, SV_SHOOTC, SV_EXPLODE };
// these types don't not contribute to overflow, just because the bots will have to send them too
loopi(sizeof(exempt) / sizeof(int)) if (type == exempt[i]) return type;
if(++cl.overflow >= 200) return -2;
}
return type;
}
// server side processing of updates: does very little and most state is tracked client only
// could be extended to move more gameplay to server (at expense of lag)
void process(ENetPacket *packet, int sender, int chan)
{
ucharbuf p(packet->data, packet->dataLength);
char text[MAXTRANS];
client *cl = sender>=0 ? clients[sender] : NULL;
int type;
if(cl && !cl->isauthed)
{
if(chan==0) return;
else if(chan!=1 || getint(p)!=SV_CONNECT) disconnect_client(*cl, DISC_TAGT);
else
{
cl->acversion = getint(p);
cl->acbuildtype = getint(p);
cl->acthirdperson = getint(p);
cl->acguid = getint(p) & (0x80 | 0x1F00 | 0x40 | 0x20 | 0x8 | 0x4);
const int connectauthtoken = getint(p), connectauthuser = getint(p);
getstring(text, p);
filtername(text, text);
if(!text[0]) copystring(text, "unarmed");
copystring(cl->name, text, MAXNAMELEN + 1);
getstring(text, p);
copystring(cl->pwd, text);
getstring(text, p);
filterlang(cl->lang, text);
cl->state.nextprimary = getint(p);
cl->state.nextsecondary = getint(p);
cl->state.nextperk1 = getint(p);
cl->state.nextperk2 = getint(p);
loopi(2) cl->skin[i] = getint(p);
logversion(*cl);
int disc = p.remaining() ? DISC_TAGT : allowconnect(*cl, connectauthtoken, connectauthuser);
if (disc) disconnect_client(*cl, disc);
else cl->isauthed = true;
}
if(!cl->isauthed) return;
if(cl->type==ST_TCPIP)
{
loopv(clients) if(i != sender)
{
client &dup = *clients[i];
if(dup.type==ST_TCPIP && dup.peer->address.host==cl->peer->address.host && dup.peer->address.port==cl->peer->address.port)
disconnect_client(dup, DISC_DUP);
}
// ask the master-server about this client
extern void connectcheck(int cn, int guid, const char *hostname, int authreq, int authuser);
connectcheck(sender, cl->acguid, cl->hostname, cl->authreq, cl->authuser);
}
sendwelcome(*cl);
if(restorescore(*cl)) { sendresume(*cl, true); senddisconnectedscores(); }
else if(cl->type==ST_TCPIP) senddisconnectedscores(cl);
sendinitclient(*cl);
findlimit(*cl, false);
if (curvote)
{
sendcallvote(cl);
curvote->evaluate();
}
checkai(); // connected
// convertcheck();
while(reassignai());
}
if(!cl) { logline(ACLOG_ERROR, "<NULL> client in process()"); return; } // should never happen anyway
if(packet->flags&ENET_PACKET_FLAG_RELIABLE) reliablemessages = true;
#define QUEUE_MSG { if(cl->type==ST_TCPIP) while(curmsg<p.length()) cl->messages.add(p.buf[curmsg++]); }
#define QUEUE_BUF(body) \
{ \
if(cl->type==ST_TCPIP) \
{ \
curmsg = p.length(); \
{ body; } \
} \
}
#define QUEUE_INT(n) QUEUE_BUF(putint(cl->messages, n))
#define QUEUE_UINT(n) QUEUE_BUF(putuint(cl->messages, n))
#define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cl->messages))
#define MSG_PACKET(packet) \
packetbuf buf(16 + p.length() - curmsg, ENET_PACKET_FLAG_RELIABLE); \
putint(buf, SV_CLIENT); \
putint(buf, cl->clientnum); \
/*putuint(buf, p.length() - curmsg);*/ \
buf.put(&p.buf[curmsg], p.length() - curmsg); \
ENetPacket *packet = buf.finalize();
int curmsg;
while((curmsg = p.length()) < p.maxlen)
{
type = checktype(getint(p), *cl);
#ifdef _DEBUG
if(type!=SV_POS && type!=SV_POSC && type!=SV_CLIENTPING && type!=SV_PINGPONG && type!=SV_CLIENT)
{
DEBUGVAR(cl->name);
ASSERT(type>=0 && type<SV_NUM);
DEBUGVAR(messagenames[type]);
protocoldebug(true);
}
else protocoldebug(false);
#endif
switch(type)
{
case SV_TEXT:
{
const int flags = getint(p) & SAY_CLIENT,
voice = getint(p),
targ = flags & SAY_PRIVATE ? getint(p) : -1;
getstring(text, p);
filtertext(text, text);
trimtrailingwhitespace(text);
if(*text)
sendtext(text, *cl, flags, voice, targ);
break;
}
case SV_TYPING:
{
// FIXME: this is not totally reliable for clients connecting
// after this message has been sent
const int typing = getint(p);
sendf(NULL, 1, "ri3", SV_TYPING, sender, typing);
break;
}
case SV_MAPIDENT:
{
int gzs = getint(p);
int rev = getint(p);
if(!isdedicated || (smapstats.cgzsize == gzs && smapstats.hdr.maprevision == rev))
{ // here any game really starts for a client: spawn, if it's a new game - don't spawn if the game was already running
cl->isonrightmap = true;
if (cl->loggedwrongmap) logline(ACLOG_INFO, "[%s] %s is now on the right map: revision %d/%d", cl->gethostname(), cl->formatname(), rev, gzs);
bool spawn = false;
if(team_isspect(cl->team))
{
if(numclients() < 2 && !m_demo(gamemode) && mastermode != MM_MATCH) // spawn on empty servers
{
spawn = updateclientteam(*cl, chooseteam(*cl), FTR_SILENT);
}
}
else
{
if((cl->freshgame || numclients() < 2) && !m_demo(gamemode)) spawn = true;
}
cl->freshgame = false;
if(spawn) sendspawn(*cl);
}
else
{
forcedeath(*cl);
logline(ACLOG_INFO, "[%s] %s is on the wrong map: revision %d/%d", cl->gethostname(), cl->formatname(), rev, gzs);
cl->loggedwrongmap = true;
}
break;
}
case SV_WEAPCHANGE: // cn weap
{
int cn = getint(p), gunselect = getint(p);
if (!cl->hasclient(cn)) break;
client &cp = *clients[cn];
if (gunselect < 0 || gunselect >= NUMGUNS) break;
#if (SERVER_BUILTIN_MOD & 8)
if (gunselect != cp.state.primary)
{
// stop bots from switching back
sendf(NULL, 1, "ri5", SV_RELOAD, cn, gunselect, cp.state.mag[gunselect] = 0, cp.state.ammo[gunselect] = 0);
// disallow switching
sendf(sender, 1, "ri3", SV_WEAPCHANGE, cn, cp.state.primary);
}
#else
cp.state.gunwait[cp.state.gunselect = gunselect] += SWITCHTIME(cp.state.perk1 == PERK_TIME);
QUEUE_MSG;
#endif
break;
}
case SV_QUICKSWITCH:
{
const int cn = getint(p);
if (!cl->hasclient(cn)) break;
clientstate &cs = clients[cn]->state;
cs.gunwait[cs.gunselect = cs.primary] += SWITCHTIME(cs.perk1 == PERK_TIME) / 2;
QUEUE_MSG;
break;
}
case SV_THROWNADE:
{
const int cn = getint(p);
vec from, vel;
loopi(3) from[i] = getint(p)/DMF;
loopi(3) vel[i] = getint(p)/DNF;
int cooked = clamp(getint(p), 1, NADETTL);
if (!cl->hasclient(cn)) break;
clientstate &cps = clients[cn]->state;
if (cps.grenades.throwable <= 0) break;
--cps.grenades.throwable;
checkpos(from);
if (vel.magnitude() > NADEPOWER) vel.normalize().mul(NADEPOWER);
sendf(NULL, 1, "ri9x", SV_THROWNADE, cn, (int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF),
(int)(vel.x*DNF), (int)(vel.y*DNF), (int)(vel.z*DNF), cooked, sender);
break;
}
case SV_THROWKNIFE:
{
const int cn = getint(p);
vec from, vel;
loopi(3) from[i] = getint(p) / DMF;
loopi(3) vel[i] = getint(p) / DNF;
if (!cl->hasclient(cn)) break;
clientstate &cps = clients[cn]->state;
if (cps.knives.throwable <= 0) break;
--cps.knives.throwable;
checkpos(from);
if (vel.magnitude() > KNIFEPOWER) vel.normalize().mul(KNIFEPOWER);
sendf(NULL, 1, "ri8x", SV_THROWKNIFE, cn,
(int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF),
(int)(vel.x*DNF), (int)(vel.y*DNF), (int)(vel.z*DNF),
sender);
break;
}
case SV_STREAKUSE:
{
vec o;
loopi(3) o[i] = getint(p) / DMF;
// can't use streaks unless alive
if (!cl->state.isalive(gamemillis)) break;
// check how many airstrikes available first
if (cl->state.airstrikes > 0)
usestreak(*cl, STREAK_AIRSTRIKE, NULL, &o);
break;
}
case SV_LOADOUT:
{
int nextprimary = getint(p), nextsecondary = getint(p), perk1 = getint(p), perk2 = getint(p);
clientstate &cs = cl->state;
cs.nextperk1 = perk1;
cs.nextperk2 = perk2;
if (nextprimary >= 0 && nextprimary < NUMGUNS)
cs.nextprimary = nextprimary;
if (nextsecondary >= 0 && nextsecondary < NUMGUNS)
cs.nextsecondary = nextsecondary;
break;
}
case SV_SWITCHNAME:
{
QUEUE_MSG;
getstring(text, p);
filtername(text, text);
if(!text[0]) copystring(text, "unarmed");
QUEUE_STR(text);
bool namechanged = strcmp(cl->name, text) != 0;
if (namechanged) logline(ACLOG_INFO, "[%s] %s changed name to %s", cl->gethostname(), cl->formatname(), text);
copystring(cl->name, text, MAXNAMELEN + 1);
if(namechanged)
{
// very simple spam detection (possible FIXME: centralize spam detection)
if(servmillis - cl->lastprofileupdate < 1000)
{
++cl->fastprofileupdates;
if(cl->fastprofileupdates == 3) sendservmsg("\f3Please do not spam");
// TODO: ACR would delay/throttle name changes
}
else if(servmillis - cl->lastprofileupdate > 10000) cl->fastprofileupdates = 0;
cl->lastprofileupdate = servmillis;
switch (const int nwl = nickblacklist.checkwhitelist(*cl))
{
case NWL_PWDFAIL:
case NWL_IPFAIL:
logline(ACLOG_INFO, "[%s] '%s' matches nickname whitelist: wrong IP/PWD", cl->gethostname(), cl->name);
disconnect_client(*cl, nwl == NWL_IPFAIL ? DISC_NAME_IP : DISC_NAME_PWD);
break;
case NWL_UNLISTED:
{
int l = nickblacklist.checkblacklist(cl->name);
if(l >= 0)
{
logline(ACLOG_INFO, "[%s] '%s' matches nickname blacklist line %d", cl->gethostname(), cl->name, l);
disconnect_client(*cl, DISC_NAME);
}
break;
}
}
}
break;
}
case SV_SETTEAM:
{
int t = getint(p);
if (cl->team == t || !team_isvalid(t)) break;
if (mastermode != MM_MATCH && (t == TEAM_CLA_SPECT || t == TEAM_RVSF_SPECT))
t = TEAM_SPECT;
if (cl->role < CR_ADMIN && t < TEAM_SPECT)
{
if (mastermode == MM_OPEN && cl->state.forced && team_base(cl->team) != team_base(t))
{
// no free will changes for forced people
sendf(cl, 1, "ri2", SV_TEAMDENY, 0x10);
break;
}
else if (!autobalance_mode)
{
sendf(cl, 1, "ri2", SV_TEAMDENY, 0x11);
break;
}
else if (m_team(gamemode, mutators))
{
int *teamsizes = numteamclients(sender);
if (mastermode == MM_MATCH)
{
if (matchteamsize && t != TEAM_SPECT)
{
if (team_base(t) != team_base(cl->team))
{
// no switching sides in match mode when teamsize is set
sendf(cl, 1, "ri2", SV_TEAMDENY, 0x12);
break;
}
else if (team_isactive(t) && teamsizes[t] >= matchteamsize)
{
// ensure maximum team size
sendf(cl, 1, "ri2", SV_TEAMDENY, t);
break;
}
}
}
else if (team_isactive(t) && autoteam && teamsizes[t] > teamsizes[team_opposite(t)])
{
// don't switch to an already bigger team
sendf(cl, 1, "ri2", SV_TEAMDENY, t);
break;
}
}
}
updateclientteam(*cl, t, FTR_PLAYERWISH);
checkai(); // user switch
// convertcheck();
break;
}
case SV_SWITCHSKIN:
{
loopi(2) cl->skin[i] = getint(p);
QUEUE_MSG;
if(servmillis - cl->lastprofileupdate < 1000)
{
++cl->fastprofileupdates;
if(cl->fastprofileupdates == 3) sendservmsg("\f3Please do not spam");
// TODO: throttle/delay skin switches
}
else if(servmillis - cl->lastprofileupdate > 10000) cl->fastprofileupdates = 0;
cl->lastprofileupdate = servmillis;
break;
}
case SV_THIRDPERSON:
sendf(NULL, 1, "ri3x", SV_THIRDPERSON, sender, cl->acthirdperson = getint(p), sender);
break;
case SV_LEVEL:
sendf(NULL, 1, "ri3x", SV_LEVEL, sender, cl->level = clamp(getint(p), 1, MAXLEVEL), sender);
break;
case SV_TRYSPAWN:
{
clientstate &cs = cl->state;
if (cs.state == CS_WAITING) // dequeue spawn
{
cs.state = CS_DEAD;
sendf(cl, 1, "ri2", SV_TRYSPAWN, 0);
break;
}
if (!cl->isonrightmap || !maplayout) // need the map for spawning
{
//sendf(sender, 1, "ri", SV_MAPIDENT);
break;
}
if (cs.state != CS_DEAD || cs.lastspawn >= 0) break; // not dead or already enqueued
if (team_isspect(cl->team))
{
updateclientteam(*cl, chooseteam(*cl), FTR_PLAYERWISH);
checkai(); // spawn unspectate
// convertcheck();
}
// can the player be enqueued?
if (!canspawn(*cl)) break;
// enqueue for spawning
cs.state = CS_WAITING;
const int waitremain = SPAWNDELAY - gamemillis + cs.lastdeath;
sendf(cl, 1, "ri2", SV_TRYSPAWN, waitremain >= 1 ? waitremain : 1);
break;
}
case SV_SPAWN:
{
int cn = getint(p), ls = getint(p);
vec o;
loopi(3) o[i] = getint(p) / DMF;
if(!cl->hasclient(cn)) break;
client &cp = *clients[cn];
clientstate &cs = cp.state;
if((cs.state!=CS_ALIVE && cs.state!=CS_DEAD) || ls!=cs.lifesequence || cs.lastspawn<0) break;
cs.lastspawn = gamemillis; // extend spawn protection time
cs.state = CS_ALIVE;
vec lasto = cs.o;
cs.o = cp.spawnp = o;
cp.spj = cp.ldt = 40;
// send spawn packet, but not with QUEUE_BUF -- we need it sequenced
sendf(NULL, 1, "rxi3i6vvi4", sender, SV_SPAWN, cn, ls,
cs.health, cs.armour, cs.perk1, cs.perk2, cs.primary, cs.secondary,
NUMGUNS, cs.ammo, NUMGUNS, cs.mag, (int)(o.x*DMF), (int)(o.y*DMF), (int)(o.z*DMF), cl->y);
// bad spawn adjustment?
if (!m_edit(gamemode) && (lasto.distxy(cs.o) >= 6 * PLAYERRADIUS)) // || fabs(lasto.z - cs.o.z) >= 2 * PLAYERHEIGHT))
serverdied(cp, cp, 0, OBIT_SPAWN, FRAG_NONE, cs.o);
break;
}
case SV_SUICIDE:
{
const int cn = getint(p);
if(!cl->hasclient(cn)) break;
client *cp = clients[cn];
if(cp->state.state != CS_DEAD)
{
#if (SERVER_BUILTIN_MOD & 64)
if (cp->type != ST_AI && !cp->nuked)
{
cp->nuked = true;
nuke(*cp, true, true, true);
}
else
#endif
cp->suicide( cn == sender ? OBIT_DEATH : OBIT_BOT, cn == sender ? FRAG_GIB : FRAG_NONE);
}
break;
}
case SV_SHOOT: // cn id weap to.x to.y to.z heads.length heads.v
case SV_SHOOTC: // cn id weap
{
const int cn = getint(p), id = getint(p), weap = getint(p);
shotevent *ev = new shotevent(0, id, weap);
if (!(ev->compact = (type == SV_SHOOTC)))
{
loopi(3) ev->to[i] = getint(p) / DMF;
loopi(MAXCLIENTS)
{
posinfo info;
info.cn = getint(p);
if (info.cn < 0) break; // cannot hit self, so MAXCLIENTS should be the end
loopj(3) info.o[j] = getint(p) / DMF;
loopj(3) info.head[j] = getint(p) / DMF;
// reject duplicate positions
int k = 0;
for (k = 0; k < ev->pos.length(); k++)
if (ev->pos[k].cn == info.cn)
break;
// add if not found
if (k >= ev->pos.length())
ev->pos.add(info);
}
}
client *cp = cl->hasclient(cn) ? clients[cn] : NULL;
if (cp)
{
ev->millis = cp->getmillis(gamemillis, id);
cp->addevent(ev);
}
else delete ev;
break;
}
case SV_EXPLODE: // cn id weap flags x y z
{
const int cn = getint(p), id = getint(p), weap = getint(p), flags = getint(p);
vec o;
loopi(3) o[i] = getint(p) / DMF;
client *cp = cl->hasclient(cn) ? clients[cn] : NULL;
if (!cp) break;
cp->addevent(new destroyevent(cp->getmillis(gamemillis, id), id, weap, flags, o));
break;
}
case SV_AKIMBO:
{
const int cn = getint(p), id = getint(p);
if (!cl->hasclient(cn)) break;
client *cp = clients[cn];
cp->addevent(new akimboevent(cp->getmillis(gamemillis, id), id));
break;
}
case SV_RELOAD: // cn id weap
{
int cn = getint(p), id = getint(p), weap = getint(p);
if (!cl->hasclient(cn)) break;
client *cp = clients[cn];
cp->addevent(new reloadevent(cp->getmillis(gamemillis, id), id, weap));
break;
}
case SV_PINGPONG:
sendf(cl, 1, "ii", SV_PINGPONG, getint(p));
break;
case SV_CLIENTPING:
{
int ping = getint(p);
if(!cl) break;
ping = clamp(ping, 0, 9999);
cl->ping = cl->ping == 9999 ? ping : (cl->ping * 4 + ping) / 5;
sendf(NULL, 1, "i3", SV_CLIENTPING, sender, cl->ping);
break;
}
case SV_POS:
{
int cn = getint(p);
const bool broadcast = cl->hasclient(cn);
vec newo, dvel;
loopi(3) newo[i] = getuint(p)/DMF;
int newy = getuint(p);
int newp = getint(p);
int newg = getuint(p);
if ((newg >> 3) & 1) getint(p); // roll
if ((newg >> 0) & 1) dvel.x = getint(p) / DVELF;
if ((newg >> 1) & 1) dvel.y = getint(p) / DVELF;
if ((newg >> 2) & 1) dvel.z = getint(p) / DVELF;
int newf = getuint(p);
if(!valid_client(cn)) break;
client &cp = *clients[cn];
clientstate &cs = cp.state;
if(interm || !broadcast || (cs.state!=CS_ALIVE && cs.state!=CS_EDITING)) break;
// relay if still alive
if(!cp.isonrightmap || m_demo(gamemode) || !movechecks(cp, newo, newf, newg)) break;
cs.o = newo;
cs.vel.add(dvel);
cp.y = newy;
cp.p = newp;
cp.g = newg;
cp.f = newf;
checkmove(cp);
if(!isdedicated) break;
cp.position.setsize(0);
while(curmsg<p.length()) cp.position.add(p.buf[curmsg++]);
break;
}
case SV_POSC:
{
bitbuf<ucharbuf> q(p);
int cn = q.getbits(5);
const bool broadcast = cl->hasclient(cn);
int usefactor = q.getbits(2) + 7;
int xt = q.getbits(usefactor + 4);
int yt = q.getbits(usefactor + 4);
const int newy = (q.getbits(9)*360)/512;
const int newp = ((q.getbits(8)-128)*90)/127;
if(!q.getbits(1)) q.getbits(6);
vec dvel;
if (!q.getbits(1))
{
dvel.x = (q.getbits(4) - 8) / DVELF;
dvel.y = (q.getbits(4) - 8) / DVELF;
dvel.z = (q.getbits(4) - 8) / DVELF;
}
const int newf = q.getbits(8);
int negz = q.getbits(1);
int zfull = q.getbits(1);
int s = q.rembits();
if(s < 3) s += 8;
if(zfull) s = 11;
int zt = q.getbits(s);
if(negz) zt = -zt;
int g1 = q.getbits(1); // scoping
int g2 = q.getbits(1); // shooting
q.getbits(1); // sprinting
const int newg = (g1<<4) | (g2<<5);
if(!broadcast) break;
client &cp = *clients[cn];
clientstate &cs = cp.state;
if(!cp.isonrightmap || p.remaining() || p.overread()) { p.flags = 0; break; }
if(((newf >> 6) & 1) != (cs.lifesequence & 1) || usefactor != (smapstats.hdr.sfactor < 7 ? 7 : smapstats.hdr.sfactor)) break;
// relay if still alive
vec newo(xt / DMF, yt / DMF, zt / DMF);
if(m_demo(gamemode) || !movechecks(cp, newo, newf, newg)) break;
cs.o = newo;
cs.vel.add(dvel);
cp.y = newy;
cp.p = newp;
cp.f = newf;
cp.g = newg;
checkmove(cp);
if(!isdedicated) break;
cp.position.setsize(0);
while(curmsg<p.length()) cp.position.add(p.buf[curmsg++]);
break;
}
case SV_SENDMAP:
{
getstring(text, p);
filtertext(text, text);
const char *sentmap = behindpath(text), *reject = NULL;
int mapsize = getint(p);
int cfgsize = getint(p);
int cfgsizegz = getint(p);
int revision = getint(p);
if(p.remaining() < mapsize + cfgsizegz || MAXMAPSENDSIZE < mapsize + cfgsizegz)
{
p.forceoverread();
break;
}
int mp = findmappath(sentmap);
if(readonlymap(mp))
{
reject = "map is ro";
defformatstring(msg)("\f3map upload rejected: map %s is readonly", sentmap);
sendservmsg(msg, cl);
}
else if( scl.incoming_limit && ( scl.incoming_limit << 20 ) < incoming_size + mapsize + cfgsizegz )
{
reject = "server incoming reached its limits";
sendservmsg("\f3server does not support more incomings: limit reached", cl);
}
else if(mp == MAP_NOTFOUND && strchr(scl.mapperm, 'C') && cl->role < CR_ADMIN)
{
reject = "no permission for initial upload";
sendservmsg("\f3initial map upload rejected: you need to be admin", cl);
}
else if(mp == MAP_TEMP && revision >= mapbuffer.revision && !strchr(scl.mapperm, 'u') && cl->role < CR_ADMIN) // default: only admins can update maps
{
reject = "no permission to update";
sendservmsg("\f3map update rejected: you need to be admin", cl);
}
else if(mp == MAP_TEMP && revision < mapbuffer.revision && !strchr(scl.mapperm, 'r') && cl->role < CR_ADMIN) // default: only admins can revert maps to older revisions
{
reject = "no permission to revert revision";
sendservmsg("\f3map revert to older revision rejected: you need to be admin to upload an older map", cl);
}
else
{
if(mapbuffer.sendmap(sentmap, mapsize, cfgsize, cfgsizegz, &p.buf[p.len]))
{
incoming_size += mapsize + cfgsizegz;
logline(ACLOG_INFO,"[%s] %s sent map %s, rev %d, %d + %d(%d) bytes written",
clients[sender]->gethostname(), clients[sender]->formatname(), sentmap, revision, mapsize, cfgsize, cfgsizegz);
defformatstring(msg)("%s (%d) up%sed map %s, rev %d%s", clients[sender]->formatname(), sender, mp == MAP_NOTFOUND ? "load": "dat", sentmap, revision,
/*strcmp(sentmap, behindpath(smapname)) || smode == GMODE_COOPEDIT ? "" :*/ "\f3 (restart game to use new map version)");
sendservmsg(msg);
}
else
{
reject = "write failed (no 'incoming'?)";
sendservmsg("\f3map upload failed", cl);
}
}
if (reject)
{
logline(ACLOG_INFO,"[%s] %s sent map %s rev %d, rejected: %s",
clients[sender]->gethostname(), clients[sender]->formatname(), sentmap, revision, reject);
}
p.len += mapsize + cfgsizegz;
break;
}
case SV_RECVMAP:
{
if(mapbuffer.available())
{
resetflag(cl->clientnum); // drop ctf flag
savedscore *sc = findscore(*cl, true); // save score
if(sc) sc->save(cl->state, cl->team);
mapbuffer.sendmap(cl, 2);
cl->mapchange(true);
sendwelcome(*cl, 2); // resend state properly
}
#ifndef STANDALONE
else if(cl->type == ST_LOCAL && !cl->isonrightmap)
{
cl->isonrightmap = true;
conoutf("isonrightmap has been reset to true");
}
#endif
else sendservmsg("no map to get", cl);
break;
}
case SV_REMOVEMAP:
{
getstring(text, p);
filtertext(text, text);
string filename;
const char *rmmap = behindpath(text), *reject = NULL;
int mp = findmappath(rmmap);
int reqrole = strchr(scl.mapperm, 'D') ? CR_ADMIN : (strchr(scl.mapperm, 'd') ? CR_DEFAULT : CR_ADMIN + 100);
if(cl->role < reqrole) reject = "no permission";
else if(readonlymap(mp)) reject = "map is readonly";
else if(mp == MAP_NOTFOUND) reject = "map not found";
else
{
formatstring(filename)(SERVERMAP_PATH_INCOMING "%s.cgz", rmmap);
remove(filename);
formatstring(filename)(SERVERMAP_PATH_INCOMING "%s.cfg", rmmap);
remove(filename);
defformatstring(msg)("map '%s' deleted", rmmap);
sendservmsg(msg, cl);
logline(ACLOG_INFO, "[%s] deleted map %s", clients[sender]->gethostname(), rmmap);
}
if (reject)
{
logline(ACLOG_INFO, "[%s] deleting map %s failed: %s", clients[sender]->gethostname(), rmmap, reject);
defformatstring(msg)("\f3can't delete map '%s', %s", rmmap, reject);
sendservmsg(msg, cl);
}
break;
}
case SV_DROPFLAG:
{
int fl = clienthasflag(sender);
flagaction(fl, FA_DROP, sender);
/*
while(fl >= 0)
{
flagaction(fl, FA_DROP, sender);
fl = clienthasflag(sender);
}
*/
break;
}
case SV_CLAIMPRIV: // claim
{
getstring(text, p);
pwddetail pd;
pd.line = -1;
if (cl->type == ST_LOCAL) setpriv(*cl, CR_MAX);
else if (!passwords.check(cl->name, text, cl->salt, &pd))
{
if (cl->authpriv >= CR_MASTER)
{
logline(ACLOG_INFO, "[%s] %s was already authed for %s", cl->gethostname(), cl->formatname(), privname(cl->authpriv));
setpriv(*cl, cl->authpriv);
}
else if (cl->role < CR_ADMIN && text[0])
{
disconnect_client(*cl, DISC_SOPLOGINFAIL); // avoid brute-force
return;
}
}
else if (!pd.priv)
{
sendf(cl, 1, "ri4", SV_CLAIMPRIV, sender, 0, 2);
if (pd.line >= 0) logline(ACLOG_INFO, "[%s] %s used non-privileged password on line %d", cl->gethostname(), cl->formatname(), pd.line);
}
else
{
setpriv(*cl, pd.priv);
if (pd.line >= 0) logline(ACLOG_INFO, "[%s] %s used %s password on line %d", cl->gethostname(), cl->formatname(), privname(pd.priv), pd.line);
}
break;
}
case SV_SETPRIV: // relinquish
setpriv(*cl, CR_DEFAULT);
break;
case SV_AUTH_ACR_CHAL:
{
uchar info[48+32];
p.get(info, 48+32);
bool answered = answerchallenge(*cl, &info[0], &info[48]);
if (cl->authreq && answered) cl->isauthed = true;
else checkauthdisc(*cl);
break;
}
case SV_CALLVOTE:
{
voteinfo *vi = new voteinfo;
vi->type = getint(p);
switch(vi->type)
{
case SA_MAP:
{
int muts = getint(p), mode = getint(p);
getstring(text, p);
filtertext(text, text);
if (text[0] == '+' && text[1] == '1')
{
if (nextmapname[0])
{
copystring(text, nextmapname);
mode = nextgamemode;
muts = nextmutators;
}
else
{
#ifdef STANDALONE
int ccs = mode ? maprot.next(false, false) : maprot.get_next();
configset *c = maprot.get(ccs);
if (c)
{
strcpy(text, c->mapname);
mode = c->mode;
muts = c->muts;
}
else fatal("unable to get next map in maprot");
#else
defformatstring(nextmapalias)("nextmap_%s", getclientmap());
copystring(text, getalias(nextmapalias));
mode = getclientmode();
muts = mutators;
#endif
}
}
int qmode = (mode >= G_MAX ? mode - G_MAX : mode);
modecheck(qmode, muts);
if(m_demo(mode)) vi->action = new demoplayaction(newstring(text));
else
{
char *vmap = newstring(text[0] ? behindpath(text) : "");
vi->action = new mapaction(vmap, qmode, muts, sender, qmode!=mode);
}
break;
}
case SA_KICK:
{
int cn = getint(p);
getstring(text, p);
filtertext(text, text);
trimtrailingwhitespace(text);
vi->action = new kickaction(cn, text, cn == sender);
break;
}
case SA_BAN:
{
int m = getint(p), cn = getint(p);
getstring(text, p);
m = clamp(m, 1, 60);
if (cl->role < CR_ADMIN && m >= 10) m = 10;
filtertext(text, text);
trimtrailingwhitespace(text);
vi->action = new banaction(cn, m, text, cn == sender);
break;
}
case SA_REMBANS:
vi->action = new removebansaction();
break;
case SA_MASTERMODE:
vi->action = new mastermodeaction(getint(p));
break;
case SA_AUTOTEAM:
vi->action = new autoteamaction(getint(p) > 0);
break;
case SA_FORCETEAM:
{
int team = getint(p), cn = getint(p);
vi->action = new forceteamaction(cn, sender, team);
break;
}
case SA_GIVEADMIN:
{
int role = getint(p), cn = getint(p);
vi->action = new giveadminaction(cn, role, sender);
break;
}
// case SA_MAP:
case SA_RECORDDEMO:
vi->action = new recorddemoaction(getint(p) != 0);
break;
case SA_STOPDEMO:
// compatibility
break;
case SA_CLEARDEMOS:
vi->action = new cleardemosaction(getint(p));
break;
case SA_SERVERDESC:
getstring(text, p);
filtertext(text, text);
vi->action = new serverdescaction(newstring(text), sender);
break;
case SA_SHUFFLETEAMS:
vi->action = new shuffleteamaction();
break;
case SA_BOTBALANCE:
vi->action = new botbalanceaction(getint(p));
break;
case SA_SUBDUE:
vi->action = new subdueaction(getint(p));
break;
case SA_REVOKE:
vi->action = new revokeaction(getint(p));
break;
default:
vi->type = SA_KICK;
vi->action = new kickaction(-1, "<invalid type placeholder>", false);
break;
}
vi->owner = sender;
vi->callmillis = servmillis;
if(!scallvote(*vi)) delete vi;
break;
}
case SV_VOTE:
{
int vote = getint(p);
if (!curvote || !curvote->action || vote < 0 || vote >= VOTE_NUM) break;
if (cl->vote == vote)
{
if (vote == VOTE_NEUTRAL)
break;
// try to veto
if (cl->role >= curvote->action->reqcall && cl->role >= curvote->action->reqveto)
curvote->evaluate(true, vote, sender);
else
sendf(cl, 1, "ri2", SV_CALLVOTEERR, VOTEE_VETOPERM);
break;
}
logline(ACLOG_INFO, "[%s] %s %s %s",
cl->gethostname(),
cl->formatname(),
cl->vote == VOTE_NEUTRAL ? "voted" : "changed vote to",
vote == VOTE_NEUTRAL ? "neutral" : vote == VOTE_NO ? "no" : "yes");
cl->vote = vote;
sendf(NULL, 1, "ri3", SV_VOTE, sender, vote);
curvote->evaluate();
break;
}
case SV_WHOIS:
{
const int cn = getint(p);
if (!valid_client(cn) || clients[cn]->type != ST_TCPIP) break;
sendf(NULL, 1, "ri4", SV_WHOIS, -1, sender, cn);
uchar ipv6[16];
int mask = 0;
if (true) // if (IPv4)
{
uint ipv4 = clients[cn]->peer->address.host;
if (cn == sender) mask = 32;
else switch (clients[sender]->role)
{
// admins and server owner: f.f.f.f/32 full ip
case CR_MAX: case CR_ADMIN: mask = 32; break;
// masters and users: f.f.h/12 full, full, half, empty
case CR_MASTER: case CR_DEFAULT: default: mask = 20; break;
}
if (mask < 32) ipv4 &= (1 << mask) - 1;
memset(ipv6, 0, sizeof(char) * 10);
ipv6[10] = 0xFF;
ipv6[11] = 0xFF;
ipv6[12] = ipv4 & 0xFF;
ipv4 >>= 8;
ipv6[13] = ipv4 & 0xFF;
ipv4 >>= 8;
ipv6[14] = ipv4 & 0xFF;
ipv4 >>= 8;
ipv6[15] = ipv4 & 0xFF;
mask += 96;
}
sendf(cl, 1, "ri2mi2s", SV_WHOIS, cn, 16, ipv6, mask, clients[cn]->peer->address.port, clients[cn]->authname);
break;
}
case SV_LISTDEMOS:
listdemos(cl);
break;
case SV_GETDEMO:
senddemo(*cl, getint(p));
break;
case SV_SOUND:
{
int cn = getint(p), snd = getint(p);
if (!cl->hasclient(cn)) break;
switch(snd)
{
case S_NOAMMO:
if (!clients[cn]->state.mag[clients[cn]->state.gunselect])
QUEUE_MSG;
break;
case S_JUMP:
#if (SERVER_BUILTIN_MOD & 1)
// native moonjump for humans
#if !(SERVER_BUILTIN_MOD & 2)
if (m_gib(gamemode, mutators))
#endif
if (snd == S_JUMP && cn == sender)
{
sendf(NULL, 1, "ri8i3", SV_DAMAGE, cn, cn, 200 * HEALTHSCALE, clients[cn]->state.armour, clients[cn]->state.health, GUN_KNIFE, FRAG_GIB, (int)(clients[cn]->state.o.x*DMF), (int)(clients[cn]->state.o.y*DMF), INT_MIN);
sendf(NULL, 1, "ri8i3", SV_DAMAGE, cn, cn, 0, clients[cn]->state.armour, clients[cn]->state.health, GUN_HEAL, FRAG_NONE, 0, 0, 0);
}
#endif
// intentional fallthrough
case S_SOFTLAND:
case S_HARDLAND:
QUEUE_MSG;
break;
}
break;
}
case SV_EXTENSION:
{
// AC server extensions
//
// rules:
// 1. extensions MUST NOT modify gameplay or the behavior of the game in any way
// 2. extensions may ONLY be used to extend or automate server administration tasks
// 3. extensions may ONLY operate on the server and must not send any additional data to the connected clients
// 4. extensions not adhering to these rules may cause the hosting server being banned from the masterserver
//
// also note that there is no guarantee that custom extensions will work in future AC versions
getstring(text, p, 64);
char *ext = text; // extension specifier in the form of OWNER::EXTENSION, see sample below
int n = getint(p); // length of data after the specifier
if(n < 0 || n > 50) return;
// sample
if(!strcmp(ext, "driAn::writelog"))
{
// owner: driAn - root@sprintf.org
// extension: writelog - WriteLog v1.0
// description: writes a custom string to the server log
// access: requires admin privileges
// usage: /serverextension driAn::writelog "your log message here.."
// note: There is a 49 character limit. The server will ignore messages with 50+ characters.
getstring(text, p, n);
if(valid_client(sender) && clients[sender]->role>=CR_ADMIN)
{
logline(ACLOG_INFO, "[%s] %s writes to log: %s", cl->gethostname(), cl->formatname(), text);
sendservmsg("your message has been logged", cl);
}
}
else if(!strcmp(ext, "set::teamsize"))
{
// intermediate solution to set the teamsize (will be voteable)
getstring(text, p, n);
if(valid_client(sender) && clients[sender]->role>=CR_ADMIN && mastermode == MM_MATCH)
{
changematchteamsize(atoi(text));
defformatstring(msg)("match team size set to %d", matchteamsize);
sendservmsg(msg);
}
}
// else if()
// add other extensions here
else for(; n > 0; n--) getint(p); // ignore unknown extensions
break;
}
// Edit messages
case SV_EDITH:
case SV_EDITT:
case SV_EDITS:
case SV_EDITD:
case SV_EDITE:
{
int x = getint(p);
int y = getint(p);
int xs = getint(p);
int ys = getint(p);
int v = getint(p);
switch (type)
{
#define seditloop(body) \
{ \
const int ssize = 1 << maplayout_factor; /* borrow the OUTBORD macro */ \
loop(xx, xs) loop(yy, ys) if (!OUTBORD(x + xx, y + yy)) \
{ \
const int id = getmaplayoutid(x + xx, y + yy); \
body \
} \
}
case SV_EDITH:
{
int offset = getint(p);
seditloop({
if (!v) // ceil
{
getsblock(id).ceil += offset;
if (getsblock(id).ceil <= getsblock(id).floor)
getsblock(id).ceil = getsblock(id).floor + 1;
}
else // floor
{
getsblock(id).floor += offset;
if (getsblock(id).floor >= getsblock(id).ceil)
getsblock(id).floor = getsblock(id).ceil - 1;
}
});
break;
}
case SV_EDITS:
{
seditloop({ getsblock(id).type = v; });
break;
}
case SV_EDITD:
{
seditloop({
getsblock(id).vdelta += v;
if (getsblock(id).vdelta < 0)
getsblock(id).vdelta = 0;
});
break;
}
case SV_EDITE:
{
int low = 127, hi = -128;
seditloop({
if (getsblock(id).floor<low) low = getsblock(id).floor;
if (getsblock(id).ceil>hi) hi = getsblock(id).ceil;
});
seditloop({
if (!v) getsblock(id).ceil = hi; else getsblock(id).floor = low;
if (getsblock(id).floor >= getsblock(id).ceil) getsblock(id).floor = getsblock(id).ceil - 1;
});
break;
}
// ignore texture
case SV_EDITT: getint(p); break;
}
QUEUE_MSG;
break;
}
case SV_EDITW:
// set water level
smapstats.hdr.waterlevel = getint(p);
// water color alpha
loopi(4) getint(p);
QUEUE_MSG;
break;
case SV_EDITMODE:
{
const bool editing = getint(p) != 0;
if (cl->state.state != (editing ? CS_ALIVE : CS_EDITING)) break;
if (!m_edit(gamemode) && editing && cl->type == ST_TCPIP)
{
// unacceptable!
cl->cheat("tried editmode");
break;
}
cl->state.state = editing ? CS_EDITING : CS_ALIVE;
cl->state.onfloor = true; // prevent falling damage
//cl->state.allowspeeding(gamemillis, 1000); // prevent speeding detection
QUEUE_MSG;
break;
}
case SV_EDITENT:
{
const int id = getint(p), type = getint(p);
vec o;
loopi(3) o[i] = getint(p);
int attr1 = getint(p), attr2 = getint(p), attr3 = getint(p), attr4 = getint(p);
while(sents.length() <= id) sents.add().type = NOTUSED;
entity &e = sents[max(id, 0)];
// server entity
e.type = type;
e.transformtype(smode, smuts);
e.x = o.x;
e.y = o.y;
e.z = o.z;
e.attr1 = attr1;
e.attr2 = attr2;
e.attr3 = attr3;
e.attr4 = attr4;
// is it spawned?
if((e.spawned = e.fitsmode(smode, smuts)))
sendf(NULL, 1, "ri2", SV_ITEMSPAWN, id);
e.spawntime = 0;
QUEUE_MSG;
break;
}
case SV_NEWMAP: // the server needs to create a new layout
{
const int size = getint(p);
if (size == -2) --maplayout_factor;
else if(size < 0) ++maplayout_factor;
else maplayout_factor = size;
// move ents? for enlarge/shrink
DELETEA(maplayout)
if(maplayout_factor >= 0) snewmap(maplayout_factor);
QUEUE_MSG;
break;
}
default:
case -1:
disconnect_client(*cl, DISC_TAGT);
return;
case -2:
disconnect_client(*cl, DISC_OVERFLOW);
return;
}
}
if (p.overread() && sender >= 0) disconnect_client(*cl, DISC_EOP);
#ifdef _DEBUG
protocoldebug(false);
#endif
}
void localclienttoserver(int chan, ENetPacket *packet)
{
process(packet, 0, chan);
}
client &addclient()
{
client *c = NULL;
loopv(clients)
{
if(clients[i]->type==ST_EMPTY) { c = clients[i]; break; }
else if(clients[i]->type==ST_AI) { deleteai(*clients[i]); c = clients[i]; break; }
}
if(!c)
{
c = new client;
c->clientnum = clients.length();
c->ownernum = -1;
clients.add(c);
}
c->reset();
return *c;
}
void checkintermission()
{
if(minremain>0)
{
minremain = (gamemillis>=gamelimit || forceintermission) ? 0 : (gamelimit - gamemillis + 60000 - 1)/60000;
sendf(NULL, 1, "ri3", SV_TIMEUP, (gamemillis>=gamelimit || forceintermission) ? gamelimit : gamemillis, gamelimit);
}
if(!interm && minremain<=0) interm = gamemillis+10000;
forceintermission = false;
}
void resetserverifempty()
{
loopv(clients) if(clients[i]->type!=ST_EMPTY) return;
resetserver("", G_DM, G_M_NONE, 10);
matchteamsize = 0;
#ifdef STANDALONE
botbalance = -1;
#else
botbalance = 0;
#endif
autoteam = true;
changemastermode(MM_OPEN);
nextmapname[0] = '\0';
savedlimits.shrink(0);
}
void sendworldstate()
{
static enet_uint32 lastsend = 0;
if(clients.empty()) return;
enet_uint32 curtime = enet_time_get()-lastsend;
if(curtime<40) return;
bool flush = buildworldstate();
lastsend += curtime - (curtime%40);
if(flush) enet_host_flush(serverhost);
if(demorecord) recordpackets = true; // enable after 'old' worldstate is sent
}
void rereadcfgs(void)
{
maprot.read();
ipblacklist.read();
nickblacklist.read();
forbiddenlist.read();
passwords.read();
}
void loggamestatus(const char *reason)
{
int fragscore[2] = {0, 0}, flagscore[2] = {0, 0}, pnum[2] = {0, 0};
string text;
formatstring(text)("%d minutes remaining", minremain);
logline(ACLOG_INFO, "");
logline(ACLOG_INFO, "Game status: %s on %s, %s, %s, %d clients%c %s",
modestr(gamemode, mutators), smapname, reason ? reason : text, mmfullname(mastermode), totalclients, custom_servdesc ? ',' : '\0', servdesc_current);
if(!scl.loggamestatus) return;
logline(ACLOG_INFO, "cn name %s%s score frag death ping role host", m_team(gamemode, mutators) ? "team " : "", m_flags(gamemode) ? "flag " : "");
loopv(clients)
{
client &c = *clients[i];
if(c.type == ST_EMPTY || !c.name[0]) continue;
const bool bot = c.type == ST_AI;
formatstring(text)("%-3d %-16s ", c.clientnum, c.name); // cn, name
if(m_team(gamemode, mutators)) concatformatstring(text, "%-4s ", team_string(c.team, true)); // teamname (abbreviated)
if(m_flags(gamemode)) concatformatstring(text, "%4d ", c.state.flagscore); // flag
concatformatstring(text, "%6d ", c.state.points); // score
concatformatstring(text, "%4d %5d", c.state.frags, c.state.deaths); // frag death
const int ping = bot && valid_client(c.ownernum) ? clients[c.ownernum]->ping : c.ping;
if(bot)
logline(ACLOG_INFO, "%s%5d %-7s bot:%d", text, ping, privname(c.role), c.ownernum);
else
logline(ACLOG_INFO, "%s%5d %-7s %s", text, ping, privname(c.role), c.hostname);
if(c.team != TEAM_SPECT)
{
int t = team_base(c.team);
flagscore[t] += c.state.flagscore;
fragscore[t] += c.state.frags;
pnum[t] += 1;
}
}
if(mastermode == MM_MATCH)
{
loopv(savedscores)
{
savedscore &sc = savedscores[i];
if(sc.valid)
{
formatstring(text)(m_team(gamemode, mutators) ? "%-4s " : "", team_string(sc.team, true));
if(m_flags(gamemode)) concatformatstring(text, "%4d ", sc.flagscore);
logline(ACLOG_INFO, " %-16s %s%4d %5d%s - disconnected", sc.name, text, sc.frags, sc.deaths, m_team(gamemode, mutators) ? " -" : "");
if(sc.team != TEAM_SPECT)
{
int t = team_base(sc.team);
flagscore[t] += sc.flagscore;
fragscore[t] += sc.frags;
pnum[t] += 1;
}
}
}
}
if(m_team(gamemode, mutators))
{
loopi(2) logline(ACLOG_INFO, "Team %4s:%3d players,%5d frags%c%5d flags", team_string(i), pnum[i], fragscore[i], m_flags(gamemode) ? ',' : '\0', flagscore[i]);
}
logline(ACLOG_INFO, "");
}
static unsigned char chokelog[MAXCLIENTS + 1] = { 0 };
void linequalitystats(int elapsed)
{
static unsigned int chokes[MAXCLIENTS + 1] = { 0 }, spent[MAXCLIENTS + 1] = { 0 }, chokes_raw[MAXCLIENTS + 1] = { 0 }, spent_raw[MAXCLIENTS + 1] = { 0 };
if(elapsed)
{ // collect data
int c1 = 0, c2 = 0, r1 = 0, numc = 0;
loopv(clients)
{
client &c = *clients[i];
if(c.type != ST_TCPIP) continue;
numc++;
enet_uint32 &rtt = c.peer->lastRoundTripTime, &throttle = c.peer->packetThrottle;
if(rtt < c.bottomRTT + c.bottomRTT / 3)
{
if(servmillis - c.connectmillis < 5000)
c.bottomRTT = rtt;
else
c.bottomRTT = (c.bottomRTT * 15 + rtt) / 16; // simple IIR
}
if(throttle < 22) c1++;
if(throttle < 11) c2++;
if(rtt > c.bottomRTT * 2 && rtt - c.bottomRTT > 300) r1++;
}
spent_raw[numc] += elapsed;
int t = numc < 7 ? numc : (numc + 1) / 2 + 3;
chokes_raw[numc] += ((c1 >= t ? c1 + c2 : 0) + (r1 >= t ? r1 : 0)) * elapsed;
}
else
{ // calculate compressed statistics
defformatstring(msg)("Uplink quality [ ");
int ncs = 0;
loopj(scl.maxclients)
{
int i = j + 1;
int nc = chokes_raw[i] / 1000 / i;
chokes[i] += nc;
ncs += nc;
spent[i] += spent_raw[i] / 1000;
chokes_raw[i] = spent_raw[i] = 0;
int s = 0, c = 0;
if(spent[i])
{
frexp((double)spent[i] / 30, &s);
if(s < 0) s = 0;
if(s > 15) s = 15;
if(chokes[i])
{
frexp(((double)chokes[i]) / spent[i], &c);
c = 15 + c;
if(c < 0) c = 0;
if(c > 15) c = 15;
}
}
chokelog[i] = (s << 4) + c;
concatformatstring(msg, "%02X ", chokelog[i]);
}
logline(ACLOG_DEBUG, "%s] +%d", msg, ncs);
}
}
void serverslice(uint timeout) // main server update, called from cube main loop in sp, or dedicated server loop
{
static int msend = 0, mrec = 0, csend = 0, crec = 0, mnum = 0, cnum = 0;
#ifdef STANDALONE
int nextmillis = (int)enet_time_get();
if(svcctrl) svcctrl->keepalive();
#else
int nextmillis = isdedicated ? (int)enet_time_get() : lastmillis;
#endif
int diff = nextmillis - servmillis;
gamemillis += diff;
servmillis = nextmillis;
servertime = ((diff + 3 * servertime)>>2);
if (servertime > 40) serverlagged = servmillis;
#ifndef STANDALONE
if(m_demo(gamemode))
{
readdemo();
extern void silenttimeupdate(int milliscur, int millismax);
silenttimeupdate(gamemillis, gametimemaximum);
}
#endif
if(minremain>0)
{
processevents();
checkitemspawns(diff);
bool ktfflagingame = false;
loopi(2) if ((!i || m_team(gamemode, mutators)) && !steamscores[i].valid) sendteamscore(i);
loopv(clients) if (valid_client(i) && !clients[i]->state.valid)
{
sendf(NULL, 1, "ri9", SV_SCORE, i,
clients[i]->state.points,
clients[i]->state.flagscore,
clients[i]->state.frags,
clients[i]->state.assists,
clients[i]->state.deaths,
clients[i]->state.pointstreak,
clients[i]->state.deathstreak);
clients[i]->state.valid = true;
}
if (m_flags(gamemode))
{
if (m_secure(gamemode))
{
loopv(ssecures)
{
// service around every 40 milliseconds, for the best (25 fps)
// 10000ms / 255 units = ~39.2 ms / unit
int sec_diff = (gamemillis - ssecures[i].last_service) / 39;
if (!sec_diff) continue;
ssecures[i].last_service += sec_diff * 39;
if (!m_gsp1(gamemode, mutators)) sec_diff *= 2; // secure faster if non-direct
int teams_inside[2] = { 0 };
loopvj(clients)
if (valid_client(j) && (clients[j]->team >= 0 && clients[j]->team < 2) && clients[j]->state.state == CS_ALIVE && clients[j]->state.o.dist(ssecures[i].o) <= 8.f + PLAYERRADIUS)
++teams_inside[clients[j]->team];
const int returnbonus = ssecures[i].team == TEAM_SPECT ? 1 : m_gsp1(gamemode, mutators) ? 2 : 0; // how fast flags can return to its original owner, but 0 counts as 1 if there is a defender
int defending = 0, opposing = 0;
loopj(2)
{
if (j == ssecures[i].enemy || ssecures[i].enemy == TEAM_SPECT) opposing += teams_inside[j];
else defending += teams_inside[j];
}
if (opposing > defending)
{
// starting to secure/overthrow?
if (ssecures[i].enemy == TEAM_SPECT)
{
int team_max = 0, max_team = TEAM_SPECT;
bool teams_matched = true;
loopj(2) // prepared for more teams
{
if (teams_inside[j] > team_max)
{
team_max = teams_inside[j];
max_team = j;
teams_matched = false;
}
else if (teams_inside[j] == team_max) teams_matched = true;
}
// first frame: start to capture, but we don't know how many units to give
if (!teams_matched && max_team != ssecures[i].team) ssecures[i].enemy = max_team;
}
else
{
// securing/overthrowing
ssecures[i].overthrown += sec_diff * (opposing - defending);
if (ssecures[i].overthrown >= 255)
{
const bool is_secure = ssecures[i].team == TEAM_SPECT || m_gsp1(gamemode, mutators);
loopvj(clients)
if (valid_client(j) && clients[j]->team == ssecures[i].enemy && clients[j]->state.state == CS_ALIVE && clients[j]->state.o.dist(ssecures[i].o) <= 8.f + PLAYERRADIUS)
{
addpt(*clients[j], SECUREPT, is_secure ? PR_SECURE_SECURE : PR_SECURE_OVERTHROW);
clients[j]->state.invalidate().flagscore += m_gsp1(gamemode, mutators) ? ssecures[i].team == TEAM_SPECT ? 2 : 3 : 1;
}
ssecures[i].team = is_secure ? ssecures[i].enemy : TEAM_SPECT;
if (is_secure)
{
ssecures[i].enemy = TEAM_SPECT;
ssecures[i].overthrown = 0;
}
else ssecures[i].overthrown = max(1, ssecures[i].overthrown - 255);
}
sendsecureflaginfo(&ssecures[i]);
}
}
else if ((defending > opposing || (!opposing && returnbonus)) && ssecures[i].overthrown)
{
// going back to the original owner
ssecures[i].overthrown -= sec_diff * (max(1, returnbonus) + defending - opposing);
if (ssecures[i].overthrown <= 0)
{
ssecures[i].enemy = TEAM_SPECT;
ssecures[i].overthrown = 0;
}
sendsecureflaginfo(&ssecures[i]);
}
// else: we are at an impasse
}
static int lastsecurereward = 0;
if (servmillis > lastsecurereward + 5000)
{
// reward points for having some bases secured
lastsecurereward = servmillis;
int bonuses[2] = { 0 };
loopv(ssecures)
if (ssecures[i].team >= 0 && ssecures[i].team < 2)
{
++bonuses[ssecures[i].team];
++usesteamscore(ssecures[i].team).flagscore;
}
loopv(clients)
if (valid_client(i) && (clients[i]->team >= 0 && clients[i]->team < 2) && bonuses[clients[i]->team])
addpt(*clients[i], SECUREDPT * bonuses[clients[i]->team], PR_SECURE_SECURED);
}
}
else if (m_overload(gamemode))
{
loopi(2)
{
// same service time as secure flags
sflaginfo &f = sflaginfos[i];
int sec_diff = (gamemillis - f.lastupdate) / 39;
if (!sec_diff) continue;
if (f.damage && gamemillis > f.damagetime + (m_gsp1(gamemode, mutators) ? 3000 : 5000))
{
if ((f.damage -= sec_diff * (m_gsp1(gamemode, mutators) ? 1250 : 750)) < 0)
f.damage = 0;
sendf(NULL, 1, "ri3", SV_FLAGOVERLOAD, i, 255 - f.damage / 1000);
}
else if (f.damagetime > f.lastupdate)
sendf(NULL, 1, "ri3", SV_FLAGOVERLOAD, i, 255 - f.damage / 1000);
f.lastupdate += sec_diff * 39;
}
}
else
{
loopi(2)
{
sflaginfo &f = sflaginfos[i];
if (f.state == CTFF_DROPPED && gamemillis - f.lastupdate > (m_capture(gamemode) ? 30000 : 10000)) flagaction(i, FA_RESET, -1);
if (m_hunt(gamemode) && f.state == CTFF_INBASE && gamemillis - f.lastupdate > (smapstats.flags[0] && smapstats.flags[1] ? 10000 : 1000))
htf_forceflag(i);
if (m_keep(gamemode) && f.state == CTFF_STOLEN && gamemillis - f.lastupdate > 15000)
flagaction(i, FA_SCORE, -1);
if (f.state == CTFF_INBASE || f.state == CTFF_STOLEN) ktfflagingame = true;
}
}
}
if(m_keep(gamemode) && !ktfflagingame) flagaction(rnd(2), FA_RESET, -1); // ktf flag watchdog
arenacheck();
convertcheck();
if ( scl.afk_limit && mastermode == MM_OPEN && next_afk_check < servmillis && gamemillis > 20 * 1000 ) check_afk();
}
if(curvote)
{
if(!curvote->isalive()) curvote->evaluate(true);
if(curvote->result!=VOTE_NEUTRAL) DELETEP(curvote);
}
int nonlocalclients = numnonlocalclients();
if(forceintermission || ((smode>1 || (gamemode==0 && nonlocalclients)) && gamemillis-diff>0 && gamemillis/60000!=(gamemillis-diff)/60000))
checkintermission();
if(m_demo(gamemode) && !demoplayback) maprot.restart();
else if(interm && ( (scl.demo_interm && sending_demo) ? gamemillis>(interm<<1) : gamemillis>interm ) )
{
sending_demo = false;
loggamestatus("game finished");
if(demorecord) enddemorecord();
interm = nextsendscore = 0;
//start next game
if(nextmapname[0]) startgame(nextmapname, nextgamemode, nextmutators);
else maprot.next();
nextmapname[0] = '\0';
}
resetserverifempty();
if(!isdedicated) return; // below is network only
serverms(smode, smuts, numplayers(false), minremain, smapname, servmillis, serverhost->address, &mnum, &msend, &mrec, &cnum, &csend, &crec, SERVER_PROTOCOL_VERSION);
if (autobalance && m_team(gamemode, mutators) && !m_zombie(gamemode) && !m_duke(gamemode, mutators) && !interm && servmillis - lastfillup > 5000 && refillteams()) lastfillup = servmillis;
loopv(clients)
{
client &cl = *clients[i];
if (cl.type == ST_TCPIP && (!cl.isauthed || cl.connectauth) && cl.connectmillis + 10000 <= servmillis)
disconnect_client(cl, DISC_TIMEOUT);
}
static unsigned int lastThrottleEpoch = 0;
if(serverhost->bandwidthThrottleEpoch != lastThrottleEpoch)
{
if(lastThrottleEpoch) linequalitystats(serverhost->bandwidthThrottleEpoch - lastThrottleEpoch);
lastThrottleEpoch = serverhost->bandwidthThrottleEpoch;
}
if(servmillis>nextstatus) // display bandwidth stats, useful for server ops
{
nextstatus = servmillis + 60 * 1000;
rereadcfgs();
if(nonlocalclients || serverhost->totalSentData || serverhost->totalReceivedData)
{
if(nonlocalclients) loggamestatus(NULL);
logline(ACLOG_INFO, "Status at %s: %d remote clients, %.1f send, %.1f rec (K/sec);"
" Ping: #%d|%d|%d; CSL: #%d|%d|%d (bytes)",
timestring(true, "%d-%m-%Y %H:%M:%S"), nonlocalclients, serverhost->totalSentData/60.0f/1024, serverhost->totalReceivedData/60.0f/1024,
mnum, msend, mrec, cnum, csend, crec);
mnum = msend = mrec = cnum = csend = crec = 0;
linequalitystats(0);
}
serverhost->totalSentData = serverhost->totalReceivedData = 0;
}
ENetEvent event;
bool serviced = false;
while(!serviced)
{
if(enet_host_check_events(serverhost, &event) <= 0)
{
if(enet_host_service(serverhost, &event, timeout) <= 0) break;
serviced = true;
}
switch(event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
client &c = addclient();
c.type = ST_TCPIP;
c.peer = event.peer;
c.peer->data = (void *)(size_t)c.clientnum;
c.connectmillis = servmillis;
c.state.state = CS_DEAD;
c.salt = (rnd(0x1000000)*((servmillis%1000)+1)) ^ randomMT();
char hn[1024];
copystring(c.hostname, (enet_address_get_host_ip(&c.peer->address, hn, sizeof(hn))==0) ? hn : "unknown");
logline(ACLOG_INFO, "[%s] client connected", c.gethostname());
sendservinfo(c);
totalclients++;
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
int cn = (int)(size_t)event.peer->data;
if(valid_client(cn)) process(event.packet, cn, event.channelID);
if(event.packet->referenceCount==0) enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
int cn = (int)(size_t)event.peer->data;
if(!valid_client(cn)) break;
disconnect_client(*clients[cn]);
break;
}
default:
break;
}
}
sendworldstate();
}
void cleanupserver()
{
if(serverhost) { enet_host_destroy(serverhost); serverhost = NULL; }
if(svcctrl)
{
svcctrl->stop();
DELETEP(svcctrl);
}
exitlogging();
}
int getpongflags(enet_uint32 ip)
{
int flags = mastermode << PONGFLAG_MASTERMODE;
flags |= scl.serverpassword[0] ? 1 << PONGFLAG_PASSWORD : 0;
loopv(bans) if(bans[i].address.host == ip) { flags |= 1 << PONGFLAG_BANNED; break; }
if (ipblacklist.check(ip))
flags |= 1 << PONGFLAG_BLACKLIST;
/*
if (ipmutelist.check(ip))
flags |= 1 << PONGFLAG_MUTE;
*/
if (scl.bypassglobalbans)
flags |= 1 << PONGFLAG_BYPASSBANS;
if (scl.bypassglobalpriv)
flags |= 1 << PONGFLAG_BYPASSPRIV;
return flags;
}
void extping_namelist(ucharbuf &p)
{
loopv(clients)
{
if(clients[i]->type == ST_TCPIP && clients[i]->isauthed) sendstring(clients[i]->name, p);
}
sendstring("", p);
}
void extping_serverinfo(ucharbuf &pi, ucharbuf &po)
{
char lang[3];
lang[0] = tolower(getint(pi)); lang[1] = tolower(getint(pi)); lang[2] = '\0';
const char *reslang = lang, *buf = infofiles.getinfo(lang); // try client language
if(!buf) buf = infofiles.getinfo(reslang = "en"); // try english
sendstring(buf ? reslang : "", po);
if(buf)
{
for(const char *c = buf; *c && po.remaining() > MAXINFOLINELEN + 10; c += strlen(c) + 1) sendstring(c, po);
sendstring("", po);
}
}
void extping_maprot(ucharbuf &po)
{
putint(po, CONFIG_MAXPAR);
string text;
bool abort = false;
loopv(maprot.configsets)
{
if(po.remaining() < 100) abort = true;
configset &c = maprot.configsets[i];
filtertext(text, c.mapname, 0);
text[30] = '\0';
sendstring(abort ? "-- list truncated --" : text, po);
loopi(CONFIG_MAXPAR) putint(po, c.par[i]);
if(abort) break;
}
sendstring("", po);
}
void extping_uplinkstats(ucharbuf &po)
{
if(scl.maxclients)
po.put(chokelog, scl.maxclients); // send logs for every used slot
}
void extinfo_cnbuf(ucharbuf &p, int cn)
{
if(cn == -1) // add all available player ids
{
loopv(clients) if(clients[i]->type != ST_EMPTY)
putint(p,clients[i]->clientnum);
}
else if(valid_client(cn)) // add single player only
{
putint(p,clients[cn]->clientnum);
}
}
void extinfo_statsbuf(ucharbuf &p, int pid, int bpos, ENetSocket &pongsock, ENetAddress &addr, ENetBuffer &buf, int len, int *csend)
{
loopv(clients)
{
if(clients[i]->type != ST_TCPIP) continue;
if(pid>-1 && clients[i]->clientnum!=pid) continue;
bool ismatch = mastermode == MM_MATCH;
putint(p,EXT_PLAYERSTATS_RESP_STATS); // send player stats following
putint(p,clients[i]->clientnum); //add player id
putint(p,clients[i]->ping); //Ping
sendstring(clients[i]->name,p); //Name
sendstring(team_string(clients[i]->team),p); //Team
// "team_string(clients[i]->team)" sometimes return NULL according to RK, causing the server to crash. WTF ?
putint(p,clients[i]->state.frags); //Frags
putint(p,clients[i]->state.flagscore); //Flagscore
putint(p,clients[i]->state.deaths); //Death
putint(p,clients[i]->state.assists); //Assists
putint(p,ismatch ? 0 : clients[i]->state.damage*100/max(clients[i]->state.shotdamage,1)); //Accuracy
putint(p,ismatch ? 0 : clients[i]->state.health); //Health
putint(p,ismatch ? 0 : clients[i]->state.armour); //Armour
putint(p,ismatch ? 0 : clients[i]->state.gunselect); //Gun selected
putint(p,clients[i]->role); //Role
putint(p,clients[i]->state.state); //State (Alive,Dead,Spawning,Lagged,Editing)
uint ip = clients[i]->peer->address.host; // only 3 byte of the ip address (privacy protected)
p.put((uchar*)&ip,3);
buf.dataLength = len + p.length();
enet_socket_send(pongsock, &addr, &buf, 1);
*csend += (int)buf.dataLength;
if(pid>-1) break;
p.len=bpos;
}
}
void extinfo_teamscorebuf(ucharbuf &p)
{
putint(p, m_team(gamemode, mutators) ? EXT_ERROR_NONE : EXT_ERROR);
putint(p, gamemode);
putint(p, minremain); // possible TODO: use gamemillis, gamelimit here too?
if(!m_team(gamemode, mutators)) return;
int teamsizes[TEAM_NUM] = { 0 }, fragscores[TEAM_NUM] = { 0 }, flagscores[TEAM_NUM] = { 0 };
loopv(clients) if(clients[i]->type!=ST_EMPTY && team_isvalid(clients[i]->team))
{
teamsizes[clients[i]->team] += 1;
fragscores[clients[i]->team] += clients[i]->state.frags;
flagscores[clients[i]->team] += clients[i]->state.flagscore;
}
loopi(TEAM_NUM) if(teamsizes[i])
{
sendstring(team_string(i), p); // team name
putint(p, fragscores[i]); // add fragscore per team
putint(p, m_flags(gamemode) ? flagscores[i] : -1); // add flagscore per team
putint(p, -1); // ?
}
}
#ifndef STANDALONE
void localdisconnect()
{
loopv(clients) if(clients[i]->type==ST_LOCAL) clients[i]->zap();
}
void localconnect()
{
servstate.reset();
client &c = addclient();
c.type = ST_LOCAL;
c.role = CR_ADMIN;
copystring(c.hostname, "local");
sendservinfo(c);
}
#endif
string server_name = "unarmed server";
void quitproc(int param)
{
// this triggers any "atexit"-calls:
exit(param == 2 ? EXIT_SUCCESS : EXIT_FAILURE); // 3 is the only reply on Win32 apparently, SIGINT == 2 == Ctrl-C
}
void initserver(bool dedicated, int argc, char **argv)
{
const char *service = NULL;
for(int i = 1; i<argc; i++)
{
if(!scl.checkarg(argv[i]))
{
char *a = &argv[i][2];
if(!scl.checkarg(argv[i]) && argv[i][0]=='-') switch(argv[i][1])
{
case '-': break;
case 'S': service = a; break;
default: break; /*printf("WARNING: unknown commandline option\n");*/ // less warnings - 2011feb05:ft: who disabled this - I think this should be on - more warnings == more clarity
}
else if (strncmp(argv[i], "assaultcube://", 13)) printf("WARNING: unknown commandline argument\n");
}
}
if(service && !svcctrl)
{
#ifdef WIN32
svcctrl = new winservice(service);
#endif
if(svcctrl)
{
svcctrl->argc = argc; svcctrl->argv = argv;
svcctrl->start();
}
}
smapname[0] = '\0';
string identity;
if(scl.logident[0]) filtertext(identity, scl.logident, 0);
else formatstring(identity)("%s#%d", scl.ip[0] ? scl.ip : "local", scl.serverport);
int conthres = scl.verbose > 1 ? ACLOG_DEBUG : (scl.verbose ? ACLOG_VERBOSE : ACLOG_INFO);
if(dedicated && !initlogging(identity, scl.syslogfacility, conthres, scl.filethres, scl.syslogthres, scl.logtimestamp))
printf("WARNING: logging not started!\n");
logline(ACLOG_INFO, "logging local ACR server (version %d, protocol %d/%d) now..", AC_VERSION, SERVER_PROTOCOL_VERSION, EXT_VERSION);
copystring(servdesc_current, scl.servdesc_full);
servermsinit(scl.master ? scl.master : AC_MASTER_URI, scl.ip, CUBE_SERVINFO_PORT(scl.serverport), dedicated);
if((isdedicated = dedicated))
{
ENetAddress address = { ENET_HOST_ANY, (enet_uint16)scl.serverport };
if(scl.ip[0] && enet_address_set_host(&address, scl.ip)<0) logline(ACLOG_WARNING, "server ip not resolved!");
serverhost = enet_host_create(&address, scl.maxclients+1, 3, 0, scl.uprate);
if(!serverhost) fatal("could not create server host");
loopi(scl.maxclients) serverhost->peers[i].data = (void *)-1;
maprot.init(scl.maprot);
maprot.next(false, true); // ensure minimum maprot length of '1'
passwords.init(scl.pwdfile, scl.adminpasswd);
ipblacklist.init(scl.blfile);
nickblacklist.init(scl.nbfile);
forbiddenlist.init(scl.forbidden);
infofiles.init(scl.infopath, scl.motdpath);
infofiles.getinfo("en"); // cache 'en' serverinfo
logline(ACLOG_VERBOSE, "holding up to %d recorded demos in memory", scl.maxdemos);
if(scl.demopath[0]) logline(ACLOG_VERBOSE,"all recorded demos will be written to: \"%s\"", scl.demopath);
if(scl.voteperm[0]) logline(ACLOG_VERBOSE,"vote permission string: \"%s\"", scl.voteperm);
if(scl.mapperm[0]) logline(ACLOG_VERBOSE,"map permission string: \"%s\"", scl.mapperm);
logline(ACLOG_VERBOSE,"server description: \"%s\"", scl.servdesc_full);
if(scl.servdesc_pre[0] || scl.servdesc_suf[0]) logline(ACLOG_VERBOSE,"custom server description: \"%sCUSTOMPART%s\"", scl.servdesc_pre, scl.servdesc_suf);
logline(ACLOG_VERBOSE,"maxclients: %d, lag trust: %d", scl.maxclients, scl.lagtrust);
if(scl.master) logline(ACLOG_VERBOSE,"master server URL: \"%s\"", scl.master);
if(scl.serverpassword[0]) logline(ACLOG_VERBOSE,"server password: \"%s\"", hiddenpwd(scl.serverpassword));
}
resetserverifempty();
if(isdedicated) // do not return, this becomes main loop
{
#ifdef WIN32
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
#endif
// kill -2 / Ctrl-C - see http://msdn.microsoft.com/en-us/library/xdkz3x12%28v=VS.100%29.aspx (or VS-2008?) for caveat (seems not to pertain to AC - 2011feb05:ft)
if (signal(SIGINT, quitproc) == SIG_ERR) logline(ACLOG_INFO, "Cannot handle SIGINT!");
// kill -15 / probably process-manager on Win32 *shrug*
if (signal(SIGTERM, quitproc) == SIG_ERR) logline(ACLOG_INFO, "Cannot handle SIGTERM!");
#ifndef WIN32
// kill -1
if (signal(SIGHUP, quitproc) == SIG_ERR) logline(ACLOG_INFO, "Cannot handle SIGHUP!");
// kill -9 is uncatchable - http://en.wikipedia.org/wiki/SIGKILL
//if (signal(SIGKILL, quitproc) == SIG_ERR) logline(ACLOG_INFO, "Cannot handle SIGKILL!");
#endif
logline(ACLOG_INFO, "dedicated server started, waiting for clients...");
logline(ACLOG_INFO, "Ctrl-C to exit"); // this will now actually call the atexit-hooks below - thanks to SIGINT hooked above - noticed and signal-code-docs found by SKB:2011feb05:ft:
atexit(enet_deinitialize);
atexit(cleanupserver);
enet_time_set(0);
for(;;) serverslice(5);
}
}
#ifdef STANDALONE
void localservertoclient(int chan, uchar *buf, int len, bool demo) {}
// coverity[+kill]
void fatal(const char *s, ...)
{
defvformatstring(msg,s,s);
defformatstring(out)("ACR fatal error: %s", msg);
if (logline(ACLOG_ERROR, "%s", out));
else puts(out);
cleanupserver();
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
#ifdef WIN32
//atexit((void (__cdecl *)(void))_CrtDumpMemoryLeaks);
#ifndef _DEBUG
#ifndef __GNUC__
__try {
#endif
#endif
#endif
for(int i = 1; i<argc; i++)
{
if (!strncmp(argv[i],"--wizard",8)) return wizardmain(argc, argv);
}
if(enet_initialize()<0) fatal("Unable to initialise network module");
initserver(true, argc, argv);
return EXIT_SUCCESS;
#if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__)
} __except(stackdumper(0, GetExceptionInformation()), EXCEPTION_CONTINUE_SEARCH) { return 0; }
#endif
}
#endif
153
pages