1793 lines
53 KiB
C++
1793 lines
53 KiB
C++
// clientgame.cpp: core game related stuff
|
|
|
|
#include "cube.h"
|
|
#include "bot/bot.h"
|
|
|
|
int nextmode = 0; // nextmode becomes gamemode after next map load
|
|
VAR(gamemode, 1, 0, 0);
|
|
VARP(modeacronyms, 0, 1, 1);
|
|
|
|
flaginfo flaginfos[2];
|
|
|
|
void mode(int n)
|
|
{
|
|
nextmode = n;
|
|
if(m_mp(n) || !multiplayer()) addmsg(SV_GAMEMODE, "ri", n);
|
|
}
|
|
COMMAND(mode, ARG_1INT);
|
|
|
|
void setbottimeout(int m, int t)
|
|
{
|
|
if(m==1 || m==2)
|
|
{
|
|
if(t>0 && t<61)
|
|
{
|
|
switch(m)
|
|
{
|
|
case 1:
|
|
{
|
|
extern int botmatch_dm_minremain;
|
|
botmatch_dm_minremain = t;
|
|
break;
|
|
}
|
|
case 2:
|
|
{
|
|
extern int botmatch_tm_minremain;
|
|
botmatch_tm_minremain = t;
|
|
break;
|
|
}
|
|
default: break;
|
|
}
|
|
}
|
|
else conoutf(_("%c3Error: %c5valid range for time is 1-60"), CC, CC);
|
|
}
|
|
else conoutf(_("%c3Error: %c5mode needs to be 1 for FFA or 2 for team modes"), CC, CC);
|
|
}
|
|
COMMAND(setbottimeout, ARG_2INT);
|
|
|
|
bool intermission = false;
|
|
int arenaintermission = 0;
|
|
struct serverstate servstate = { 0 };
|
|
|
|
playerent *player1 = newplayerent(); // our client
|
|
vector<playerent *> players; // other clients
|
|
|
|
int lastmillis = 0, totalmillis = 0, nextmillis = 0;
|
|
int lasthit = 0;
|
|
int curtime = 0;
|
|
string clientmap = "";
|
|
int spawnpermission = SP_WRONGMAP;
|
|
|
|
char *getclientmap() { return clientmap; }
|
|
|
|
int getclientmode() { return gamemode; }
|
|
|
|
extern bool sendmapidenttoserver;
|
|
|
|
void setskin(playerent *pl, int skin, int team)
|
|
{
|
|
if(!pl) return;
|
|
pl->setskin(team, skin);
|
|
}
|
|
|
|
extern char *global_name;
|
|
|
|
bool duplicatename(playerent *d, char *name = NULL)
|
|
{
|
|
if(!name) name = d->name;
|
|
if(d!=player1 && !strcmp(name, player1->name)) return true;
|
|
if(!strcmp(name, "you")) return true;
|
|
loopv(players) if(players[i] && d!=players[i] && !strcmp(name, players[i]->name)) return true;
|
|
global_name = player1->name; // this certainly is not the best place to put this
|
|
return false;
|
|
}
|
|
|
|
char *colorname(playerent *d, char *name, const char *prefix)
|
|
{
|
|
if(!name) name = d->name;
|
|
if(name[0] && !duplicatename(d, name)) return name;
|
|
static string cname[4];
|
|
static int num = 0;
|
|
num = (num + 1) % 4;
|
|
formatstring(cname[num])("%s%s \fs\f6(%d)\fr", prefix, name, d->clientnum);
|
|
return cname[num];
|
|
}
|
|
|
|
char *colorping(int ping)
|
|
{
|
|
static string cping;
|
|
formatstring(cping)("\fs\f%d%d\fr", ping <= 500 ? 0 : ping <= 1000 ? 2 : 3, ping);
|
|
return cping;
|
|
}
|
|
|
|
char *colorpj(int pj)
|
|
{
|
|
static string cpj;
|
|
formatstring(cpj)("\fs\f%d%d\fr", pj <= 90 ? 0 : pj <= 170 ? 2 : 3, pj);
|
|
return cpj;
|
|
}
|
|
|
|
const char *highlight(const char *text)
|
|
{
|
|
static char result[MAXTRANS + 10];
|
|
const char *marker = getalias("HIGHLIGHT"), *sep = " ,;:!\"'";
|
|
if(!marker || !strstr(text, player1->name)) return text;
|
|
filterrichtext(result, marker);
|
|
defformatstring(subst)("\fs%s%s\fr", result, player1->name);
|
|
char *temp = newstring(text);
|
|
char *s = strtok(temp, sep), *l = temp, *c, *r = result;
|
|
result[0] = '\0';
|
|
while(s)
|
|
{
|
|
if(!strcmp(s, player1->name))
|
|
{
|
|
if(MAXTRANS - strlen(result) > strlen(subst) + (s - l))
|
|
{
|
|
for(c = l; c < s; c++) *r++ = text[c - temp];
|
|
*r = '\0';
|
|
strcat(r, subst);
|
|
}
|
|
l = s + strlen(s);
|
|
}
|
|
s = strtok(NULL, sep);
|
|
}
|
|
if(MAXTRANS - strlen(result) > strlen(text) - (l - temp)) strcat(result, text + (l - temp));
|
|
delete[] temp;
|
|
return *result ? result : text;
|
|
}
|
|
|
|
void ignore(int cn)
|
|
{
|
|
playerent *d = getclient(cn);
|
|
if(d && d != player1) d->ignored = true;
|
|
}
|
|
|
|
void listignored()
|
|
{
|
|
string pl;
|
|
pl[0] = '\0';
|
|
loopv(players) if(players[i] && players[i]->ignored) concatformatstring(pl, ", %s", colorname(players[i]));
|
|
if(*pl) conoutf(_("ignored players: %s"), pl + 2);
|
|
else conoutf(_("no players were ignored."));
|
|
}
|
|
|
|
void clearignored(char *ccn)
|
|
{
|
|
int cn = ccn && *ccn ? atoi(ccn) : -1;
|
|
loopv(players) if(players[i] && (cn < 0 || cn == i)) players[i]->ignored = false;
|
|
}
|
|
|
|
void muteplayer(int cn)
|
|
{
|
|
playerent *d = getclient(cn);
|
|
if(d && d != player1) d->muted = true;
|
|
}
|
|
|
|
void listmuted()
|
|
{
|
|
string pl;
|
|
pl[0] = '\0';
|
|
loopv(players) if(players[i] && players[i]->muted) concatformatstring(pl, ", %s", colorname(players[i]));
|
|
if(*pl) conoutf(_("muted players: %s"), pl + 2);
|
|
else conoutf(_("no players were muted."));
|
|
}
|
|
|
|
void clearmuted(char *ccn)
|
|
{
|
|
int cn = ccn && *ccn ? atoi(ccn) : -1;
|
|
loopv(players) if(players[i] && (cn < 0 || cn == i)) players[i]->muted = false;
|
|
}
|
|
|
|
COMMAND(ignore, ARG_1INT);
|
|
COMMAND(listignored, ARG_NONE);
|
|
COMMAND(clearignored, ARG_1STR);
|
|
COMMAND(muteplayer, ARG_1INT);
|
|
COMMAND(listmuted, ARG_NONE);
|
|
COMMAND(clearmuted, ARG_1STR);
|
|
|
|
void newname(const char *name)
|
|
{
|
|
if(name[0])
|
|
{
|
|
string tmpname;
|
|
filtertext(tmpname, name, 0, MAXNAMELEN);
|
|
if(identexists("onNameChange"))
|
|
{
|
|
defformatstring(onnamechange)("onNameChange %d \"%s\"", player1->clientnum, tmpname);
|
|
execute(onnamechange);
|
|
}
|
|
copystring(player1->name, tmpname);//12345678901234//
|
|
if(!player1->name[0]) copystring(player1->name, "unarmed");
|
|
updateclientname(player1);
|
|
addmsg(SV_SWITCHNAME, "rs", player1->name);
|
|
}
|
|
else conoutf(_("your name is: %s"), player1->name);
|
|
//alias(_("curname"), player1->name); // WTF? stef went crazy - this isn't something to translate either.
|
|
alias("curname", player1->name);
|
|
}
|
|
|
|
int teamatoi(const char *name)
|
|
{
|
|
string uc;
|
|
strtoupper(uc, name);
|
|
loopi(TEAM_NUM) if(!strcmp(teamnames[i], uc)) return i;
|
|
return -1;
|
|
}
|
|
|
|
void newteam(char *name)
|
|
{
|
|
if(*name)
|
|
{
|
|
int nt = teamatoi(name);
|
|
if(nt == player1->team) return; // same team
|
|
if(!team_isvalid(nt)) { conoutf(_("%c3\"%s\" is not a valid team name (try CLA, RVSF or SPECTATOR)"), CC, name); return; }
|
|
if(team_isspect(nt))
|
|
{
|
|
if(player1->state != CS_DEAD) { conoutf(_("you'll need to be in a \"dead\" state to become a spectator")); return; }
|
|
if(!multiplayer()) { conoutf(_("you cannot spectate in singleplayer")); return; }
|
|
}
|
|
if(player1->state == CS_EDITING) conoutf(_("you can't change team while editing"));
|
|
else addmsg(SV_SWITCHTEAM, "ri", nt);
|
|
}
|
|
else conoutf(_("your team is: %s"), team_string(player1->team));
|
|
}
|
|
|
|
void benchme()
|
|
{
|
|
if(team_isactive(player1->team) && servstate.mastermode == MM_MATCH)
|
|
addmsg(SV_SWITCHTEAM, "ri", team_tospec(player1->team));
|
|
}
|
|
|
|
int _setskin(char *s, int t)
|
|
{
|
|
if(s && *s)
|
|
{
|
|
setskin(player1, atoi(s), t);
|
|
addmsg(SV_SWITCHSKIN, "rii", player1->skin(0), player1->skin(1));
|
|
}
|
|
return player1->skin(t);
|
|
}
|
|
|
|
ICOMMANDF(skin_cla, ARG_1EST, (char *s) { return _setskin(s, TEAM_CLA); });
|
|
ICOMMANDF(skin_rvsf, ARG_1EST, (char *s) { return _setskin(s, TEAM_RVSF); });
|
|
ICOMMANDF(skin, ARG_1EST, (char *s) { return _setskin(s, player1->team); });
|
|
|
|
int curmodeattr(char *attr)
|
|
{
|
|
if(!strcmp(attr, "team")) return m_teammode;
|
|
else if(!strcmp(attr, "arena")) return m_arena;
|
|
else if(!strcmp(attr, "flag")) return m_flags;
|
|
else if(!strcmp(attr, "bot")) return m_botmode;
|
|
return 0;
|
|
}
|
|
|
|
COMMANDN(team, newteam, ARG_1STR);
|
|
COMMANDN(name, newname, ARG_1STR);
|
|
COMMAND(benchme, ARG_NONE);
|
|
ICOMMANDF(isclient, ARG_1EXP, (int cn) { return getclient(cn) != NULL ? 1 : 0; } );
|
|
ICOMMANDF(isSpect, ARG_IVAL, (void) { return (player1->team==TEAM_SPECT || player1->spectatemode==SM_FLY); });
|
|
ICOMMANDF(currole, ARG_IVAL, (void) { return player1->clientrole; });
|
|
ICOMMANDF(curmastermode, ARG_IVAL, (void) { return servstate.mastermode; });
|
|
ICOMMANDF(curautoteam, ARG_IVAL, (void) { return servstate.autoteam; });
|
|
COMMAND(curmodeattr, ARG_1EST);
|
|
COMMANDF(curmap, ARG_1INT, (int cleaned) { result(cleaned ? behindpath(getclientmap()) : getclientmap()); });
|
|
ICOMMANDF(curplayers, ARG_IVAL, (void) { return players.length() + 1; });
|
|
VARP(showscoresondeath, 0, 1, 1);
|
|
VARP(autoscreenshot, 0, 0, 1);
|
|
|
|
// macros for playerinfo() & teaminfo(). Use this to replace pstats_xxx ?
|
|
#define ATTR_INT(name, attribute) if(!strcmp(attr, #name)) { intret(attribute); return; }
|
|
#define ATTR_FLOAT(name, attribute) if(!strcmp(attr, #name)) { floatret(attribute); return; }
|
|
#define ATTR_STR(name, attribute) if(!strcmp(attr, #name)) { result(attribute); return; }
|
|
|
|
void playerinfo(const char *cn, const char *attr)
|
|
{
|
|
if(!*cn || !cn || !*attr || !attr) return;
|
|
|
|
int clientnum = atoi(cn); // get player clientnum
|
|
playerent *p = clientnum < 0 ? player1 : getclient(clientnum);
|
|
if(!p)
|
|
{
|
|
if(!m_botmode && multiplayer(false)) // bot clientnums are still glitchy, causing this message to sometimes appear in offline/singleplayer when it shouldn't??? -Bukz 2012may
|
|
conoutf("invalid clientnum cn: %s attr: %s", cn, attr);
|
|
return;
|
|
}
|
|
|
|
if(p == player1)
|
|
{
|
|
ATTR_INT(magcontent, p->weaponsel->mag);
|
|
ATTR_INT(ammo, p->weaponsel->ammo);
|
|
ATTR_INT(primary, p->primary);
|
|
ATTR_INT(curweapon, p->weaponsel->type);
|
|
ATTR_INT(nextprimary, p->nextprimary);
|
|
}
|
|
|
|
if(p == player1
|
|
|| (team_base(p->team) == team_base(player1->team) && m_teammode)
|
|
|| player1->team == TEAM_SPECT
|
|
|| m_coop)
|
|
{
|
|
ATTR_INT(health, p->health);
|
|
ATTR_INT(armour, p->armour);
|
|
ATTR_INT(attacking, p->attacking);
|
|
ATTR_INT(scoping, p->scoping);
|
|
ATTR_FLOAT(x, p->o.x);
|
|
ATTR_FLOAT(y, p->o.y);
|
|
ATTR_FLOAT(z, p->o.z);
|
|
}
|
|
ATTR_STR(name, p->name);
|
|
ATTR_INT(team, p->team);
|
|
ATTR_INT(ping, p->ping);
|
|
ATTR_INT(pj, p->plag);
|
|
ATTR_INT(state, p->state);
|
|
ATTR_INT(role, p->clientrole);
|
|
ATTR_INT(frags, p->frags);
|
|
ATTR_INT(flags, p->flagscore);
|
|
ATTR_INT(points, p->points);
|
|
ATTR_INT(deaths, p->deaths);
|
|
ATTR_INT(tks, p->tks);
|
|
ATTR_INT(alive, p->state == CS_ALIVE ? 1 : 0);
|
|
ATTR_INT(spec, p->team == TEAM_SPECT || p->spectatemode == SM_FLY ? 1 : 0);
|
|
ATTR_INT(cn, p->clientnum); // only useful to get player1's client number.
|
|
conoutf("invalid attribute: %s", attr);
|
|
}
|
|
|
|
void playerinfolocal(const char *attr)
|
|
{
|
|
playerinfo("-1", attr);
|
|
}
|
|
|
|
COMMANDN(player, playerinfo, ARG_3STR);
|
|
COMMANDN(player1, playerinfolocal, ARG_2STR);
|
|
|
|
void teaminfo(const char *team, const char *attr)
|
|
{
|
|
if(!team || !attr || !m_teammode) return;
|
|
int t = atoi(team); // get player clientnum
|
|
if(!team_isactive(t))
|
|
{
|
|
conoutf("invalid team: %s", team);
|
|
return;
|
|
}
|
|
int t_flags = 0;
|
|
int t_frags = 0;
|
|
int t_deaths = 0;
|
|
int t_points = 0;
|
|
|
|
string teammembers = "", tmp;
|
|
|
|
loopv(players) if(players[i] && players[i]->team == t)
|
|
{
|
|
t_frags += players[i]->frags;
|
|
t_deaths += players[i]->deaths;
|
|
t_points += players[i]->points;
|
|
t_flags += players[i]->flagscore;
|
|
sprintf(tmp, "%s%d ", teammembers, players[i]->clientnum);
|
|
concatstring(teammembers, tmp);
|
|
}
|
|
|
|
loopv(discscores) if(discscores[i].team == t)
|
|
{
|
|
t_frags += discscores[i].frags;
|
|
t_deaths += discscores[i].deaths;
|
|
t_points += discscores[i].points;
|
|
t_flags += discscores[i].flags;
|
|
}
|
|
|
|
if(player1->team == t)
|
|
{
|
|
t_frags += player1->frags;
|
|
t_deaths += player1->deaths;
|
|
t_points += player1->points;
|
|
t_flags += player1->flagscore;
|
|
sprintf(tmp, "%s%d ", teammembers, player1->clientnum);
|
|
concatstring(teammembers, tmp);
|
|
}
|
|
|
|
ATTR_INT(flags, t_flags);
|
|
ATTR_INT(frags, t_frags);
|
|
ATTR_INT(deaths, t_deaths);
|
|
ATTR_INT(points, t_points);
|
|
ATTR_STR(name, team_string(t));
|
|
ATTR_STR(players, teammembers);
|
|
conoutf("invalid attribute: %s", attr);
|
|
}
|
|
|
|
COMMAND(teaminfo, ARG_2STR);
|
|
|
|
void deathstate(playerent *pl)
|
|
{
|
|
pl->state = CS_DEAD;
|
|
pl->spectatemode = SM_DEATHCAM;
|
|
pl->respawnoffset = pl->lastpain = lastmillis;
|
|
pl->move = pl->strafe = 0;
|
|
pl->pitch = pl->roll = 0;
|
|
pl->attacking = false;
|
|
pl->weaponsel->onownerdies();
|
|
|
|
if(pl==player1)
|
|
{
|
|
if(showscoresondeath) showscores(true);
|
|
setscope(false);
|
|
setburst(false);
|
|
if(editmode) toggleedit(true);
|
|
damageblend(-1);
|
|
if(pl->team == TEAM_SPECT) spectatemode(SM_FLY);
|
|
else if(team_isspect(pl->team)) spectatemode(SM_FOLLOW1ST);
|
|
if(pl->spectatemode == SM_DEATHCAM) player1->followplayercn = FPCN_DEATHCAM;
|
|
}
|
|
else pl->resetinterp();
|
|
}
|
|
|
|
void spawnstate(playerent *d) // reset player state not persistent accross spawns
|
|
{
|
|
d->respawn();
|
|
d->spawnstate(gamemode);
|
|
if(d==player1)
|
|
{
|
|
setscope(false);
|
|
setburst(false);
|
|
}
|
|
if(d->deaths==0) d->resetstats();
|
|
}
|
|
|
|
playerent *newplayerent() // create a new blank player
|
|
{
|
|
playerent *d = new playerent;
|
|
d->lastupdate = totalmillis;
|
|
setskin(d, rnd(6));
|
|
weapon::equipplayer(d); // flowtron : avoid overwriting d->spawnstate(gamemode) stuff from the following line (this used to be called afterwards)
|
|
spawnstate(d);
|
|
return d;
|
|
}
|
|
|
|
botent *newbotent() // create a new blank player
|
|
{
|
|
botent *d = new botent;
|
|
d->lastupdate = totalmillis;
|
|
setskin(d, rnd(6));
|
|
weapon::equipplayer(d);
|
|
spawnstate(d); // move like above
|
|
int nextcn = 0;
|
|
bool lukin = true;
|
|
while(lukin)
|
|
{
|
|
bool used = nextcn==getclientnum();
|
|
loopv(players) if(!used && players[i]) if(players[i]->clientnum==nextcn) used = true;
|
|
if(!used) lukin = false; else nextcn++;
|
|
}
|
|
loopv(players) if(i!=getclientnum() && !players[i])
|
|
{
|
|
players[i] = d;
|
|
d->clientnum = nextcn;
|
|
return d;
|
|
}
|
|
if(players.length()==getclientnum()) players.add(NULL);
|
|
d->clientnum = nextcn;
|
|
players.add(d);
|
|
return d;
|
|
}
|
|
|
|
void freebotent(botent *d)
|
|
{
|
|
loopv(players) if(players[i]==d)
|
|
{
|
|
DELETEP(players[i]);
|
|
players.remove(i);
|
|
}
|
|
}
|
|
|
|
VAR(lastpm, 1, -1, 0);
|
|
void zapplayer(playerent *&d)
|
|
{
|
|
if(d && d->clientnum == lastpm) lastpm = -1;
|
|
DELETEP(d);
|
|
}
|
|
|
|
void movelocalplayer()
|
|
{
|
|
if(player1->state==CS_DEAD && !player1->allowmove())
|
|
{
|
|
if(lastmillis-player1->lastpain<2000)
|
|
{
|
|
player1->move = player1->strafe = 0;
|
|
moveplayer(player1, 10, false);
|
|
}
|
|
}
|
|
else if(!intermission)
|
|
{
|
|
moveplayer(player1, 10, true);
|
|
checkitems(player1);
|
|
}
|
|
}
|
|
|
|
// use physics to extrapolate player position
|
|
VARP(smoothmove, 0, 75, 100);
|
|
VARP(smoothdist, 0, 8, 16);
|
|
|
|
void predictplayer(playerent *d, bool move)
|
|
{
|
|
d->o = d->newpos;
|
|
d->o.z += d->eyeheight;
|
|
d->yaw = d->newyaw;
|
|
d->pitch = d->newpitch;
|
|
if(move)
|
|
{
|
|
moveplayer(d, 1, false);
|
|
d->newpos = d->o;
|
|
d->newpos.z -= d->eyeheight;
|
|
}
|
|
float k = 1.0f - float(lastmillis - d->smoothmillis)/smoothmove;
|
|
if(k>0)
|
|
{
|
|
d->o.add(vec(d->deltapos).mul(k));
|
|
d->yaw += d->deltayaw*k;
|
|
if(d->yaw<0) d->yaw += 360;
|
|
else if(d->yaw>=360) d->yaw -= 360;
|
|
d->pitch += d->deltapitch*k;
|
|
}
|
|
}
|
|
|
|
void moveotherplayers()
|
|
{
|
|
loopv(players) if(players[i] && players[i]->type==ENT_PLAYER)
|
|
{
|
|
playerent *d = players[i];
|
|
const int lagtime = totalmillis-d->lastupdate;
|
|
if(!lagtime || intermission) continue;
|
|
else if(lagtime>1000 && d->state==CS_ALIVE)
|
|
{
|
|
d->state = CS_LAGGED;
|
|
continue;
|
|
}
|
|
if(d->state==CS_ALIVE || d->state==CS_EDITING)
|
|
{
|
|
if(smoothmove && d->smoothmillis>0) predictplayer(d, true);
|
|
else moveplayer(d, 1, false);
|
|
}
|
|
else if(d->state==CS_DEAD && lastmillis-d->lastpain<2000) moveplayer(d, 1, true);
|
|
}
|
|
}
|
|
|
|
|
|
bool showhudtimer(int maxsecs, int startmillis, const char *msg, bool flash)
|
|
{
|
|
static string str = "";
|
|
static int tickstart = 0, curticks = -1, maxticks = -1;
|
|
int nextticks = (lastmillis - startmillis) / 200;
|
|
if(tickstart!=startmillis || maxticks != 5*maxsecs)
|
|
{
|
|
tickstart = startmillis;
|
|
maxticks = 5*maxsecs;
|
|
curticks = -1;
|
|
copystring(str, "\f3");
|
|
}
|
|
if(curticks >= maxticks) return false;
|
|
nextticks = min(nextticks, maxticks);
|
|
while(curticks < nextticks)
|
|
{
|
|
if(++curticks%5) concatstring(str, ".");
|
|
else
|
|
{
|
|
defformatstring(sec)("%d", maxsecs - (curticks/5));
|
|
concatstring(str, sec);
|
|
}
|
|
}
|
|
if(nextticks < maxticks) hudeditf(HUDMSG_TIMER|HUDMSG_OVERWRITE, flash ? str : str+2);
|
|
else hudeditf(HUDMSG_TIMER, msg);
|
|
return true;
|
|
}
|
|
|
|
int lastspawnattempt = 0;
|
|
|
|
void showrespawntimer()
|
|
{
|
|
if(intermission || spawnpermission > SP_OK_NUM) return;
|
|
if(m_arena)
|
|
{
|
|
if(!arenaintermission) return;
|
|
showhudtimer(5, arenaintermission, "FIGHT!", lastspawnattempt >= arenaintermission && lastmillis < lastspawnattempt+100);
|
|
}
|
|
else if(player1->state==CS_DEAD && m_flags && (!player1->isspectating() || player1->spectatemode==SM_DEATHCAM))
|
|
{
|
|
int secs = 5;
|
|
showhudtimer(secs, player1->respawnoffset, "READY!", lastspawnattempt >= arenaintermission && lastmillis < lastspawnattempt+100);
|
|
}
|
|
}
|
|
|
|
struct scriptsleep { int wait, millis; char *cmd; bool persist; };
|
|
vector<scriptsleep> sleeps;
|
|
|
|
void addsleep(int msec, const char *cmd, bool persist)
|
|
{
|
|
scriptsleep &s = sleeps.add();
|
|
s.wait = max(msec, 1);
|
|
s.millis = lastmillis;
|
|
s.cmd = newstring(cmd);
|
|
s.persist = persist;
|
|
}
|
|
|
|
void addsleep_(char *msec, char *cmd, char *persist)
|
|
{
|
|
addsleep(atoi(msec), cmd, atoi(persist) != 0);
|
|
}
|
|
|
|
void resetsleep(bool force)
|
|
{
|
|
loopv(sleeps) if(!sleeps[i].persist || force)
|
|
{
|
|
DELETEA(sleeps[i].cmd);
|
|
sleeps.remove(i);
|
|
}
|
|
}
|
|
|
|
void resetsleep_()
|
|
{
|
|
resetsleep(true);
|
|
}
|
|
|
|
COMMANDN(sleep, addsleep_, ARG_3STR);
|
|
COMMANDN(resetsleeps, resetsleep_, ARG_NONE);
|
|
|
|
void updateworld(int curtime, int lastmillis) // main game update loop
|
|
{
|
|
// process command sleeps
|
|
loopv(sleeps)
|
|
{
|
|
if(lastmillis - sleeps[i].millis >= sleeps[i].wait)
|
|
{
|
|
char *cmd = sleeps[i].cmd;
|
|
sleeps[i].cmd = NULL;
|
|
execute(cmd);
|
|
delete[] cmd;
|
|
if(sleeps[i].cmd || !sleeps.inrange(i)) break;
|
|
sleeps.remove(i--);
|
|
}
|
|
}
|
|
|
|
syncentchanges();
|
|
physicsframe();
|
|
checkweaponstate();
|
|
if(getclientnum()>=0) shoot(player1, worldpos); // only shoot when connected to server
|
|
movebounceents();
|
|
moveotherplayers();
|
|
gets2c();
|
|
showrespawntimer();
|
|
|
|
// Added by Rick: let bots think
|
|
if(m_botmode) BotManager.Think();
|
|
|
|
movelocalplayer();
|
|
c2sinfo(player1); // do this last, to reduce the effective frame lag
|
|
}
|
|
|
|
#define SECURESPAWNDIST 15
|
|
int spawncycle = -1;
|
|
int fixspawn = 2;
|
|
|
|
// returns -1 for a free place, else dist to the nearest enemy
|
|
float nearestenemy(vec place, int team)
|
|
{
|
|
float nearestenemydist = -1;
|
|
loopv(players)
|
|
{
|
|
playerent *other = players[i];
|
|
if(!other || isteam(team, other->team)) continue;
|
|
float dist = place.dist(other->o);
|
|
if(dist < nearestenemydist || nearestenemydist == -1) nearestenemydist = dist;
|
|
}
|
|
if(nearestenemydist >= SECURESPAWNDIST || nearestenemydist < 0) return -1;
|
|
else return nearestenemydist;
|
|
}
|
|
|
|
void findplayerstart(playerent *d, bool mapcenter, int arenaspawn)
|
|
{
|
|
int r = fixspawn-->0 ? 4 : rnd(10)+1;
|
|
entity *e = NULL;
|
|
if(!mapcenter)
|
|
{
|
|
int type = m_teammode ? team_base(d->team) : 100;
|
|
if(m_arena && arenaspawn >= 0)
|
|
{
|
|
int x = -1;
|
|
loopi(arenaspawn + 1) x = findentity(PLAYERSTART, x+1, type);
|
|
if(x >= 0) e = &ents[x];
|
|
}
|
|
else if((m_teammode || m_arena) && !m_ktf) // ktf uses ffa spawns
|
|
{
|
|
loopi(r) spawncycle = findentity(PLAYERSTART, spawncycle+1, type);
|
|
if(spawncycle >= 0) e = &ents[spawncycle];
|
|
}
|
|
else
|
|
{
|
|
float bestdist = -1;
|
|
|
|
loopi(r)
|
|
{
|
|
spawncycle = m_ktf && numspawn[2] > 5 ? findentity(PLAYERSTART, spawncycle+1, 100) : findentity(PLAYERSTART, spawncycle+1);
|
|
if(spawncycle < 0) continue;
|
|
float dist = nearestenemy(vec(ents[spawncycle].x, ents[spawncycle].y, ents[spawncycle].z), d->team);
|
|
if(!e || dist < 0 || (bestdist >= 0 && dist > bestdist)) { e = &ents[spawncycle]; bestdist = dist; }
|
|
}
|
|
}
|
|
}
|
|
|
|
if(e)
|
|
{
|
|
d->o.x = e->x;
|
|
d->o.y = e->y;
|
|
d->o.z = e->z;
|
|
d->yaw = e->attr1;
|
|
d->pitch = 0;
|
|
d->roll = 0;
|
|
}
|
|
else
|
|
{
|
|
d->o.x = d->o.y = (float)ssize/2;
|
|
d->o.z = 4;
|
|
}
|
|
entinmap(d);
|
|
if(identexists("onSpawn")/* && (m_teammode && d->team == player1->team)*/)
|
|
{
|
|
defformatstring(onspawn)("onSpawn %d", d->clientnum);
|
|
execute(onspawn);
|
|
}
|
|
}
|
|
|
|
void spawnplayer(playerent *d)
|
|
{
|
|
d->respawn();
|
|
d->spawnstate(gamemode);
|
|
d->state = (d==player1 && editmode) ? CS_EDITING : CS_ALIVE;
|
|
findplayerstart(d);
|
|
}
|
|
|
|
void respawnself()
|
|
{
|
|
if( m_mp(gamemode) ) addmsg(SV_TRYSPAWN, "r");
|
|
else
|
|
{
|
|
showscores(false);
|
|
setscope(false);
|
|
setburst(false);
|
|
lasthit = 0;
|
|
spawnplayer(player1);
|
|
player1->lifesequence++;
|
|
player1->weaponswitch(player1->primweap);
|
|
player1->weaponchanging -= player1->weapons[player1->gunselect]->weaponchangetime/2; // 2011jan16:ft: for a little no-shoot after spawn
|
|
}
|
|
}
|
|
|
|
extern int checkarea(int maplayout_factor, char *maplayout);
|
|
extern int MA;
|
|
extern float Mh;
|
|
|
|
bool bad_map() // this function makes a pair with good_map from clients2c
|
|
{
|
|
return (gamemode != GMODE_COOPEDIT && ( Mh >= MAXMHEIGHT || MA >= MAXMAREA ));
|
|
}
|
|
|
|
inline const char * spawn_message()
|
|
{
|
|
if (spawnpermission == SP_WRONGMAP)
|
|
{
|
|
// Don't use "/n" within these messages. If you do, the words won't align to the middle of the screen.
|
|
if (securemapcheck(getclientmap()))
|
|
return "3The server will NOT allow spawning or getmap!"; // Also see client.cpp which has a conoutf message
|
|
else return "3You must be on the correct map to spawn. Type /getmap to download it.";
|
|
}
|
|
else if (m_coop) return "3Type /getmap or send a map and vote for it to start co-op edit.";
|
|
else return "4Awaiting permission to spawn. \f2DON'T PANIC!";
|
|
// Despite its many glaring (and occasionally fatal) inaccuracies, AssaultCube itself has outsold the
|
|
// Encyclopedia Galactica because it is slightly cheaper, and because it has the words "Don't Panic"
|
|
// in large, friendly letters.
|
|
}
|
|
|
|
int waiting_permission = 0;
|
|
|
|
bool tryrespawn()
|
|
{
|
|
if ( m_mp(gamemode) && bad_map() )
|
|
{
|
|
hudoutf("This map is not supported in multiplayer. Read the docs about map quality/dimensions.");
|
|
}
|
|
else if(spawnpermission > SP_OK_NUM)
|
|
{
|
|
hudeditf(HUDMSG_TIMER, "\f%s", spawn_message());
|
|
}
|
|
else if(player1->state==CS_DEAD || player1->state==CS_SPECTATE)
|
|
{
|
|
if(team_isspect(player1->team))
|
|
{
|
|
respawnself();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
int respawnmillis = player1->respawnoffset+(m_arena ? 0 : (m_flags ? 5000 : 2000));
|
|
if(lastmillis>respawnmillis)
|
|
{
|
|
player1->attacking = false;
|
|
if(m_arena)
|
|
{
|
|
if(!arenaintermission) hudeditf(HUDMSG_TIMER, "waiting for new round to start...");
|
|
else lastspawnattempt = lastmillis;
|
|
return false;
|
|
}
|
|
if (lastmillis > waiting_permission)
|
|
{
|
|
waiting_permission = lastmillis + 1000;
|
|
respawnself();
|
|
}
|
|
else hudeditf(HUDMSG_TIMER, "\f%s", spawn_message());
|
|
return true;
|
|
}
|
|
else lastspawnattempt = lastmillis;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
VARP(hitsound, 0, 0, 1);
|
|
|
|
// client kill messages
|
|
void setkillmessage(int gun, bool gib, const char *message)
|
|
{
|
|
if(!message || !*message)
|
|
{
|
|
result(killmessage(gun, gib));
|
|
return;
|
|
}
|
|
|
|
if(gun < 0 || gun >= NUMGUNS)
|
|
{
|
|
conoutf("invalid gun specified");
|
|
return;
|
|
}
|
|
|
|
copystring(killmessages[gib?1:0][gun], message, sizeof(killmessages[gib?1:0][gun]));
|
|
}
|
|
|
|
COMMANDF(fragmessage, ARG_2STR, (const char *gun, const char *message) { setkillmessage(atoi(gun), false, message); });
|
|
COMMANDF(gibmessage, ARG_2STR, (const char *gun, const char *message) { setkillmessage(atoi(gun), true, message); });
|
|
|
|
void writekillmsgcfg()
|
|
{
|
|
stream *f = openfile(path("config/killmessages.cfg", true), "w");
|
|
if(!f) return;
|
|
f->printf("// kill messages for each weapon\n");
|
|
loopi(NUMGUNS)
|
|
{
|
|
const char *fragmsg = killmessage(i, false);
|
|
const char *gibmsg = killmessage(i, true);
|
|
f->printf("\nfragmessage %d \"%s\"", i, fragmsg);
|
|
f->printf("\ngibmessage %d \"%s\"", i, gibmsg);
|
|
}
|
|
f->printf("\n");
|
|
}
|
|
|
|
// damage arriving from the network, monsters, yourself, all ends up here.
|
|
|
|
void dodamage(int damage, playerent *pl, playerent *actor, int gun, bool gib, bool local)
|
|
{
|
|
if(pl->state != CS_ALIVE || intermission) return;
|
|
pl->respawnoffset = pl->lastpain = lastmillis;
|
|
// could the author of the FIXME below please elaborate what's to fix?! (ft:2011mar28)
|
|
// I suppose someone wanted to play the hitsound for player1 or spectated player (lucas:2011may22)
|
|
playerent *h = player1->isspectating() && player1->followplayercn >= 0 && (player1->spectatemode == SM_FOLLOW1ST || player1->spectatemode == SM_FOLLOW3RD || player1->spectatemode == SM_FOLLOW3RD_TRANSPARENT) ? getclient(player1->followplayercn) : NULL;
|
|
if(!h) h = player1;
|
|
if(identexists("onHit"))
|
|
{
|
|
defformatstring(o)("onHit %d %d %d %d %d", actor->clientnum, pl->clientnum, damage, gun, gib ? 1 : 0);
|
|
execute(o);
|
|
}
|
|
if(actor==h && pl!=actor)
|
|
{
|
|
if(hitsound && lasthit != lastmillis) audiomgr.playsound(S_HITSOUND, SP_HIGH);
|
|
lasthit = lastmillis;
|
|
}
|
|
|
|
if (pl != player1)
|
|
{
|
|
damageeffect(damage, pl);
|
|
audiomgr.playsound(S_PAIN1+rnd(5), pl);
|
|
}
|
|
|
|
if(local) damage = pl->dodamage(damage);
|
|
else if(actor==player1) return;
|
|
|
|
if(pl==player1)
|
|
{
|
|
updatedmgindicator(actor->o);
|
|
damageblend(damage);
|
|
pl->damageroll(damage);
|
|
}
|
|
|
|
if(pl->health<=0) { if(local) dokill(pl, actor, gib, gun >= 0 ? gun : actor->weaponsel->type); }
|
|
else if(pl==player1) audiomgr.playsound(S_PAIN6, SP_HIGH);
|
|
else audiomgr.playsound(S_PAIN1+rnd(5), pl);
|
|
}
|
|
|
|
void dokill(playerent *pl, playerent *act, bool gib, int gun)
|
|
{
|
|
if(pl->state!=CS_ALIVE || intermission) return;
|
|
|
|
if(identexists("onKill"))
|
|
{
|
|
defformatstring(killevent)("onKill %d %d %d %d", act->clientnum, pl->clientnum, gun, gib ? 1 : 0);
|
|
execute(killevent);
|
|
}
|
|
|
|
string pname, aname, death;
|
|
copystring(pname, pl==player1 ? "you" : colorname(pl));
|
|
copystring(aname, act==player1 ? "you" : colorname(act));
|
|
copystring(death, killmessage(gun, gib));
|
|
void (*outf)(const char *s, ...) = (pl == player1 || act == player1) ? hudoutf : conoutf;
|
|
|
|
if(pl==act)
|
|
{
|
|
outf("\f2%s suicided%s", pname, pl==player1 ? "!" : "");
|
|
}
|
|
else if(isteam(pl->team, act->team))
|
|
{
|
|
if(pl==player1) outf("\f2you were %s by teammate %s", death, aname);
|
|
else outf("%s%s %s teammate %s", act==player1 ? "\f3" : "\f2", aname, death, pname);
|
|
}
|
|
else
|
|
{
|
|
if(pl==player1) outf("\f2you were %s by %s", death, aname);
|
|
else outf("\f2%s %s %s", aname, death, pname);
|
|
}
|
|
|
|
if(pl == act || isteam(pl->team, act->team))
|
|
{
|
|
act->tks++;
|
|
if(!m_mp(gamemode)) act->frags--;
|
|
}
|
|
else if(!m_mp(gamemode)) act->frags += ( gib && gun != GUN_GRENADE && gun != GUN_SHOTGUN) ? 2 : 1;
|
|
|
|
if(gib)
|
|
{
|
|
if(pl!=act && gun == GUN_SNIPER) audiomgr.playsound(S_HEADSHOT, SP_LOW);
|
|
addgib(pl);
|
|
}
|
|
|
|
deathstate(pl);
|
|
pl->deaths++;
|
|
audiomgr.playsound(S_DIE1+rnd(2), pl);
|
|
}
|
|
/* Removed in favor of the new (player) && (player1) commands ???
|
|
See /config/compatibility.cfg
|
|
void pstat_score(int cn)
|
|
{
|
|
//if(intermission)
|
|
//{
|
|
string scorestring;
|
|
int p_flags = 0;
|
|
int p_frags = 0;
|
|
int p_deaths = 0;
|
|
int p_points = 0;
|
|
int p_tks = 0;
|
|
playerent *pl = cn==player1->clientnum?player1:getclient(cn);
|
|
if(pl)
|
|
{
|
|
p_flags = pl->flagscore;
|
|
p_frags = pl->frags;
|
|
p_deaths = pl->deaths;
|
|
p_points = pl->points;
|
|
p_tks = pl->tks;
|
|
}
|
|
formatstring(scorestring)("%d %d %d %d %d %d %s", p_flags, p_frags, p_deaths, p_points, pl ? pl->team : -1, p_tks, pl ? pl->name : "\"\"");
|
|
result(scorestring);
|
|
//}
|
|
//else result("0 0 0 0 -1 \"\"");
|
|
}
|
|
COMMAND(pstat_score, ARG_1INT);
|
|
*/
|
|
|
|
void pstat_weap(int cn)
|
|
{
|
|
string weapstring = "";
|
|
playerent *pl = getclient(cn);
|
|
if(pl) loopi(NUMGUNS) concatformatstring(weapstring, "%s%d %d", strlen(weapstring) ? " " : "", pl->pstatshots[i], pl->pstatdamage[i]);
|
|
result(weapstring);
|
|
}
|
|
|
|
COMMAND(pstat_weap, ARG_1INT);
|
|
|
|
VAR(minutesremaining, 1, 0, 0);
|
|
VAR(gametimecurrent, 1, 0, 0);
|
|
VAR(gametimemaximum, 1, 0, 0);
|
|
VAR(lastgametimeupdate, 1, 0, 0);
|
|
|
|
void silenttimeupdate(int milliscur, int millismax)
|
|
{
|
|
lastgametimeupdate = lastmillis;
|
|
gametimecurrent = milliscur;
|
|
gametimemaximum = millismax;
|
|
minutesremaining = (gametimemaximum - gametimecurrent + 60000 - 1) / 60000;
|
|
}
|
|
|
|
void timeupdate(int milliscur, int millismax)
|
|
{
|
|
bool display = lastmillis - lastgametimeupdate > 1000; // avoid double-output
|
|
|
|
silenttimeupdate(milliscur, millismax);
|
|
|
|
if(!display) return;
|
|
if(!minutesremaining)
|
|
{
|
|
intermission = true;
|
|
extern bool needsautoscreenshot;
|
|
if(autoscreenshot) needsautoscreenshot = true;
|
|
player1->attacking = false;
|
|
conoutf(_("intermission:"));
|
|
conoutf(_("game has ended!"));
|
|
consolescores();
|
|
showscores(true);
|
|
if(identexists("start_intermission")) execute("start_intermission");
|
|
}
|
|
else
|
|
{
|
|
extern int clockdisplay; // only output to console if no hud-clock is being shown
|
|
if(minutesremaining==1)
|
|
{
|
|
audiomgr.musicsuggest(M_LASTMINUTE1 + rnd(2), 70*1000, true);
|
|
hudoutf("1 minute left!");
|
|
if(identexists("onLastMin")) execute("onLastMin");
|
|
}
|
|
else if(clockdisplay==0) conoutf(_("time remaining: %d minutes"), minutesremaining);
|
|
}
|
|
}
|
|
|
|
playerent *newclient(int cn) // ensure valid entity
|
|
{
|
|
if(cn<0 || cn>=MAXCLIENTS)
|
|
{
|
|
neterr("clientnum");
|
|
return NULL;
|
|
}
|
|
while(cn>=players.length()) players.add(NULL);
|
|
playerent *d = players[cn];
|
|
if(d) return d;
|
|
d = newplayerent();
|
|
players[cn] = d;
|
|
d->clientnum = cn;
|
|
return d;
|
|
}
|
|
|
|
playerent *getclient(int cn) // ensure valid entity
|
|
{
|
|
if(cn == player1->clientnum) return player1;
|
|
return players.inrange(cn) ? players[cn] : NULL;
|
|
}
|
|
|
|
void initclient()
|
|
{
|
|
newname("unarmed");
|
|
player1->team = TEAM_SPECT;
|
|
}
|
|
|
|
entity flagdummies[2] = // in case the map does not provide flags
|
|
{
|
|
entity(-1, -1, -1, CTF_FLAG, 0, 0, 0, 0),
|
|
entity(-1, -1, -1, CTF_FLAG, 0, 1, 0, 0)
|
|
};
|
|
|
|
void initflag(int i)
|
|
{
|
|
flaginfo &f = flaginfos[i];
|
|
f.flagent = &flagdummies[i];
|
|
f.pos = vec(f.flagent->x, f.flagent->y, f.flagent->z);
|
|
f.ack = true;
|
|
f.actor = NULL;
|
|
f.actor_cn = -1;
|
|
f.team = i;
|
|
f.state = m_ktf ? CTFF_IDLE : CTFF_INBASE;
|
|
}
|
|
|
|
void zapplayerflags(playerent *p)
|
|
{
|
|
loopi(2) if(flaginfos[i].state==CTFF_STOLEN && flaginfos[i].actor==p) initflag(i);
|
|
}
|
|
|
|
void preparectf(bool cleanonly=false)
|
|
{
|
|
loopi(2) initflag(i);
|
|
if(!cleanonly)
|
|
{
|
|
loopv(ents)
|
|
{
|
|
entity &e = ents[i];
|
|
if(e.type==CTF_FLAG)
|
|
{
|
|
e.spawned = true;
|
|
if(e.attr2>=2) { conoutf(_("%c3invalid ctf-flag entity (%i)"), CC, i); e.attr2 = 0; }
|
|
flaginfo &f = flaginfos[e.attr2];
|
|
f.flagent = &e;
|
|
f.pos.x = (float) e.x;
|
|
f.pos.y = (float) e.y;
|
|
f.pos.z = (float) e.z;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct gmdesc { int mode; char *desc; };
|
|
vector<gmdesc> gmdescs;
|
|
|
|
void gamemodedesc(char *modenr, char *desc)
|
|
{
|
|
if(!modenr || !desc) return;
|
|
struct gmdesc &gd = gmdescs.add();
|
|
gd.mode = atoi(modenr);
|
|
gd.desc = newstring(desc);
|
|
}
|
|
|
|
COMMAND(gamemodedesc, ARG_2STR);
|
|
|
|
void resetmap(bool mrproper)
|
|
{
|
|
resetsleep();
|
|
resetzone();
|
|
clearminimap();
|
|
cleardynlights();
|
|
pruneundos();
|
|
changedents.setsize(0);
|
|
particlereset();
|
|
if(mrproper)
|
|
{
|
|
audiomgr.clearworldsounds();
|
|
setvar("gamespeed", 100);
|
|
setvar("paused", 0);
|
|
setvar("fog", 180);
|
|
setvar("fogcolour", 0x8099B3);
|
|
setvar("shadowyaw", 45);
|
|
}
|
|
}
|
|
|
|
int suicided = -1;
|
|
extern bool good_map();
|
|
extern bool item_fail;
|
|
extern int MA, F2F, Ma, Hhits;
|
|
extern float Mh;
|
|
|
|
VARP(mapstats_hud, 0, 0, 1);
|
|
|
|
void showmapstats()
|
|
{
|
|
conoutf("\f2Map Quality Stats");
|
|
conoutf(" The mean height is: %.2f", Mh);
|
|
if (Hhits) conoutf(" Height check is: %d", Hhits);
|
|
if (MA) conoutf(" The max area is: %d (of %d)", MA, Ma);
|
|
if (m_flags && F2F < 1000) conoutf(" Flag-to-flag distance is: %d", (int)fSqrt(F2F));
|
|
if (item_fail) conoutf(" There are one or more items too close to each other in this map");
|
|
}
|
|
COMMAND(showmapstats, ARG_NONE);
|
|
|
|
VARP(showmodedescriptions, 0, 1, 1);
|
|
|
|
void startmap(const char *name, bool reset) // called just after a map load
|
|
{
|
|
copystring(clientmap, name);
|
|
sendmapidenttoserver = true;
|
|
// Added by Rick
|
|
if(m_botmode) BotManager.BeginMap(name);
|
|
else kickallbots();
|
|
// End add by Rick
|
|
clearbounceents();
|
|
preparectf(!m_flags);
|
|
suicided = -1;
|
|
spawncycle = -1;
|
|
lasthit = 0;
|
|
if(m_valid(gamemode) && !m_mp(gamemode)) respawnself();
|
|
else findplayerstart(player1);
|
|
if(good_map()==MAP_IS_BAD) conoutf(_("You cannot play in this map due to quality requisites. Please, report this incident."));
|
|
if (mapstats_hud) showmapstats();
|
|
|
|
if(!reset) return;
|
|
|
|
player1->frags = player1->flagscore = player1->deaths = player1->lifesequence = player1->points = player1->tks = 0;
|
|
loopv(players) if(players[i]) players[i]->frags = players[i]->flagscore = players[i]->deaths = players[i]->lifesequence = players[i]->points = players[i]->tks = 0;
|
|
if(editmode) toggleedit(true);
|
|
intermission = false;
|
|
showscores(false);
|
|
reorderscorecolumns();
|
|
minutesremaining = -1;
|
|
lastgametimeupdate = 0;
|
|
arenaintermission = 0;
|
|
bool noflags = (m_ctf || m_ktf) && (!numflagspawn[0] || !numflagspawn[1]);
|
|
if(*clientmap) conoutf(_("game mode is \"%s\"%s"), modestr(gamemode, modeacronyms > 0), noflags ? " - \f2but there are no flag bases on this map" : "");
|
|
|
|
if(showmodedescriptions && (multiplayer(false) || m_botmode))
|
|
{
|
|
loopv(gmdescs) if(gmdescs[i].mode == gamemode)
|
|
{
|
|
//conoutf(_("%c1%s"), CC, gmdescs[i].desc); // 3rd useless call to translation - these should be translated inside the cube-script-definition
|
|
conoutf("\f1%s", gmdescs[i].desc);
|
|
}
|
|
}
|
|
|
|
// run once
|
|
if(firstrun)
|
|
{
|
|
persistidents = false;
|
|
execfile("config/firstrun.cfg");
|
|
persistidents = true;
|
|
firstrun = false;
|
|
}
|
|
// execute mapstart event once
|
|
const char *mapstartonce = getalias("mapstartonce");
|
|
if(mapstartonce && mapstartonce[0])
|
|
{
|
|
addsleep(0, mapstartonce); // do this as a sleep to make sure map changes don't recurse inside a welcome packet
|
|
// BTW: in v1.0.4 sleep 1 was required to make it work on initial mapload [flowtron:2010jun25]
|
|
alias("mapstartonce", "");
|
|
}
|
|
// execute mapstart event
|
|
const char *mapstartalways = getalias("mapstartalways");
|
|
if(mapstartalways && mapstartalways[0])
|
|
{
|
|
addsleep(0, mapstartalways);
|
|
}
|
|
}
|
|
|
|
void suicide()
|
|
{
|
|
if(player1->state == CS_ALIVE && suicided!=player1->lifesequence)
|
|
{
|
|
addmsg(SV_SUICIDE, "r");
|
|
suicided = player1->lifesequence;
|
|
}
|
|
}
|
|
|
|
COMMAND(suicide, ARG_NONE);
|
|
|
|
// console and audio feedback
|
|
|
|
void flagmsg(int flag, int message, int actor, int flagtime)
|
|
{
|
|
static int musicplaying = -1;
|
|
playerent *act = getclient(actor);
|
|
if(actor != getclientnum() && !act && message != FM_RESET) return;
|
|
bool own = flag == team_base(player1->team);
|
|
bool neutral = team_isspect(player1->team);
|
|
bool firstperson = actor == getclientnum();
|
|
bool teammate = !act ? true : isteam(player1->team, act->team);
|
|
bool firstpersondrop = false;
|
|
defformatstring(ownerstr)("the %s", teamnames[flag]);
|
|
const char *teamstr = m_ktf ? "the" : neutral ? ownerstr : own ? "your" : "the enemy";
|
|
const char *flagteam = (m_ktf && !neutral) ? (teammate ? "your teammate " : "your enemy ") : "";
|
|
|
|
if(identexists("onFlag"))
|
|
{
|
|
defformatstring(onflagevent)("onFlag %d %d %d", message, actor, flag);
|
|
execute(onflagevent);
|
|
}
|
|
|
|
switch(message)
|
|
{
|
|
case FM_PICKUP:
|
|
audiomgr.playsound(S_FLAGPICKUP, SP_HIGHEST);
|
|
if(firstperson)
|
|
{
|
|
hudoutf("\f2you have the %sflag", m_ctf ? "enemy " : "");
|
|
audiomgr.musicsuggest(M_FLAGGRAB, m_ctf ? 90*1000 : 900*1000, true);
|
|
musicplaying = flag;
|
|
}
|
|
else hudoutf("\f2%s%s has %s flag", flagteam, colorname(act), teamstr);
|
|
break;
|
|
case FM_LOST:
|
|
case FM_DROP:
|
|
{
|
|
const char *droplost = message == FM_LOST ? "lost" : "dropped";
|
|
audiomgr.playsound(S_FLAGDROP, SP_HIGHEST);
|
|
if(firstperson)
|
|
{
|
|
hudoutf("\f2you %s the flag", droplost);
|
|
firstpersondrop = true;
|
|
}
|
|
else hudoutf("\f2%s %s %s flag", colorname(act), droplost, teamstr);
|
|
break;
|
|
}
|
|
case FM_RETURN:
|
|
audiomgr.playsound(S_FLAGRETURN, SP_HIGHEST);
|
|
if(firstperson) hudoutf("\f2you returned your flag");
|
|
else hudoutf("\f2%s returned %s flag", colorname(act), teamstr);
|
|
break;
|
|
case FM_SCORE:
|
|
audiomgr.playsound(S_FLAGSCORE, SP_HIGHEST);
|
|
if(firstperson)
|
|
{
|
|
hudoutf("\f2you scored");
|
|
if(m_ctf) firstpersondrop = true;
|
|
}
|
|
else hudoutf("\f2%s scored for %s", colorname(act), neutral ? teamnames[act->team] : teammate ? "your team" : "the enemy team");
|
|
break;
|
|
case FM_KTFSCORE:
|
|
{
|
|
audiomgr.playsound(S_VOTEPASS, SP_HIGHEST); // need better ktf sound here
|
|
const char *ta = firstperson ? "you have" : colorname(act);
|
|
const char *tb = firstperson ? "" : " has";
|
|
const char *tc = firstperson ? "" : flagteam;
|
|
int m = flagtime / 60;
|
|
if(m)
|
|
hudoutf("\f2%s%s%s kept the flag for %d minute%s %d seconds now", tc, ta, tb, m, m == 1 ? "" : "s", flagtime % 60);
|
|
else
|
|
hudoutf("\f2%s%s%s kept the flag for %d seconds now", tc, ta, tb, flagtime);
|
|
break;
|
|
}
|
|
case FM_SCOREFAIL: // sound?
|
|
hudoutf("\f2%s failed to score (own team flag not taken)", firstperson ? "you" : colorname(act));
|
|
break;
|
|
case FM_RESET:
|
|
audiomgr.playsound(S_FLAGRETURN, SP_HIGHEST);
|
|
hudoutf("the server reset the flag");
|
|
firstpersondrop = true;
|
|
break;
|
|
}
|
|
if(firstpersondrop && flag == musicplaying)
|
|
{
|
|
audiomgr.musicfadeout(M_FLAGGRAB);
|
|
musicplaying = -1;
|
|
}
|
|
}
|
|
|
|
void dropflag() { tryflagdrop(true); }
|
|
COMMAND(dropflag, ARG_NONE);
|
|
|
|
char *votestring(int type, const char *arg1, const char *arg2)
|
|
{
|
|
const char *msgs[] = { "kick player %s, reason: %s", "ban player %s, reason: %s", "remove all bans", "set mastermode to %s", "%s autoteam", "force player %s to the enemy team", "give admin to player %s", "load map %s in mode %s%s", "%s demo recording for the next match", "stop demo recording", "clear all demos", "set server description to '%s'", "shuffle teams"};
|
|
const char *msg = msgs[type];
|
|
char *out = newstring(MAXSTRLEN);
|
|
out[MAXSTRLEN] = '\0';
|
|
switch(type)
|
|
{
|
|
case SA_KICK:
|
|
case SA_BAN:
|
|
case SA_FORCETEAM:
|
|
case SA_GIVEADMIN:
|
|
{
|
|
int cn = atoi(arg1);
|
|
playerent *p = getclient(cn);
|
|
if(!p) break;
|
|
if (type == SA_KICK || type == SA_BAN)
|
|
{
|
|
string reason = "";
|
|
if(m_teammode) formatstring(reason)("%s (%d tks, ping %d)", arg2, p->tks, p->ping);
|
|
else formatstring(reason)("%s (ping %d)", arg2, p->ping);
|
|
formatstring(out)(msg, colorname(p), reason);
|
|
}
|
|
else formatstring(out)(msg, colorname(p));
|
|
break;
|
|
}
|
|
case SA_MASTERMODE:
|
|
formatstring(out)(msg, mmfullname(atoi(arg1)));
|
|
break;
|
|
case SA_AUTOTEAM:
|
|
case SA_RECORDDEMO:
|
|
formatstring(out)(msg, atoi(arg1) == 0 ? "disable" : "enable");
|
|
break;
|
|
case SA_MAP:
|
|
{
|
|
int n = atoi(arg2);
|
|
if ( n >= GMODE_NUM )
|
|
{
|
|
formatstring(out)(msg, arg1, modestr(n-GMODE_NUM, modeacronyms > 0)," (in the next game)");
|
|
}
|
|
else
|
|
{
|
|
formatstring(out)(msg, arg1, modestr(n, modeacronyms > 0),"");
|
|
}
|
|
break;
|
|
}
|
|
case SA_SERVERDESC:
|
|
formatstring(out)(msg, arg1);
|
|
break;
|
|
default:
|
|
formatstring(out)(msg, arg1, arg2);
|
|
break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
votedisplayinfo *newvotedisplayinfo(playerent *owner, int type, const char *arg1, const char *arg2)
|
|
{
|
|
if(type < 0 || type >= SA_NUM) return NULL;
|
|
votedisplayinfo *v = new votedisplayinfo();
|
|
v->owner = owner;
|
|
v->type = type;
|
|
v->millis = totalmillis + (30+10)*1000;
|
|
char *votedesc = votestring(type, arg1, arg2);
|
|
copystring(v->desc, votedesc);
|
|
DELETEA(votedesc);
|
|
return v;
|
|
}
|
|
|
|
votedisplayinfo *curvote = NULL, *calledvote = NULL;
|
|
|
|
void callvote(int type, const char *arg1, const char *arg2)
|
|
{
|
|
if(calledvote) return;
|
|
votedisplayinfo *v = newvotedisplayinfo(player1, type, arg1, arg2);
|
|
if(v)
|
|
{
|
|
calledvote = v;
|
|
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
|
|
putint(p, SV_CALLVOTE);
|
|
putint(p, v->type);
|
|
switch(v->type)
|
|
{
|
|
case SA_KICK:
|
|
case SA_BAN:
|
|
putint(p, atoi(arg1));
|
|
sendstring(arg2, p);
|
|
break;
|
|
case SA_MAP:
|
|
sendstring(arg1, p);
|
|
putint(p, nextmode);
|
|
break;
|
|
case SA_SERVERDESC:
|
|
sendstring(arg1, p);
|
|
break;
|
|
case SA_STOPDEMO:
|
|
case SA_REMBANS:
|
|
case SA_SHUFFLETEAMS:
|
|
break;
|
|
default:
|
|
putint(p, atoi(arg1));
|
|
break;
|
|
}
|
|
sendpackettoserv(1, p.finalize());
|
|
if(identexists("onCallVote"))
|
|
{
|
|
defformatstring(runas)("%s %d %d [%s] [%s]", "onCallVote", type, -1, arg1, arg2);
|
|
execute(runas);
|
|
}
|
|
}
|
|
else conoutf(_("%c3invalid vote"), CC);
|
|
}
|
|
|
|
void scallvote(char *type, const char *arg1, const char *arg2)
|
|
{
|
|
if(type && inmainloop)
|
|
{
|
|
int t = atoi(type);
|
|
switch (t)
|
|
{
|
|
case SA_MAP: // FIXME
|
|
{
|
|
string n;
|
|
itoa(n, nextmode);
|
|
callvote(t, arg1, n);
|
|
break;
|
|
}
|
|
case SA_KICK:
|
|
case SA_BAN:
|
|
{
|
|
if (!arg1 || !isdigit(arg1[0]) || !arg2 || strlen(arg2) <= 3 || !multiplayer(false))
|
|
{
|
|
if(!multiplayer(false))
|
|
conoutf(_("%c3%s is not available in singleplayer."), CC, t == SA_BAN ? "Ban" : "Kick");
|
|
else if(arg1 && !isdigit(arg1[0])) conoutf(_("%c3invalid vote"), CC);
|
|
else conoutf(_("%c3invalid reason"), CC);
|
|
break;
|
|
}
|
|
}
|
|
default:
|
|
callvote(t, arg1, arg2);
|
|
}
|
|
}
|
|
}
|
|
|
|
int vote(int v)
|
|
{
|
|
if(!curvote || v < 0 || v >= VOTE_NUM) return 0;
|
|
if(curvote->localplayervoted) { conoutf(_("%c3you voted already"), CC); return 0; }
|
|
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
|
|
putint(p, SV_VOTE);
|
|
putint(p, v);
|
|
sendpackettoserv(1, p.finalize());
|
|
if(!curvote) return 0;
|
|
curvote->stats[v]++;
|
|
curvote->localplayervoted = true;
|
|
return 1;
|
|
}
|
|
|
|
VAR(votepending, 1, 0, 0);
|
|
|
|
void displayvote(votedisplayinfo *v)
|
|
{
|
|
if(!v) return;
|
|
DELETEP(curvote);
|
|
curvote = v;
|
|
conoutf(_("%s called a vote: %s"), v->owner ? colorname(v->owner) : "", curvote->desc);
|
|
audiomgr.playsound(S_CALLVOTE, SP_HIGHEST);
|
|
curvote->localplayervoted = false;
|
|
votepending = 1;
|
|
}
|
|
|
|
void callvotesuc()
|
|
{
|
|
if(!calledvote) return;
|
|
displayvote(calledvote);
|
|
calledvote = NULL;
|
|
vote(VOTE_YES); // not automatically done by callvote to keep a clear sequence
|
|
}
|
|
|
|
void callvoteerr(int e)
|
|
{
|
|
if(e < 0 || e >= VOTEE_NUM) return;
|
|
conoutf(_("%c3could not vote: %s"), CC, voteerrorstr(e));
|
|
DELETEP(calledvote);
|
|
}
|
|
|
|
void votecount(int v) { if(curvote && v >= 0 && v < VOTE_NUM) curvote->stats[v]++; }
|
|
void voteresult(int v)
|
|
{
|
|
if(curvote && v >= 0 && v < VOTE_NUM)
|
|
{
|
|
curvote->result = v;
|
|
curvote->millis = totalmillis + 5000;
|
|
conoutf(_("vote %s"), v == VOTE_YES ? _("passed") : _("failed"));
|
|
if(multiplayer(false)) audiomgr.playsound(v == VOTE_YES ? S_VOTEPASS : S_VOTEFAIL, SP_HIGH);
|
|
if(identexists("onVoteEnd")) execute("onVoteEnd");
|
|
votepending = 0;
|
|
}
|
|
}
|
|
|
|
void clearvote() { DELETEP(curvote); DELETEP(calledvote); }
|
|
|
|
const char *modestrings[] =
|
|
{
|
|
"tdm", "coop", "dm", "lms", "ts", "ctf", "pf", "btdm", "bdm", "lss",
|
|
"osok", "tosok", "bosok", "htf", "tktf", "ktf", "tpf", "tlss", "bpf", "blss", "btsurv", "btosok"
|
|
};
|
|
|
|
void setnext(char *arg1, char *arg2)
|
|
{
|
|
for (int i = 0; i < GMODE_NUM; i++)
|
|
{
|
|
switch(i)
|
|
{
|
|
case GMODE_COOPEDIT:
|
|
case GMODE_BOTTEAMDEATHMATCH:
|
|
case GMODE_BOTDEATHMATCH:
|
|
case GMODE_BOTONESHOTONEKILL:
|
|
continue;
|
|
}
|
|
if ( !strcmp(arg1,modestrings[i]) )
|
|
{
|
|
string n;
|
|
itoa(n, i+GMODE_NUM);
|
|
nextmode=i+GMODE_NUM;
|
|
callvote(SA_MAP, arg2, n);
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
COMMAND(setnext, ARG_2STR);
|
|
|
|
void gonext(char *arg1)
|
|
{
|
|
if(calledvote || !multiplayer(false)) return;
|
|
packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
|
|
putint(p, SV_CALLVOTE);
|
|
putint(p, SA_MAP);
|
|
sendstring("+1", p);
|
|
putint(p, atoi(arg1));
|
|
sendpackettoserv(1, p.finalize());
|
|
}
|
|
COMMAND(gonext, ARG_1STR);
|
|
|
|
COMMANDN(callvote, scallvote, ARG_3STR); //fixme,ah
|
|
COMMAND(vote, ARG_1EXP);
|
|
|
|
void cleanplayervotes(playerent *p)
|
|
{
|
|
if(calledvote && calledvote->owner==p) calledvote->owner = NULL;
|
|
if(curvote && curvote->owner==p) curvote->owner = NULL;
|
|
}
|
|
|
|
void whois(int cn)
|
|
{
|
|
addmsg(SV_WHOIS, "ri", cn);
|
|
}
|
|
COMMAND(whois, ARG_1INT);
|
|
|
|
void findcn(char *name)
|
|
{
|
|
loopv(players) if(players[i] && !strcmp(name, players[i]->name))
|
|
{
|
|
intret(players[i]->clientnum);
|
|
return;
|
|
}
|
|
if(!strcmp(name, player1->name)) { intret(player1->clientnum); return; }
|
|
intret(-1);
|
|
}
|
|
COMMAND(findcn, ARG_1STR);
|
|
|
|
void findpn(int cn)
|
|
{
|
|
loopv(players) if(players[i] && players[i]->clientnum == cn)
|
|
{
|
|
result(players[i]->name);
|
|
return;
|
|
}
|
|
if(player1->clientnum == cn) { result(player1->name); return; }
|
|
result("");
|
|
}
|
|
COMMAND(findpn, ARG_1INT);
|
|
|
|
int sessionid = 0;
|
|
|
|
void setadmin(char *claim, char *password)
|
|
{
|
|
if(!claim && !password) return;
|
|
else
|
|
{
|
|
int y = atoi(claim);
|
|
if((y != 1) && (player1->clientrole))
|
|
{
|
|
conoutf(_("you released admin status"));
|
|
addmsg(SV_SETADMIN, "ri", 0);
|
|
}
|
|
else
|
|
addmsg(SV_SETADMIN, "ris", y, genpwdhash(player1->name, password, sessionid));
|
|
}
|
|
}
|
|
|
|
COMMAND(setadmin, ARG_2STR);
|
|
|
|
struct mline { string name, cmd; };
|
|
static vector<mline> mlines;
|
|
|
|
void *kickmenu = NULL, *banmenu = NULL, *forceteammenu = NULL, *giveadminmenu = NULL;
|
|
|
|
void refreshsopmenu(void *menu, bool init)
|
|
{
|
|
menureset(menu);
|
|
mlines.shrink(0);
|
|
mlines.reserve(players.length());
|
|
loopv(players) if(players[i])
|
|
{
|
|
mline &m = mlines.add();
|
|
copystring(m.name, colorname(players[i]));
|
|
string kbr;
|
|
if(getalias("_kickbanreason")!=NULL) formatstring(kbr)(" [ %s ]", getalias("_kickbanreason")); // leading space!
|
|
formatstring(m.cmd)("%s %d%s", menu==kickmenu ? "kick" : (menu==banmenu ? "ban" : (menu==forceteammenu ? "forceteam" : "giveadmin")), i, (menu==kickmenu||menu==banmenu)?(strlen(kbr)>8?kbr:" NONE"):""); // 8==3 + "format-extra-chars"
|
|
menumanual(menu, m.name, m.cmd);
|
|
}
|
|
}
|
|
|
|
extern bool watchingdemo;
|
|
|
|
// rotate through all spec-able players
|
|
playerent *updatefollowplayer(int shiftdirection)
|
|
{
|
|
if(!shiftdirection)
|
|
{
|
|
playerent *f = players.inrange(player1->followplayercn) ? players[player1->followplayercn] : NULL;
|
|
if(f && (watchingdemo || !f->isspectating())) return f;
|
|
}
|
|
|
|
// collect spec-able players
|
|
vector<playerent *> available;
|
|
loopv(players) if(players[i])
|
|
{
|
|
if(player1->team != TEAM_SPECT && !watchingdemo && m_teammode && team_base(players[i]->team) != team_base(player1->team)) continue;
|
|
if(players[i]->state==CS_DEAD || players[i]->isspectating()) continue;
|
|
available.add(players[i]);
|
|
}
|
|
if(!available.length()) return NULL;
|
|
|
|
// rotate
|
|
int oldidx = -1;
|
|
if(players.inrange(player1->followplayercn)) oldidx = available.find(players[player1->followplayercn]);
|
|
if(oldidx<0) oldidx = 0;
|
|
int idx = (oldidx+shiftdirection) % available.length();
|
|
if(idx<0) idx += available.length();
|
|
|
|
player1->followplayercn = available[idx]->clientnum;
|
|
return players[player1->followplayercn];
|
|
}
|
|
|
|
void spectate()
|
|
{
|
|
if(m_demo) return;
|
|
if(!team_isspect(player1->team)) addmsg(SV_SWITCHTEAM, "ri", TEAM_SPECT);
|
|
else tryrespawn();
|
|
}
|
|
|
|
void setfollowplayer(int cn)
|
|
{
|
|
// silently ignores invalid player-cn value passed
|
|
if(players.inrange(cn))
|
|
{
|
|
if(!(m_teammode && !watchingdemo && team_base(players[cn]->team) != team_base(player1->team)))
|
|
{
|
|
player1->followplayercn = cn;
|
|
if(player1->spectatemode == SM_FLY) player1->spectatemode = SM_FOLLOW1ST;
|
|
}
|
|
}
|
|
}
|
|
|
|
// set new spect mode
|
|
void spectatemode(int mode)
|
|
{
|
|
if(!m_teammode && !team_isspect(player1->team) && servstate.mastermode == MM_MATCH) return; // during ffa matches only SPECTATORS can spectate
|
|
if(mode == player1->spectatemode) return;
|
|
showscores(false);
|
|
switch(mode)
|
|
{
|
|
case SM_FOLLOW1ST:
|
|
case SM_FOLLOW3RD:
|
|
case SM_FOLLOW3RD_TRANSPARENT:
|
|
{
|
|
if(players.length() && updatefollowplayer()) break;
|
|
else mode = SM_FLY;
|
|
}
|
|
case SM_FLY:
|
|
{
|
|
if(player1->spectatemode != SM_FLY)
|
|
{
|
|
playerent *f = getclient(player1->followplayercn);
|
|
if(f)
|
|
{
|
|
player1->o = f->o;
|
|
player1->yaw = f->yaw;
|
|
player1->pitch = 0.0f;
|
|
player1->resetinterp();
|
|
}
|
|
else entinmap(player1); // or drop 'em at a random place
|
|
player1->followplayercn = FPCN_FLY;
|
|
}
|
|
break;
|
|
}
|
|
case SM_OVERVIEW:
|
|
player1->followplayercn = FPCN_OVERVIEW;
|
|
break;
|
|
default: break;
|
|
}
|
|
player1->spectatemode = mode;
|
|
}
|
|
|
|
void togglespect() // cycle through all spectating modes
|
|
{
|
|
if(m_botmode)
|
|
spectatemode(SM_FLY);
|
|
else
|
|
{
|
|
int mode;
|
|
if(player1->spectatemode==SM_NONE) mode = SM_FOLLOW1ST; // start with 1st person spect
|
|
else mode = SM_FOLLOW1ST + ((player1->spectatemode - SM_FOLLOW1ST + 1) % (SM_OVERVIEW-SM_FOLLOW1ST)); // replace SM_OVERVIEW by SM_NUM to enable overview mode
|
|
spectatemode(mode);
|
|
}
|
|
}
|
|
|
|
void changefollowplayer(int shift)
|
|
{
|
|
updatefollowplayer(shift);
|
|
}
|
|
|
|
COMMAND(spectate, ARG_NONE);
|
|
COMMAND(spectatemode, ARG_1INT);
|
|
COMMAND(togglespect, ARG_NONE);
|
|
COMMAND(changefollowplayer, ARG_1INT);
|
|
COMMAND(setfollowplayer, ARG_1INT);
|
|
|
|
int isalive() { return player1->state==CS_ALIVE ? 1 : 0; }
|
|
COMMANDN(alive, isalive, ARG_IVAL);
|
|
|
|
void serverextension(char *ext, char *args)
|
|
{
|
|
if(!ext || !ext[0]) return;
|
|
size_t n = args ? strlen(args)+1 : 0;
|
|
if(n>0) addmsg(SV_EXTENSION, "rsis", ext, n, args);
|
|
else addmsg(SV_EXTENSION, "rsi", ext, n);
|
|
}
|
|
|
|
COMMAND(serverextension, ARG_2STR);
|