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