AssaultCube Reloaded Wiki
// clientgame.cpp: core game related stuff

#include "cube.h"
#include "bot/bot.h"

int nextmode = G_DM, nextmuts = G_M_TEAM;   // nextmode becomes gamemode after next map load
VAR(gamemode, 1, 0, 0);
VAR(mutators, 1, 0, 0);
//VAR(nextGameMode, 1, 0, 0);
VARP(modeacronyms, 0, 1, 1);

flaginfo flaginfos[2];

void mode(int *n)
{
    nextmode = *n;
    modecheck(nextmode, nextmuts);
    //nextGameMode = nextmode;
}
COMMAND(mode, "i");

void muts(int *n)
{
    nextmuts = *n;
    modecheck(nextmode, nextmuts);
}
COMMAND(muts, "i");

void classicmode(int *n)
{
    static const int cmodes[][2] = {
        { G_DM, G_M_TEAM },
        { G_DM, G_M_NONE },

        { G_DM, G_M_GSP1 | G_M_TEAM | G_M_INSTA },
        { G_DM, G_M_GSP1 | G_M_INSTA },

        { G_CTF, G_M_TEAM },
        { G_BOMBER, G_M_TEAM },
        { G_HTF, G_M_TEAM },
        { G_KTF, G_M_GSP1 },
        { G_KTF, G_M_GSP1 | G_M_TEAM },

        { G_DM, G_M_PISTOL },
        { G_DM, G_M_GSP1 | G_M_GIB },

        { G_DM, G_M_GSP1 | G_M_TEAM },
        { G_DM, G_M_GSP1 },
    };
    if (*n >= 0 && *n < (int)(sizeof(cmodes) / sizeof(*cmodes)))
        modecheck(nextmode = cmodes[*n][0], nextmuts = cmodes[*n][1]);
}
COMMAND(classicmode, "i");

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;
int curtime_real = 0;
string clientmap = "";

char *getclientmap() { return clientmap; }

int getclientmode() { return gamemode; }

int getclientmutators() { return mutators; }

extern bool sendmapidenttoserver;

void setskin(playerent *pl, int skin, int team)
{
    if(!pl) return;
    pl->setskin(team, skin);
}

char *colorname(playerent *d)
{
    static string cname[4];
    static int num = 0;
    num = (num + 1) % 4;
    if(d->ownernum < 0)
        formatstring(cname[num])("%s \fs\f6(%d)\fr", d->name, d->clientnum);
    else
        formatstring(cname[num])("%s \fs\f7[%d-%d]\fr", d->name, d->clientnum, d->ownernum);
    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(int *cn)
{
    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 *cn)
{
    loopv(players) if(players[i] && (*cn < 0 || *cn == i)) players[i]->muted = false;
}

COMMAND(ignore, "i");
COMMAND(listignored, "");
COMMAND(clearignored, "i");
COMMAND(muteplayer, "i");
COMMAND(listmuted, "");
COMMAND(clearmuted, "i");

void newname(const char *name)
{
    if(name[0])
    {
        string tmpname;
        filtername(tmpname, name);
        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, "unnamed");
        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)
{
    // team number
    if (name[1] == '\0' && name[0] >= '0' && name[0] < ('0' + TEAM_NUM))
        return name[0] - '0';
    // team names
    loopi(TEAM_NUM) if(!strcasecmp(teamnames[i], name)) return i;
    // unknown
    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(player1->state == CS_EDITING) conoutf(_("you can't change team while editing"));
        else addmsg(SV_SETTEAM, "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_SETTEAM, "ri", team_tospec(player1->team));
}

int _setskin(int s, int t)
{
    setskin(player1, s, t);
    addmsg(SV_SWITCHSKIN, "rii", player1->skin(0), player1->skin(1));
    return player1->skin(t);
}

COMMANDF(skin_cla, "i", (int *s) { intret(_setskin(*s, TEAM_CLA)); });
COMMANDF(skin_rvsf, "i", (int *s) { intret(_setskin(*s, TEAM_RVSF)); });
COMMANDF(skin, "i", (int *s) { intret(_setskin(*s, player1->team)); });

void curmodeattr(char *attr)
{
    if(!strcmp(attr, "team")) { intret(m_team(gamemode, mutators)); return; }
    else if(!strcmp(attr, "arena")) { intret(m_duke(gamemode, mutators)); return; }
    else if(!strcmp(attr, "flag")) { intret(m_flags(gamemode)); return; }
    else if(!strcmp(attr, "bot")) { intret(m_ai(gamemode)); return; }
    intret(0);
}

COMMANDN(team, newteam, "s");
COMMANDN(name, newname, "s");
COMMAND(benchme, "");
COMMANDF(isclient, "i", (int *cn) { intret(getclient(*cn) != NULL ? 1 : 0); } );
COMMANDF(curmastermode, "", (void) { intret(servstate.mastermode); });
COMMANDF(curautoteam, "", (void) { intret(servstate.autoteam); });
COMMAND(curmodeattr, "s");
COMMANDF(curmap, "i", (int *cleaned) { result(*cleaned ? behindpath(getclientmap()) : getclientmap()); });
COMMANDF(curplayers, "", (void) { intret(players.length() + 1); });
VARP(showscoresondeath, 0, 1, 1);
VARP(autoscreenshot, 0, 0, 1);

void stopdemo()
{
    if(watchingdemo) enddemoplayback();
    else conoutf(_("not playing a demo"));
}
COMMAND(stopdemo, "");

// 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(int *cn, const char *attr)
{
    if(!*attr) return;

    int clientnum = *cn; // get player clientnum
    playerent *p = clientnum < 0 ? player1 : getclient(clientnum);
    if(!p)
    {
        if(!m_ai(gamemode) && 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_team(gamemode, mutators))
        || player1->team == TEAM_SPECT
        || m_edit(gamemode))
    {
        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(assists, p->assists);
    ATTR_INT(flags, p->flagscore);
    ATTR_INT(points, p->points);
    ATTR_INT(deaths, p->deaths);
    ATTR_INT(alive, p->state == CS_ALIVE ? 1 : 0);
    ATTR_INT(spect, p->team == TEAM_SPECT || p->spectatemode == SM_FLY ? 1 : 0);
    ATTR_INT(cn, p->clientnum); // only useful to get player1's client number.
    ATTR_INT(skin_cla, p->skin(TEAM_CLA));
    ATTR_INT(skin_rvsf, p->skin(TEAM_RVSF));
    ATTR_INT(skin, p->skin(player1->team));

    conoutf("invalid attribute: %s", attr);
}

void playerinfolocal(const char *attr)
{
    int cn = -1;
    playerinfo(&cn, attr);
}

COMMANDN(player, playerinfo, "is");
COMMANDN(player1, playerinfolocal, "s");

void teaminfo(const char *team, const char *attr)
{
    if(!team || !attr || !m_team(gamemode, mutators)) return;
    int t = teamatoi(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, "ss");

VARP(noob, 0, 0, 1);
VARFP(level, 1, 1, MAXLEVEL, addmsg(SV_LEVEL, "ri", level));
VARFP(experience, 0, 0, MAXEXP, addexp(0));

int lastexpadd = INT_MIN, lastexpaddamt = 0;
void addexp(int xp)
{
    if (xp)
    {
        if (lastmillis <= lastexpadd + COMBOTIME)
            lastexpaddamt += xp;
        else
            lastexpaddamt = xp;
        lastexpadd = lastmillis;
    }
    // no experience "boost" from negative points
    if (xp < 0)
        return;
    xp = xp * xp;
    if (noob) xp <<= 1;
#define xpfactor ((float)clamp(level, 1, 20))
    experience += fabs((float)xp / xpfactor);
    if (experience >= MAXEXP)
    {
        level = clamp(level + 1, 1, MAXLEVEL);
        addmsg(SV_LEVEL, "ri", level);
        experience = max(0.f, (experience - MAXEXP) / xpfactor);
    }
#undef xpfactor
}


int lastexptexttime = INT_MIN;
string lastexptext;

void expreason(const char *reason)
{
    formatstring(lastexptext)(*reason == '\f' ? "%s" : "\f2%s", reason);
    lastexptexttime = lastmillis;
}
COMMAND(expreason, "s");

bool spawnenqueued = false;

VARP(autospectate, 0, 1, 4); // 0: off, 1: same, 2: alt, +2 with bots
void deathstate(playerent *pl)
{
    if (pl == player1)
    {
        if (showscoresondeath) showscores(true);
        setscope(false);
        setburst(false);
        if (editmode) toggleedit(true);
        damageblend(-1);
        spawnenqueued = false;
    }

    pl->state = CS_DEAD;
    pl->spectatemode = SM_DEATHCAM;
    pl->respawnoffset = pl->lastpain = lastmillis;
    pl->move = pl->strafe = 0;
    pl->roll = 0;
    pl->zoomed = pl->pitchvel = /*pl->pitchreturn =*/ 0;
    pl->attacking = pl->scoping = false;
    pl->weaponsel->onownerdies();
    pl->radarmillis = pl->nametagmillis = 0;

    if(pl==player1)
    {
        if (autospectate)
        {
            playerent *killer = getclient(pl->lastkiller);
            if (!killer || killer == pl || (autospectate <= 2 && killer->ownernum >= 0))
                goto NOAUTOSPECTATE;
            pl->spectatemode = (autospectate & 1) ? SM_FOLLOWSAME : SM_FOLLOWALT;
            focus = killer;
        }
        else
        {
            NOAUTOSPECTATE:
            if (pl->lastkiller == pl->clientnum)
            {
                pl->o.z += pl->eyeheight - 1.0f;
                pl->spectatemode = SM_FLY;
            }
            else
                pl->spectatemode = SM_DEATHCAM;
        }
    }
    else pl->resetinterp();
}

void spawnstate(playerent *d)              // reset player state not persistent accross spawns
{
    d->respawn(gamemode, mutators);
    d->spawnstate(d->team, gamemode, mutators);
    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;
}

VAR(lastpm, 1, -1, 0);
void zapplayer(playerent *&d)
{
    if(d && d->clientnum == lastpm) lastpm = -1;
    if(focus == d) focus = player1;
    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 && !isowned(players[i]))
    {
        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_WAITING;
            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);
    }
}

void updateradarpos()
{
    const bool has_radar = radarup(focus) || focus->team == TEAM_SPECT;
    const int interval = has_radar ? 400 : m_capture(gamemode) ? 4000 : 750;
    loopv(players)
    {
        playerent *d = players[i];
        if (!d || d == focus) continue;

        // pointstreak or selected enemy
        if (has_radar || d == worldhit)
            goto UPDATE_POSITION;

        // flag carrier
        if ((flaginfos[0].state == CTFF_STOLEN && flaginfos[0].actor == d) ||
            (flaginfos[1].state == CTFF_STOLEN && flaginfos[1].actor == d))
        {
            int nextupdate = d->radarmillis + interval - d->radarmillis % interval;
            if (totalmillis >= nextupdate)
                goto UPDATE_POSITION;
        }

        if (isteam(focus, d))
        {
            // visible teammates (not for radar, but for nametags)
            if (IsVisible(focus->o, d->o))
                goto UPDATE_POSITION;
        }
        else if (d->perk1 != PERK_NINJA) // enemys who don't have ninja
        {
            // radar and visible
            if (focus->perk2 == PERK_RADAR && IsVisible(focus->o, d->o))
                goto UPDATE_POSITION;

            // teammates with radar
            loopvj(players)
            {
                playerent *pll = players[j];
                if (!pll || focus == pll || !isteam(focus, pll) || (pll->state != CS_ALIVE && pll->state != CS_EDITING) || pll->perk2 != PERK_RADAR)
                    continue;
                if (IsVisible(pll->o, d->o))
                    goto UPDATE_POSITION;
            }
        }
        continue;

        UPDATE_POSITION:
        d->updateradarpos(totalmillis, true);
    }
}


bool showhudtimer(int maxmillis, int startmillis, const char *msg, bool flash)
{
    static int lasttick = 0;
    if (lasttick > startmillis + maxmillis) return false;
    lasttick = lastmillis;
    const bool wave = m_progressive(gamemode, mutators), queued = !wave && spawnenqueued && !m_duke(gamemode, mutators);
    defformatstring(str)("\f%s %.1fs", _(wave ? "1Next wave in" : queued ? "2Queued for spawn:" : "3Waiting for respawn:"), (startmillis + maxmillis - lastmillis) / 1000.f);
    if (lastmillis <= startmillis + maxmillis) hudeditf(HUDMSG_TIMER | HUDMSG_OVERWRITE, flash || wave || queued ? str : str + 2);
    else hudeditf(HUDMSG_TIMER, msg);
    return true;
}

int lastspawnattempt = 0;

void showrespawntimer()
{
    if(intermission) return;
    if (m_duke(gamemode, mutators) || (m_convert(gamemode, mutators) && arenaintermission))
    {
        if(!arenaintermission) return;
        showhudtimer(5000, arenaintermission, _("FIGHT!"), lastspawnattempt >= arenaintermission && lastmillis < lastspawnattempt+100);
    }
    else if(player1->state==CS_DEAD)
        showhudtimer(SPAWNDELAY, 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_(int *msec, char *cmd, int *persist)
{
    addsleep(*msec, cmd, *persist != 0);
}

void resetsleep(bool force)
{
    loopv(sleeps) if(!sleeps[i].persist || force)
    {
        DELETEA(sleeps[i].cmd);
        sleeps.remove(i);
    }
}

COMMANDN(sleep, addsleep_, "isi");
COMMANDF(resetsleeps, "", (void) { resetsleep(true); });

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();
    updateradarpos();
    gets2c();
    showrespawntimer();

    // Added by Rick: let bots think
    if(m_ai(gamemode)) BotManager.Think();

    movelocalplayer();
    c2sinfo();   // do this last, to reduce the effective frame lag
}

void radarinfo(int &total, playerent *&last, int &lastremain, const playerent *p)
{
    // we return with the parameters!
    total = 0;
    last = NULL;
    lastremain = 0;
    // loop through players
    loopi(players.length() + 1)
    {
        playerent *pl = players.inrange(i) ? players[i] : player1;
        if (!pl) continue; // null
        if (pl->radarearned <= lastmillis) continue; // no radar!
        if (p && p != pl && p->team != TEAM_SPECT && !isteam(p, pl)) continue; // not the same team
        // add to total
        ++total;
        // we want the HIGHEST number possible
        if (pl->radarearned > lastmillis + lastremain)
        {
            lastremain = pl->radarearned - lastmillis;
            last = pl;
        }
    }
}

bool radarup(playerent *p)
{
    loopi(players.length() + 1)
    {
        playerent *pl = players.inrange(i) ? players[i] : player1;
        if (!pl) continue; // null
        if (pl->radarearned <= lastmillis) continue; // no radar!
        if (p != pl && p->team != TEAM_SPECT && !isteam(p, pl)) continue; // not the same team
        // add to total
        return true;
    }
    return false;
}

void nukeinfo(int &total, playerent *&first, int &firstremain)
{
    total = 0;
    first = NULL;
    firstremain = 0;
    // loop through players
    loopi(players.length() + 1)
    {
        playerent *pl = players.inrange(i) ? players[i] : player1;
        if (!pl) continue; // null
        if (pl->nukemillis <= lastmillis) continue; // no upcoming nuke
        // add to total
        ++total;
        // we want the LEAST number possible
        if (!firstremain || pl->nukemillis < lastmillis + firstremain)
        {
            firstremain = pl->nukemillis - lastmillis;
            first = pl;
        }
    }
}

void respawnself()
{
    addmsg(SV_TRYSPAWN, "r");
    spawnenqueued = !spawnenqueued;
}

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 (!m_edit(gamemode) && ( Mh >= MAXMHEIGHT || MA >= MAXMAREA ));
}

void tryrespawn()
{
    if ( multiplayer(false) && bad_map() )
    {
        hudoutf("This map is not supported in multiplayer. Read the docs about map quality/dimensions.");
    }
    else if(player1->state==CS_DEAD)
    {
        respawnself();
        int respawnmillis = player1->respawnoffset + (m_duke(gamemode, mutators) ? 0 : SPAWNDELAY);
        if (lastmillis > respawnmillis)
        {
            player1->attacking = false;
            if (m_duke(gamemode, mutators))
            {
                if (!arenaintermission) hudeditf(HUDMSG_TIMER, "waiting for new round to start...");
                else lastspawnattempt = lastmillis;
            }
        }
        else lastspawnattempt = lastmillis;
    }
}

VARP(hitsound, 0, 0, 1);

void burstshots(int gun, int shots)
{
    // args are passed as strings to differentiate 2 cases : shots_str == "0" or shots_str is empty (not specified from cubescript).
    if(gun >= 0 && gun < NUMGUNS && guns[gun].isauto)
    {
        if(shots >= 0) burstshotssettings[gun] = min(shots, (guns[gun].magsize-1));
        else intret(burstshotssettings[gun]);
    }
    else conoutf(_("invalid gun specified"));
}

COMMANDF(burstshots, "ii", (int *g, int *s) { burstshots(*g, *s); });

// damage arriving from the network, monsters, yourself, all ends up here.

void dodamage(int damage, playerent *pl, playerent *actor, int gun, int style, const vec &src)
{
    if(pl->state != CS_ALIVE || intermission) return;
    pl->respawnoffset = pl->lastpain = lastmillis;
    if (pl != actor)
        actor->lasthit = lastmillis;

    if(identexists("onHit"))
    {
        defformatstring(o)("onHit %d %d %d %d %d", actor->clientnum, pl->clientnum, damage, gun, style);
        execute(o);
    }
    if(actor==focus && pl!=actor)
    {
        if( hitsound && lasthit != lastmillis) audiomgr.playsound(S_HITSOUND, SP_HIGH);
        lasthit = lastmillis;
    }

    // damage direction/hit push
    if (pl != actor || gun == GUN_GRENADE || gun == GUN_RPG || pl->o.dist(src) > 4)
    {
        // damage indicator
        pl->damagestack.add(damageinfo(src, lastmillis, damage));
        // push
        vec dir = pl->o;
        dir.sub(src).normalize();
        pl->hitpush(damage, dir, actor, gun);
    }

    // critical damage
    if(style & FRAG_CRIT)
    {
        actor->addicon(eventicon::CRITICAL);
        pl->addicon(eventicon::CRITICAL);
    }

    // roll if you are hit
    if(pl==player1 || isowned(pl))
    {
        pl->damageroll(damage);
        if(pl==player1) damageblend(damage);
    }

    // sound
    if (pl == focus) audiomgr.playsound(S_PAIN6, SP_HIGH);
    else audiomgr.playsound(S_PAIN1 + rnd(5), pl);
}

void dokill(playerent *pl, playerent *act, int gun, int style, int damage, int combo, int assist, float dist)
{
    if(intermission) return;

    if(identexists("onKill"))
    {
        defformatstring(killevent)("onKill %d %d %d %d", act->clientnum, pl->clientnum, gun, style);
        execute(killevent);
    }

    // set last killer for the client's killcams
    pl->lastkiller = (gun == OBIT_ASSIST ? pl : act)->clientnum;

    const bool headshot = isheadshot(gun, style);

    // add gib
    if (style & FRAG_GIB) addgib(pl);

    int icon = -1, sound = S_NULL;
    // sounds/icons, by priority
    if (style & FRAG_FIRST)
    {
        sound = S_FIRSTBLOOD;
        icon = eventicon::FIRSTBLOOD;
    }
    else if (headshot && gun != GUN_SHOTGUN && gun != GUN_SHOTGUN_PRO) // shotgun doesn't count as a 'real' headshot
    {
        sound = S_HEADSHOT;
        icon = eventicon::HEADSHOT;
        pl->addicon(eventicon::DECAPITATED); // both get headshot info icon
    }
    else if (style & FRAG_CRIT) icon = eventicon::CRITICAL;

    // dis/play it!
    if (icon >= 0) act->addicon(icon);
    if (sound != S_NULL)
    {
        audiomgr.playsound(sound, act, act == focus ? SP_HIGHEST : SP_HIGH);
        if (pl->o.dist(act->o) >= 4)
            audiomgr.playsound(sound, pl, pl == focus ? SP_HIGHEST : SP_HIGH); // both get sounds if 1 meter apart...
    }

    // killfeed
    addobit(act, gun, style, headshot, pl, combo, assist);

    // sound
    audiomgr.playsound(S_DIE1 + rnd(2), pl);

    if (pl == act)
    {
        // suicide
        if (pl == focus)
        {
            // do a radar scan if the local player suicided
            loopv(players)
            {
                playerent *p = players[i];
                if (!p) continue;

                p->updateradarpos(totalmillis + 1000);
            }
        }
        pl->weapstats[pl->gunselect].kills--;
        pl->weapstats[pl->gunselect].deaths++;
    }
    else
    {
        if (gun >= 0 && gun < NUMGUNS)
        {
            act->weapstats[gun].kills++;
            pl->weapstats[pl->gunselect].deaths++;
        }
    }
    // deathstreak
    /*
    if (pl != act)
    {
        ++pl->deathstreak;
        act->deathstreak = 0;
    }
    pl->pointstreak = 0;
    while (pl->damagelog.length())
    {
        playerent *p = getclient(pl->damagelog.pop());
        if (!p) continue;
        p->pointstreak += isteam(p->team, pl->team) ? -2 : 2;
    }
    */

    // death state
    deathstate(pl);
}

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, "i");

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;
    }
    if(cn == getclientnum()) return player1;
    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.actor = NULL;
    f.actor_cn = -1;
    f.state = m_keep(gamemode) ? 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.attr2 < 2)
            {
                e.spawned = true;
                if(e.attr2>=2) { conoutf(_("%c3invalid ctf-flag entity (%i as %d)"), CC, i, e.attr2); 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 mdesc { int mode, muts; char *desc; };
vector<mdesc> gmdescs, mutdescs, gspdescs;

void gamemodedesc(int *modenr, char *desc)
{
    if(!desc) return;
    mdesc &gd = gmdescs.add();
    gd.mode = *modenr;
    gd.desc = newstring(desc);
}

void mutatorsdesc(int *mutnr, char *desc)
{
    if(!desc) return;
    mdesc &gd = mutdescs.add();
    gd.muts = 1 << *mutnr;
    gd.desc = newstring(desc);
}

void gspmutdesc(int *modenr, int *gspnr, char *desc)
{
    if(!desc) return;
    mdesc &gd = gspdescs.add();
    gd.mode = *modenr;
    gd.muts = 1 << (G_M_GSP + *gspnr);
    gd.desc = newstring(desc);
}
COMMAND(gamemodedesc, "is");
COMMAND(mutatorsdesc, "is");
COMMAND(gspmutdesc, "iis");

void resetmap(bool mrproper)
{
    resetsleep();
    resetzones();
    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(gamemode) && 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, "");

VARP(showmodedescriptions, 0, 1, 1);
extern bool canceldownloads;

void startmap(const char *name, bool reset)   // called just after a map load
{
    canceldownloads = false;
    copystring(clientmap, name);
    sendmapidenttoserver = true;
    // Added by Rick
    if(m_ai(gamemode)) BotManager.BeginMap(name);
    // End add by Rick
    clearbounceents();
    preparectf(!m_flags(gamemode));
    suicided = -1;
    lasthit = 0;
    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->assists = player1->flagscore = player1->deaths = player1->lifesequence = player1->points = player1->rank = player1->pointstreak = player1->deathstreak = player1->airstrikes = player1->radarmillis = player1->nametagmillis = player1->nukemillis = 0;
    loopv(players) if(players[i]) players[i]->frags = players[i]->assists = players[i]->flagscore = players[i]->deaths = players[i]->lifesequence = players[i]->points = player1->rank = players[i]->pointstreak = players[i]->deathstreak = players[i]->airstrikes = players[i]->radarmillis = players[i]->nametagmillis = players[i]->nukemillis = 0;
    if(editmode) toggleedit(true);
    intermission = false;
    showscores(false);
    needscoresreorder = true;
    minutesremaining = -1;
    lastgametimeupdate = 0;
    arenaintermission = 0;
    bool noflags = (m_capture(gamemode) || m_keep(gamemode)) && (!numflagspawn[0] || !numflagspawn[1]);
    if(*clientmap) conoutf(_("game mode is \"%s\"%s"), modestr(gamemode, mutators, modeacronyms > 0), noflags ? " - \f2but there are no flag bases on this map" : "");

    if(showmodedescriptions)
    {
        loopv(gmdescs) if(gmdescs[i].mode == gamemode)
            conoutf("\f1%s", gmdescs[i].desc);
        loopv(mutdescs)
            if(mutdescs[i].muts & mutators)
                conoutf("\f2%s", mutdescs[i].desc);
        loopv(gspdescs)
            if(gspdescs[i].mode == gamemode && gspdescs[i].muts & mutators)
                conoutf("\f3%s", gspdescs[i].desc);
    }

    // run once
    if(firstrun)
    {
        per_idents = false;
        execfile("config/firstrun.cfg");
        per_idents = 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, "ri", player1->clientnum);
        suicided = player1->lifesequence;
    }
}

COMMAND(suicide, "");

// console and audio feedback

void flagmsg(int flag, int message, int actor, int flagtime)
{
    playerent *act = getclient(actor);
    if(actor != getclientnum() && !act && message != FA_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, act);
    bool firstpersondrop = false;
    defformatstring(ownerstr)("the %s", teamnames[flag]);
    const char *teamstr = m_ktf2(gamemode, mutators) ? ownerstr : m_keep(gamemode) ? "the" : neutral ? ownerstr : own ? "your" : "the enemy";
    const char *flagteam = (m_keep(gamemode) && !neutral) ? (teammate ? "your teammate " : "your enemy ") : "";

    if(identexists("onFlag"))
    {
        defformatstring(onflagevent)("onFlag %d %d %d", message, actor, flag);
        execute(onflagevent);
    }

    switch(message)
    {
        case FA_PICKUP:
        case FA_STEAL:
            audiomgr.playsound(S_FLAGPICKUP, SP_HIGHEST);
            if(firstperson)
            {
                hudoutf("\f2you %s %s flag", message == FA_STEAL ? "stole" : "took", teamstr);
                if (!own || !m_capture(gamemode))
                    audiomgr.musicsuggest(M_FLAGGRAB, m_capture(gamemode) ? 90*1000 : 900*1000, true);
            }
            else hudoutf("\f2%s%s has %s flag", flagteam, colorname(act), teamstr);
            break;
        case FA_LOST:
        case FA_DROP:
        {
            const char *droplost = message == FA_LOST ? "lost" : "dropped";
            audiomgr.playsound(S_FLAGDROP, SP_HIGHEST);
            if(firstperson)
            {
                hudoutf("\f2you %s %s flag", droplost, teamstr);
                firstpersondrop = true;
            }
            else hudoutf("\f2%s %s %s %s flag", flagteam, colorname(act), droplost, teamstr);
            break;
        }
        case FA_RETURN:
            audiomgr.playsound(S_FLAGRETURN, SP_HIGHEST);
            if (firstperson) hudoutf("\f2you returned %s flag", teamstr);
            else hudoutf("\f2%s %s returned %s flag", flagteam, colorname(act), teamstr);
            break;
        case FA_SCORE:
            audiomgr.playsound(S_FLAGSCORE, SP_HIGHEST);
            if(firstperson)
            {
                hudoutf("\f2you scored");
                firstpersondrop = true;
            }
            else hudoutf("\f2%s scored for %s", colorname(act), neutral ? teamnames[act->team] : teammate ? "your team" : "the enemy team");
            break;
        case FA_KTFSCORE:
        {
            audiomgr.playsound(S_KTFSCORE, SP_HIGHEST);
            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 FA_SCOREFAIL: // sound?
            hudoutf("\f2%s failed to score (own team flag not taken)", firstperson ? "you" : colorname(act));
            break;
        case FA_RESET:
            audiomgr.playsound(S_FLAGRETURN, SP_HIGHEST);
            hudoutf("the server reset the flag");
            firstpersondrop = true;
            break;
    }
    if(firstpersondrop)
    {
        if ((flaginfos[0].state != CTFF_STOLEN || player1 != flaginfos[0].actor) &&
            (flaginfos[1].state != CTFF_STOLEN || player1 != flaginfos[1].actor))
            audiomgr.musicfadeout(M_FLAGGRAB);
    }
}

COMMANDN(dropflag, tryflagdrop, "");

const char *votestring(int type, const votedata &vote)
{
    if (type < 0 || type >= SA_NUM) return "<invalid vote type>";
    static string out = { 0 };
    copystring(out, "unknown vote");

    switch(type)
    {
        case SA_KICK:
        {
            playerent *p = getclient(vote.int1);
            if (p) formatstring(out)("kick %s: %s", colorname(p), vote.str1);
            else formatstring(out)("kick someone (%d) for %s", vote.int1, vote.str1);
            break;
        }
        case SA_BAN:
        {
            int cn = vote.int1, minutes = vote.int2;
            playerent *p = getclient(cn);
            if (p) formatstring(out)("ban %s for %d min: %s", colorname(p), minutes, vote.str1);
            else formatstring(out)("ban someone (%d) for %d min: %s", cn, minutes, vote.str1);
            break;
        }
        case SA_MASTERMODE:
            formatstring(out)("set mastermode to %s", mmfullname(vote.int1));
            break;
        case SA_AUTOTEAM:
            formatstring(out)("%s autoteam", vote.int1 ? "enable" : "disable");
            break;
        case SA_FORCETEAM:
        {
            playerent *p = getclient(vote.int1);
            formatstring(out)("force player %s to %s", p ? colorname(p) : "?", team_string(vote.int2));
            break;
        }
        case SA_GIVEADMIN:
        {
            playerent *p = getclient(vote.int1);
            const int priv = vote.int2;
            if (p) formatstring(out)("\fs\f0give \f%c%s \frto %s", privcolor(priv), privname(priv), colorname(p));
            else formatstring(out)("give someone (%d) \f%c%s", vote.int1, privcolor(priv), privname(priv));
            break;
        }
        case SA_MAP:
        {
            int n = vote.int1, muts = vote.int2;
            if (n >= G_MAX)
            {
                if (gamemode == n - G_MAX && mutators == muts && !strcmp(clientmap, vote.str1))
                    copystring(out, "repeat this map next");
                else
                    formatstring(out)("set next map to %s in mode %s", vote.str1, modestr(n - G_MAX, muts, modeacronyms > 0));
            }
            else
            {
                if (gamemode == n && mutators == muts && !strcmp(clientmap, vote.str1))
                    copystring(out, "reload this map");
                else
                    formatstring(out)("load map %s in mode %s", vote.str1, modestr(n, muts, modeacronyms > 0));
            }
            break;
        }
        case SA_RECORDDEMO:
            formatstring(out)("%s demo recording for the next match", vote.int1 ? "enable" : "disable");
            break;
        case SA_CLEARDEMOS:
            if (vote.int1 > 0)
                formatstring(out)("clear demo %d", vote.int1);
            else
                copystring(out, "clear all demos");
            break;
        case SA_SERVERDESC:
            formatstring(out)("set server description to '%s'", vote.str1);
            break;
        case SA_BOTBALANCE: // int1
            if (vote.int1 == 1)
                copystring(out, "bots balance teams only");
            else if (!vote.int1)
                copystring(out, "disable all bots");
            else if (vote.int1 == -1)
                copystring(out, "automatically balance bots");
            else if (vote.int1 < -1)
                formatstring(out)("balance to %d RED, %d BLUE", vote.int1 / -100, -vote.int1 % 100);
            else
                formatstring(out)("balance to %d players", vote.int1);
            break;
        case SA_SUBDUE:
        {
            playerent *p = getclient(vote.int1);
            formatstring(out)("subdue %s", p ? colorname(p) : "?");
            break;
        }
        case SA_REVOKE:
        {
            playerent *p = getclient(vote.int1);
            if (p) formatstring(out)("revoke \fs\f%c%s\fr from %s", privcolor(p->clientrole), privname(p->clientrole), colorname(p));
            else formatstring(out)("revoke privilege from someone (%d)", vote.int1);
            break;
        }
        // static
        case SA_REMBANS: copystring(out, "remove all temporary bans"); break;
        case SA_STOPDEMO: copystring(out, "stop demo recording"); break;
        case SA_SHUFFLETEAMS: copystring(out, "shuffle teams"); break;
    }
    return out;
}

votedisplayinfo *curvote = NULL;

void callvote(int type, const votedata &vote)
{
    if (type >= 0 && type < SA_NUM)
    {
        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
        putint(p, SV_CALLVOTE);
        putint(p, type);
        switch(type)
        {
            case SA_BAN:
            case SA_MAP:
                putint(p, vote.int2);
                // fallthrough
            case SA_KICK:
                putint(p, vote.int1);
                // fallthrough
            case SA_SERVERDESC:
                sendstring(vote.str1, p);
                break;
            case SA_FORCETEAM:
            case SA_GIVEADMIN:
                putint(p, vote.int2);
                // fallthrough
            case SA_MASTERMODE:
            case SA_AUTOTEAM:
            case SA_RECORDDEMO:
            case SA_CLEARDEMOS:
            case SA_BOTBALANCE:
            case SA_SUBDUE:
            case SA_REVOKE:
                putint(p, vote.int1);
                // fallthrough
            case SA_STOPDEMO:
                // compatibility
            default:
            case SA_REMBANS:
            case SA_SHUFFLETEAMS:
                break;
        }
        sendpackettoserv(1, p.finalize());
    }
    else conoutf(_("%c3invalid vote"), CC);
}

void callvote_parser(int *type, const char *arg1, const char *arg2, const char *arg3)
{
    if(type && inmainloop)
    {
        int t = *type;
        if (t < 0 || t >= SA_NUM)
            conoutf("\f3invalid vote: \f2%s %s %s %s", type, arg1, arg2, arg3);
        else
        {
            // storage of vote data
            static string str1;
            votedata vote(str1);
            switch (t)
            {
                case SA_MAP:
                    vote.int1 = nextmode;
                    vote.int2 = nextmuts;
                    // fallthrough
                case SA_SERVERDESC:
                    copystring(str1, arg1);
                    break;
                case SA_KICK:
                    vote.int1 = atoi(arg1);
                    copystring(str1, arg2);
                    break;
                case SA_BAN:
                    copystring(str1, arg3);
                    // fallthrough
                case SA_FORCETEAM:
                case SA_GIVEADMIN:
                    vote.int2 = atoi(arg2);
                    // fallthrough
                case SA_MASTERMODE:
                case SA_AUTOTEAM:
                case SA_RECORDDEMO:
                case SA_CLEARDEMOS:
                case SA_BOTBALANCE:
                case SA_SUBDUE:
                case SA_REVOKE:
                    vote.int1 = atoi(arg1);
                    // fallthrough
                case SA_STOPDEMO:
                    // compatibility
                default:
                case SA_REMBANS:
                case SA_SHUFFLETEAMS:
                    break;
            }
            callvote(t, vote);
        }
    }
}

void vote(int v)
{
    if (!curvote || curvote->result != VOTE_NEUTRAL || v < 0 || v >= VOTE_NUM) return;
    addmsg(SV_VOTE, "ri", v);
    player1->vote = v; // did you think that our bots could vote? ;)
}

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);
    player1->vote = VOTE_NEUTRAL;
    loopv(players) if (players[i]) players[i]->vote = VOTE_NEUTRAL;
    votepending = 1;
}

void callvoteerr(int e)
{
    if(e < 0 || e >= VOTEE_NUM) return;
    conoutf(_("%c3could not vote: %s"), CC, voteerrorstr(e));
}

void clearvote() { DELETEP(curvote); }

void setnext(char *map)
{
    if(!map) return;
    votedata vote(map);
    vote.int1 = nextmode + G_MAX;
    vote.int2 = nextmuts;
    callvote(SA_MAP, vote);
}
COMMAND(setnext, "s");

void gonext(int *arg1)
{
    addmsg(SV_CALLVOTE, "ri3s", SA_MAP, -1, *arg1, "+1");
}
COMMAND(gonext, "i");

COMMANDN(callvote, callvote_parser, "isss");
COMMANDF(vote, "i", (int *v) { vote(*v); });

void cleanplayervotes(playerent *p)
{
    if(curvote && curvote->owner==p) curvote->owner = NULL;
}

void whois(int *cn)
{
    addmsg(SV_WHOIS, "ri", *cn);
}
COMMAND(whois, "i");

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, "s");

int sessionid = 0;

void setadmin(int *claim, char *password)
{
    if (!*claim) addmsg(SV_SETPRIV, "r");
    else addmsg(SV_CLAIMPRIV, "rs", genpwdhash(player1->name, password, sessionid));
}

COMMAND(setadmin, "is");

struct mline { string name, cmd; };
static vector<mline> mlines;

gmenu *kickmenu = NULL, *banmenu = NULL, *forceteammenu = NULL, *giveadminmenu = NULL;

void refreshsopmenu(gmenu *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!
        else kbr[0] = '\0';
        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;

VARFP(thirdperson, -MAXTHIRDPERSON, 0, MAXTHIRDPERSON, addmsg(SV_THIRDPERSON, "ri", thirdperson));

VARP(spectatebots, 0, 0, 1);

// rotate through all spec-able players
playerent *updatefollowplayer(int shiftdirection)
{
    if(!shiftdirection)
    {
        if(focus && focus != player1 && (watchingdemo || !focus->isspectating())) return focus;
    }

    // collect spec-able players
    vector<playerent *> available;
    loopv(players)
    {
        if (!players[i]) continue;
        if (player1->team == TEAM_SPECT) continue;
        //if(!watchingdemo && m_team(gamemode, mutators) && team_base(players[i]->team) != team_base(player1->team)) continue;
        if (players[i]->ownernum >= 0 && !spectatebots) continue;
        if (players[i]->state == CS_DEAD && !m_duke(gamemode, mutators)) continue;
        available.add(players[i]);
    }
    if(!available.length()) return NULL;

    // rotate
    int oldidx = available.find(focus);
    if(oldidx<0) oldidx = 0;
    int idx = (oldidx+shiftdirection) % available.length();
    if(idx<0) idx += available.length();

    return (focus = available[idx]);
}

void spectate()
{
    if(m_demo(gamemode)) return;
    if(!team_isspect(player1->team)) addmsg(SV_SETTEAM, "ri", TEAM_SPECT);
    else tryrespawn();
}

void setfollowplayer(int cn)
{
    // silently ignores invalid player-cn value passed
    if(players.inrange(cn) && players[cn])
    {
        if(!(m_team(gamemode, mutators) && !watchingdemo && team_base(players[cn]->team) != team_base(player1->team)))
        {
            focus = players[cn];
            if(player1->spectatemode == SM_FLY) player1->spectatemode = SM_FOLLOWSAME;
        }
    }
}

// set new spect mode
void spectatemode(int mode)
{
    if((player1->state != CS_DEAD && !team_isspect(player1->team)) || (!m_team(gamemode, mutators) && !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_FOLLOWSAME:
        case SM_FOLLOWALT:
        {
            if(players.length() && updatefollowplayer()) break;
            else mode = SM_FLY;
            // fallthrough
        }
        case SM_FLY:
        {
            if(player1->spectatemode != SM_FLY)
            {
                playerent *f = updatefollowplayer();
                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
            }
            break;
        }
        case SM_OVERVIEW:
            //player1->followplayercn = FPCN_OVERVIEW;
            break;
        default: break;
    }
    player1->spectatemode = mode;
}

void togglespect() // cycle through all spectating modes
{
    int mode;
    if (player1->spectatemode == SM_NONE) mode = SM_FOLLOWSAME; // start with 1st person spect
    else mode = SM_FOLLOWSAME + ((player1->spectatemode - SM_FOLLOWSAME + 1) % (SM_OVERVIEW - SM_FOLLOWSAME)); // replace SM_OVERVIEW by SM_NUM to enable overview mode
    spectatemode(mode);
}

void changefollowplayer(int shift)
{
    updatefollowplayer(shift);
}

COMMAND(spectate, "");
COMMANDF(spectatemode, "i", (int *mode) { spectatemode(*mode); });
COMMAND(togglespect, "");
COMMANDF(changefollowplayer, "i", (int *dir) { changefollowplayer(*dir); });
COMMANDF(setfollowplayer, "i", (int *cn) { setfollowplayer(*cn); });

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, "ss");