/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
Copyright (C) 2006 Robert Beckebans <trebor_7@users.sourceforge.net>

This file is part of XreaL source code.

XreaL source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

XreaL source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with XreaL source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
//
#include <hat/engine/javascript/weapon/interface.hpp>
#include <hat/server/g_local.h>
#include <hat/gui/menudef.h>		// for the voice chats

/*
==================
DeathmatchScoreboardMessage

==================
*/
void DeathmatchScoreboardMessage(gentity_t * ent)
{
  char            entry[1024];
  char            string[1400];
  int             stringlength;
  int             i, j;
  gclient_t      *cl;
  int             numSorted, scoreFlags, accuracy, perfect;

  // send the latest information on all clients
  string[0] = 0;
  stringlength = 0;
  scoreFlags = 0;

  numSorted = level.numConnectedClients;

  for(i = 0; i < numSorted; i++)
  {
    int             ping;

    cl = &level.clients[level.sortedClients[i]];

    if(cl->pers.connected == CON_CONNECTING)
    {
      ping = -1;
    }
    else
    {
      ping = cl->ps.ping < 999 ? cl->ps.ping : 999;
    }

    if(cl->accuracy_shots)
    {
      accuracy = cl->accuracy_hits * 100 / cl->accuracy_shots;
    }
    else
    {
      accuracy = 0;
    }
    perfect = (cl->ps.persistant[PERS_RANK] == 0 && cl->ps.persistant[PERS_KILLED] == 0) ? 1 : 0;

    Com_sprintf(entry, sizeof(entry),
          " %i %i %i %i %i %i %i %i %i %i %i %i %i %i", level.sortedClients[i],
          cl->ps.persistant[PERS_SCORE], ping, (level.time - cl->pers.enterTime) / 60000,
          scoreFlags, g_entities[level.sortedClients[i]].s.powerups, accuracy,
          cl->ps.persistant[PERS_IMPRESSIVE_COUNT],
          cl->ps.persistant[PERS_EXCELLENT_COUNT],
          cl->ps.persistant[PERS_GAUNTLET_FRAG_COUNT],
          cl->ps.persistant[PERS_DEFEND_COUNT],
          cl->ps.persistant[PERS_ASSIST_COUNT], perfect, cl->ps.persistant[PERS_CAPTURES]);
    j = strlen(entry);
    if(stringlength + j > 1024)
      break;
    strcpy(string + stringlength, entry);
    stringlength += j;
  }

  trap_SendServerCommand(ent - g_entities, va("scores %i %i %i%s", i,
                        level.teamScores[TEAM_RED], level.teamScores[TEAM_BLUE], string));
}


/*
==================
Cmd_Score_f

Request current scoreboard information
==================
*/
void Cmd_Score_f(gentity_t * ent)
{
  DeathmatchScoreboardMessage(ent);
}



/*
==================
CheatsOk
==================
*/
qboolean CheatsOk(gentity_t * ent)
{
  if(!g_cheats.integer)
  {
    trap_SendServerCommand(ent - g_entities, va("print \"Cheats are not enabled on this server.\n\""));
    return qfalse;
  }
  if(ent->health <= 0)
  {
    trap_SendServerCommand(ent - g_entities, va("print \"You must be alive to use this command.\n\""));
    return qfalse;
  }
  return qtrue;
}


/*
==================
ConcatArgs
==================
*/
char           *ConcatArgs(int start)
{
  int             i, c, tlen;
  static char     line[MAX_STRING_CHARS];
  int             len;
  char            arg[MAX_STRING_CHARS];

  len = 0;
  c = trap_Argc();
  for(i = start; i < c; i++)
  {
    trap_Argv(i, arg, sizeof(arg));
    tlen = strlen(arg);
    if(len + tlen >= MAX_STRING_CHARS - 1)
    {
      break;
    }
    memcpy(line + len, arg, tlen);
    len += tlen;
    if(i != c - 1)
    {
      line[len] = ' ';
      len++;
    }
  }

  line[len] = 0;

  return line;
}

/*
==================
SanitizeString

Remove case and control characters
==================
*/
void SanitizeString(char *in, char *out)
{
  while(*in)
  {
    if(*in == 27)
    {
      in += 2;			// skip color code
      continue;
    }
    if(*in < 32)
    {
      in++;
      continue;
    }
    *out++ = tolower(*in++);
  }

  *out = 0;
}

/*
==================
ClientNumberFromString

Returns a player number for either a number or name string
Returns -1 if invalid
==================
*/
int ClientNumberFromString(gentity_t * to, char *s)
{
  gclient_t      *cl;
  int             idnum;
  char            s2[MAX_STRING_CHARS];
  char            n2[MAX_STRING_CHARS];

  // numeric values are just slot numbers
  if(s[0] >= '0' && s[0] <= '9')
  {
    idnum = atoi(s);
    if(idnum < 0 || idnum >= level.maxclients)
    {
      trap_SendServerCommand(to - g_entities, va("print \"Bad client slot: %i\n\"", idnum));
      return -1;
    }

    cl = &level.clients[idnum];
    if(cl->pers.connected != CON_CONNECTED)
    {
      trap_SendServerCommand(to - g_entities, va("print \"Client %i is not active\n\"", idnum));
      return -1;
    }
    return idnum;
  }

  // check for a name match
  SanitizeString(s, s2);
  for(idnum = 0, cl = level.clients; idnum < level.maxclients; idnum++, cl++)
  {
    if(cl->pers.connected != CON_CONNECTED)
    {
      continue;
    }
    SanitizeString(cl->pers.netname, n2);
    if(!strcmp(n2, s2))
    {
      return idnum;
    }
  }

  trap_SendServerCommand(to - g_entities, va("print \"User %s is not on the server\n\"", s));
  return -1;
}

/*
==================
Cmd_Give_f

Give items to a client
==================
*/
void Cmd_Give_f(gentity_t * ent)
{
  char           *name;
  gitem_t        *it;
  int             i;
  qboolean        give_all;
  gentity_t      *it_ent;
  trace_t         trace;

  /*
  if(!CheatsOk(ent))
  {
    return;
  }
  */

  name = ConcatArgs(1);

  if(Q_stricmp(name, "all") == 0)
    give_all = qtrue;
  else
    give_all = qfalse;

  if(give_all || Q_stricmp(name, "health") == 0)
  {
    ent->health = ent->client->ps.stats[STAT_MAX_HEALTH];
    if(!give_all)
      return;
  }

  if(give_all || Q_stricmp(name, "weapons") == 0)
  {
    ent->client->ps.stats[STAT_WEAPONS] = (1 << WP_NUM_WEAPONS) - 1 - (1 << WP_NONE);
    if(!give_all)
      return;
  }

  if(give_all || Q_stricmp(name, "ammo") == 0)
  {
    for(i = 0; i < MAX_WEAPONS; i++)
    {
      ent->client->ps.ammo[i] = 999;
    }
    if(!give_all)
      return;
  }

  if(give_all || Q_stricmp(name, "armor") == 0)
  {
    ent->client->ps.stats[STAT_ARMOR] = 200;

    if(!give_all)
      return;
  }

  if(Q_stricmp(name, "excellent") == 0)
  {
    ent->client->ps.persistant[PERS_EXCELLENT_COUNT]++;
    return;
  }
  if(Q_stricmp(name, "impressive") == 0)
  {
    ent->client->ps.persistant[PERS_IMPRESSIVE_COUNT]++;
    return;
  }
  if(Q_stricmp(name, "gauntletaward") == 0)
  {
    ent->client->ps.persistant[PERS_GAUNTLET_FRAG_COUNT]++;
    return;
  }
  if(Q_stricmp(name, "telefrag") == 0)
  {
    ent->client->ps.persistant[PERS_TELEFRAG_FRAG_COUNT]++;
    return;
  }
  if(Q_stricmp(name, "defend") == 0)
  {
    ent->client->ps.persistant[PERS_DEFEND_COUNT]++;
    return;
  }
  if(Q_stricmp(name, "assist") == 0)
  {
    ent->client->ps.persistant[PERS_ASSIST_COUNT]++;
    return;
  }

  // requesting a weapon
  if (!give_all) {
    const hat::Weapon_attrs* attrs;
    if (trap_LoadAndGetWeaponAttrs(ent->client->ps.clientNum, name, (const void**)(&attrs))) {
      int weapon_id = attrs->unique_id;
      ent->client->ps.stats[STAT_WEAPONS] |= 1 << weapon_id;
      ent->client->ps.weapon_fire_mode[weapon_id] = attrs->fire_modes[0];

      for (int i = 0; i < MAX_MAGS; i++)
        ent->client->ps.primary_ammo[weapon_id][i] = 25 + i;

      // TODO(justinvh): This needs to be dynamically created somehow
      ent->client->ps.ammo[weapon_id] = 50;
      return;
    }
  }

  // spawn a specific item right on the player
  if(!give_all)
  {
    it = BG_FindItem(name);
    if(!it)
    {
      return;
    }

    it_ent = G_Spawn();
    VectorCopy(ent->r.currentOrigin, it_ent->s.origin);
    it_ent->classname = it->classname;
    G_SpawnItem(it_ent, it);
    FinishSpawningItem(it_ent);
    memset(&trace, 0, sizeof(trace));
    Touch_Item(it_ent, ent, &trace);
    if(it_ent->inuse)
    {
      G_FreeEntity(it_ent);
    }
  }
}


/*
==================
Cmd_God_f

Sets client to godmode

argv(0) god
==================
*/
void Cmd_God_f(gentity_t * ent)
{
  char           *msg;

  if(!CheatsOk(ent))
  {
    return;
  }

  ent->flags ^= FL_GODMODE;
  if(!(ent->flags & FL_GODMODE))
    msg = "godmode OFF\n";
  else
    msg = "godmode ON\n";

  trap_SendServerCommand(ent - g_entities, va("print \"%s\"", msg));
}


/*
==================
Cmd_Notarget_f

Sets client to notarget

argv(0) notarget
==================
*/
void Cmd_Notarget_f(gentity_t * ent)
{
  char           *msg;

  if(!CheatsOk(ent))
  {
    return;
  }

  ent->flags ^= FL_NOTARGET;
  if(!(ent->flags & FL_NOTARGET))
    msg = "notarget OFF\n";
  else
    msg = "notarget ON\n";

  trap_SendServerCommand(ent - g_entities, va("print \"%s\"", msg));
}


/*
==================
Cmd_Noclip_f

argv(0) noclip
==================
*/
void Cmd_Noclip_f(gentity_t * ent)
{
  char           *msg;

  if(!CheatsOk(ent))
  {
    return;
  }

  if(ent->client->noclip)
  {
    msg = "noclip OFF\n";
  }
  else
  {
    msg = "noclip ON\n";
  }
  ent->client->noclip = (qboolean)!ent->client->noclip;

  trap_SendServerCommand(ent - g_entities, va("print \"%s\"", msg));
}


/*
==================
Cmd_LevelShot_f

This is just to help generate the level pictures
for the menus.  It goes to the intermission immediately
and sends over a command to the client to resize the view,
hide the scoreboard, and take a special screenshot
==================
*/
void Cmd_LevelShot_f(gentity_t * ent)
{
  if(!CheatsOk(ent))
  {
    return;
  }

  // doesn't work in single player
  if(g_gametype.integer != 0)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Must be in g_gametype 0 for levelshot\n\"");
    return;
  }

  BeginIntermission();
  trap_SendServerCommand(ent - g_entities, "clientLevelShot");
}


/*
==================
Cmd_LevelShot_f

This is just to help generate the level pictures
for the menus.  It goes to the intermission immediately
and sends over a command to the client to resize the view,
hide the scoreboard, and take a special screenshot
==================
*/
void Cmd_TeamTask_f(gentity_t * ent)
{
  char            userinfo[MAX_INFO_STRING];
  char            arg[MAX_TOKEN_CHARS];
  int             task;
  int             client = ent->client - level.clients;

  if(trap_Argc() != 2)
  {
    return;
  }
  trap_Argv(1, arg, sizeof(arg));
  task = atoi(arg);

  trap_GetUserinfo(client, userinfo, sizeof(userinfo));
  Info_SetValueForKey(userinfo, "teamtask", va("%d", task));
  trap_SetUserinfo(client, userinfo);
  ClientUserinfoChanged(client);
}



/*
=================
Cmd_Kill_f
=================
*/
void Cmd_Kill_f(gentity_t * ent)
{
  if(ent->client->sess.sessionTeam == TEAM_SPECTATOR)
  {
    return;
  }
  if(ent->health <= 0)
  {
    return;
  }
  ent->flags &= ~FL_GODMODE;
  ent->client->ps.stats[STAT_HEALTH] = ent->health = -999;
  player_die(ent, ent, ent, 100000, MOD_SUICIDE);
}

/*
=================
BroadCastTeamChange

Let everyone know about a team change
=================
*/
void BroadcastTeamChange(gclient_t * client, int oldTeam)
{
  if(client->sess.sessionTeam == TEAM_RED)
  {
    trap_SendServerCommand(-1, va("cp \"%s" S_COLOR_WHITE " joined the red team.\n\"", client->pers.netname));
  }
  else if(client->sess.sessionTeam == TEAM_BLUE)
  {
    trap_SendServerCommand(-1, va("cp \"%s" S_COLOR_WHITE " joined the blue team.\n\"", client->pers.netname));
  }
  else if(client->sess.sessionTeam == TEAM_SPECTATOR && oldTeam != TEAM_SPECTATOR)
  {
    trap_SendServerCommand(-1, va("cp \"%s" S_COLOR_WHITE " joined the spectators.\n\"", client->pers.netname));
  }
  else if(client->sess.sessionTeam == TEAM_FREE)
  {
    trap_SendServerCommand(-1, va("cp \"%s" S_COLOR_WHITE " joined the battle.\n\"", client->pers.netname));
  }
}

/*
=================
SetTeam
=================
*/
void SetTeam(gentity_t * ent, char *s)
{
  int             team, oldTeam;
  gclient_t      *client;
  int             clientNum;
  spectatorState_t specState;
  int             specClient;
  int             teamLeader;

  //
  // see what change is requested
  //
  client = ent->client;

  clientNum = client - level.clients;
  specClient = 0;
  specState = SPECTATOR_NOT;
  if(!Q_stricmp(s, "scoreboard") || !Q_stricmp(s, "score"))
  {
    team = TEAM_SPECTATOR;
    specState = SPECTATOR_SCOREBOARD;
  }
  else if(!Q_stricmp(s, "follow1"))
  {
    team = TEAM_SPECTATOR;
    specState = SPECTATOR_FOLLOW;
    specClient = -1;
  }
  else if(!Q_stricmp(s, "follow2"))
  {
    team = TEAM_SPECTATOR;
    specState = SPECTATOR_FOLLOW;
    specClient = -2;
  }
  else if(!Q_stricmp(s, "spectator") || !Q_stricmp(s, "s"))
  {
    team = TEAM_SPECTATOR;
    specState = SPECTATOR_FREE;
  }
  else if(g_gametype.integer >= GT_TEAM)
  {
    // if running a team game, assign player to one of the teams
    specState = SPECTATOR_NOT;
    if(!Q_stricmp(s, "red") || !Q_stricmp(s, "r"))
    {
      team = TEAM_RED;
    }
    else if(!Q_stricmp(s, "blue") || !Q_stricmp(s, "b"))
    {
      team = TEAM_BLUE;
    }
    else
    {
      // pick the team with the least number of players
      team = PickTeam(clientNum);
    }

    if(g_teamForceBalance.integer)
    {
      int             counts[TEAM_NUM_TEAMS];

      counts[TEAM_BLUE] = TeamCount(clientNum, TEAM_BLUE);
      counts[TEAM_RED] = TeamCount(clientNum, TEAM_RED);

      // We allow a spread of two
      if(team == TEAM_RED && counts[TEAM_RED] - counts[TEAM_BLUE] > 1)
      {
        // ignore the request
        trap_SendServerCommand(clientNum, "cp \"Red team has too many players.\n\"");
        return;
      }
      if(team == TEAM_BLUE && counts[TEAM_BLUE] - counts[TEAM_RED] > 1)
      {
        // ignore the request
        trap_SendServerCommand(clientNum, "cp \"Blue team has too many players.\n\"");
        return;
      }

      // It's ok, the team we are switching to has less or same number of players
    }

  }
  else
  {
    // force them to spectators if there aren't any spots free
    team = TEAM_FREE;
  }

  // override decision if limiting the players
  if((g_gametype.integer == GT_TOURNAMENT) && level.numNonSpectatorClients >= 2)
  {
    team = TEAM_SPECTATOR;
  }
  else if(g_maxGameClients.integer > 0 && level.numNonSpectatorClients >= g_maxGameClients.integer)
  {
    team = TEAM_SPECTATOR;
  }

  //
  // decide if we will allow the change
  //
  oldTeam = client->sess.sessionTeam;
  if(team == oldTeam && team != TEAM_SPECTATOR)
  {
    return;
  }

  //
  // execute the team change
  //

  // if the player was dead leave the body
/*	if(client->ps.stats[STAT_HEALTH] <= 0)
  {
    CopyToBodyQue(ent);
  }
*/
  // he starts at 'base'
  client->pers.teamState.state = TEAM_BEGIN;
  if(oldTeam != TEAM_SPECTATOR)
  {
    // Kill him (makes sure he loses flags, etc)
    ent->flags &= ~FL_GODMODE;
    ent->client->ps.stats[STAT_HEALTH] = ent->health = 0;
    player_die(ent, ent, ent, 100000, MOD_SUICIDE);

  }
  // they go to the end of the line for tournements
  if(team == TEAM_SPECTATOR)
  {
    client->sess.spectatorTime = level.time;
  }

  client->sess.sessionTeam = (team_t)team;
  client->sess.spectatorState = specState;
  client->sess.spectatorClient = specClient;

  client->sess.teamLeader = qfalse;
  if(team == TEAM_RED || team == TEAM_BLUE)
  {
    teamLeader = TeamLeader(team);
    // if there is no team leader or the team leader is a bot and this client is not a bot
    if(teamLeader == -1 || (!(g_entities[clientNum].r.svFlags & SVF_BOT) && (g_entities[teamLeader].r.svFlags & SVF_BOT)))
    {
      SetLeader(team, clientNum);
    }
  }
  // make sure there is a team leader on the team the player came from
  if(oldTeam == TEAM_RED || oldTeam == TEAM_BLUE)
  {
    CheckTeamLeader(oldTeam);
  }

  BroadcastTeamChange(client, oldTeam);

  // get and distribute relevent paramters
  ClientUserinfoChanged(clientNum);

  ClientBegin(clientNum);
}

/*
=================
StopFollowing

If the client being followed leaves the game, or you just want to drop
to free floating spectator mode
=================
*/
void StopFollowing(gentity_t * ent)
{
  ent->client->ps.persistant[PERS_TEAM] = TEAM_SPECTATOR;
  ent->client->sess.sessionTeam = TEAM_SPECTATOR;
  ent->client->sess.spectatorState = SPECTATOR_FREE;
  ent->client->ps.pm_flags &= ~PMF_FOLLOW;
  ent->r.svFlags &= ~SVF_BOT;
  ent->client->ps.clientNum = ent - g_entities;
}

/*
=================
Cmd_Team_f
=================
*/
void Cmd_Team_f(gentity_t * ent)
{
  int             oldTeam;
  char            s[MAX_TOKEN_CHARS];

  //G_Printf("Cmd_Team_f()\n");

  if(trap_Argc() != 2)
  {
    oldTeam = ent->client->sess.sessionTeam;
    switch (oldTeam)
    {
      case TEAM_BLUE:
        trap_SendServerCommand(ent - g_entities, "print \"Blue team\n\"");
        break;
      case TEAM_RED:
        trap_SendServerCommand(ent - g_entities, "print \"Red team\n\"");
        break;
      case TEAM_FREE:
        trap_SendServerCommand(ent - g_entities, "print \"Free team\n\"");
        break;
      case TEAM_SPECTATOR:
        trap_SendServerCommand(ent - g_entities, "print \"Spectator team\n\"");
        break;
    }
    return;
  }

  if(ent->client->switchTeamTime > level.time)
  {
    trap_SendServerCommand(ent - g_entities, "print \"May not switch teams more than once per 5 seconds.\n\"");
    return;
  }

  // if they are playing a tournement game, count as a loss
  if((g_gametype.integer == GT_TOURNAMENT) && ent->client->sess.sessionTeam == TEAM_FREE)
  {
    ent->client->sess.losses++;
  }

  trap_Argv(1, s, sizeof(s));

  SetTeam(ent, s);

  ent->client->switchTeamTime = level.time + 5000;
}


/*
=================
Cmd_Follow_f
=================
*/
void Cmd_Follow_f(gentity_t * ent)
{
  int             i;
  char            arg[MAX_TOKEN_CHARS];

  if(trap_Argc() != 2)
  {
    if(ent->client->sess.spectatorState == SPECTATOR_FOLLOW)
    {
      StopFollowing(ent);
    }
    return;
  }

  trap_Argv(1, arg, sizeof(arg));
  i = ClientNumberFromString(ent, arg);
  if(i == -1)
  {
    return;
  }

  // can't follow self
  if(&level.clients[i] == ent->client)
  {
    return;
  }

  // can't follow another spectator
  if(level.clients[i].sess.sessionTeam == TEAM_SPECTATOR)
  {
    return;
  }

  // if they are playing a tournement game, count as a loss
  if((g_gametype.integer == GT_TOURNAMENT) && ent->client->sess.sessionTeam == TEAM_FREE)
  {
    ent->client->sess.losses++;
  }

  // first set them to spectator
  if(ent->client->sess.sessionTeam != TEAM_SPECTATOR)
  {
    SetTeam(ent, "spectator");
  }

  ent->client->sess.spectatorState = SPECTATOR_FOLLOW;
  ent->client->sess.spectatorClient = i;
}

/*
=================
Cmd_FollowCycle_f
=================
*/
void Cmd_FollowCycle_f(gentity_t * ent, int dir)
{
  int             clientnum;
  int             original;

  // if they are playing a tournement game, count as a loss
  if((g_gametype.integer == GT_TOURNAMENT) && ent->client->sess.sessionTeam == TEAM_FREE)
  {
    ent->client->sess.losses++;
  }
  // first set them to spectator
  if(ent->client->sess.spectatorState == SPECTATOR_NOT)
  {
    SetTeam(ent, "spectator");
  }

  if(dir != 1 && dir != -1)
  {
    G_Error("Cmd_FollowCycle_f: bad dir %i", dir);
  }

  clientnum = ent->client->sess.spectatorClient;
  original = clientnum;
  do
  {
    clientnum += dir;
    if(clientnum >= level.maxclients)
    {
      clientnum = 0;
    }
    if(clientnum < 0)
    {
      clientnum = level.maxclients - 1;
    }

    // can only follow connected clients
    if(level.clients[clientnum].pers.connected != CON_CONNECTED)
    {
      continue;
    }

    // can't follow another spectator
    if(level.clients[clientnum].sess.sessionTeam == TEAM_SPECTATOR)
    {
      continue;
    }

    // this is good, we can use it
    ent->client->sess.spectatorClient = clientnum;
    ent->client->sess.spectatorState = SPECTATOR_FOLLOW;
    return;
  } while(clientnum != original);

  // leave it where it was
}


/*
==================
G_Say
==================
*/

static void G_SayTo(gentity_t * ent, gentity_t * other, int mode, int color, const char *name, const char *message)
{
  if(!other)
  {
    return;
  }
  if(!other->inuse)
  {
    return;
  }
  if(!other->client)
  {
    return;
  }
  if(other->client->pers.connected != CON_CONNECTED)
  {
    return;
  }
  if(mode == SAY_TEAM && !OnSameTeam(ent, other))
  {
    return;
  }
  // no chatting to players in tournements
  if((g_gametype.integer == GT_TOURNAMENT)
     && other->client->sess.sessionTeam == TEAM_FREE && ent->client->sess.sessionTeam != TEAM_FREE)
  {
    return;
  }

  trap_SendServerCommand(other - g_entities, va("%s \"%s%c%c%s\"",
                          mode == SAY_TEAM ? "tchat" : "chat", name, Q_COLOR_ESCAPE, color, message));
}

#define EC		"\x19"

void G_Say(gentity_t * ent, gentity_t * target, int mode, const char *chatText)
{
  int             j;
  gentity_t      *other;
  int             color;
  char            name[64];

  // don't let text be too long for malicious reasons
  char            text[MAX_SAY_TEXT];
  char            location[64];

  if(g_gametype.integer < GT_TEAM && mode == SAY_TEAM)
  {
    mode = SAY_ALL;
  }

  switch (mode)
  {
    default:
    case SAY_ALL:
      G_LogPrintf("say: %s: %s\n", ent->client->pers.netname, chatText);
      Com_sprintf(name, sizeof(name), "%s%c%c" EC ": ", ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE);
      color = COLOR_GREEN;
      break;
    case SAY_TEAM:
      G_LogPrintf("sayteam: %s: %s\n", ent->client->pers.netname, chatText);
      if(Team_GetLocationMsg(ent, location, sizeof(location)))
        Com_sprintf(name, sizeof(name), EC "(%s%c%c" EC ") (%s)" EC ": ",
              ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE, location);
      else
        Com_sprintf(name, sizeof(name), EC "(%s%c%c" EC ")" EC ": ",
              ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE);
      color = COLOR_CYAN;
      break;
    case SAY_TELL:
      if(target && g_gametype.integer >= GT_TEAM &&
         target->client->sess.sessionTeam == ent->client->sess.sessionTeam &&
         Team_GetLocationMsg(ent, location, sizeof(location)))
        Com_sprintf(name, sizeof(name), EC "[%s%c%c" EC "] (%s)" EC ": ", ent->client->pers.netname, Q_COLOR_ESCAPE,
              COLOR_WHITE, location);
      else
        Com_sprintf(name, sizeof(name), EC "[%s%c%c" EC "]" EC ": ", ent->client->pers.netname, Q_COLOR_ESCAPE,
              COLOR_WHITE);
      color = COLOR_MAGENTA;
      break;
  }

  Q_strncpyz(text, chatText, sizeof(text));

  if(target)
  {
    G_SayTo(ent, target, mode, color, name, text);
    return;
  }

  // echo the text to the console
  if(g_dedicated.integer)
  {
    G_Printf("%s%s\n", name, text);
  }

  // send it to all the apropriate clients
  for(j = 0; j < level.maxclients; j++)
  {
    other = &g_entities[j];
    G_SayTo(ent, other, mode, color, name, text);
  }
}


/*
==================
Cmd_Say_f
==================
*/
static void Cmd_Say_f(gentity_t * ent, int mode, qboolean arg0)
{
  char           *p;

  if(trap_Argc() < 2 && !arg0)
  {
    return;
  }

  if(arg0)
  {
    p = ConcatArgs(0);
  }
  else
  {
    p = ConcatArgs(1);
  }

  G_Say(ent, NULL, mode, p);
}

/*
==================
Cmd_Tell_f
==================
*/
static void Cmd_Tell_f(gentity_t * ent)
{
  int             targetNum;
  gentity_t      *target;
  char           *p;
  char            arg[MAX_TOKEN_CHARS];

  if(trap_Argc() < 2)
  {
    return;
  }

  trap_Argv(1, arg, sizeof(arg));
  targetNum = atoi(arg);
  if(targetNum < 0 || targetNum >= level.maxclients)
  {
    return;
  }

  target = &g_entities[targetNum];
  if(!target || !target->inuse || !target->client)
  {
    return;
  }

  p = ConcatArgs(2);

  G_LogPrintf("tell: %s to %s: %s\n", ent->client->pers.netname, target->client->pers.netname, p);
  G_Say(ent, target, SAY_TELL, p);
  // don't tell to the player self if it was already directed to this player
  // also don't send the chat back to a bot
  if(ent != target && !(ent->r.svFlags & SVF_BOT))
  {
    G_Say(ent, ent, SAY_TELL, p);
  }
}


static void G_VoiceTo(gentity_t * ent, gentity_t * other, int mode, const char *id, qboolean voiceonly)
{
  int             color;
  char           *cmd;

  if(!other)
  {
    return;
  }
  if(!other->inuse)
  {
    return;
  }
  if(!other->client)
  {
    return;
  }
  if(mode == SAY_TEAM && !OnSameTeam(ent, other))
  {
    return;
  }
  // no chatting to players in tournements
  if((g_gametype.integer == GT_TOURNAMENT))
  {
    return;
  }

  if(mode == SAY_TEAM)
  {
    color = COLOR_CYAN;
    cmd = "vtchat";
  }
  else if(mode == SAY_TELL)
  {
    color = COLOR_MAGENTA;
    cmd = "vtell";
  }
  else
  {
    color = COLOR_GREEN;
    cmd = "vchat";
  }

  trap_SendServerCommand(other - g_entities, va("%s %d %d %d %s", cmd, voiceonly, ent->s.number, color, id));
}

void G_Voice(gentity_t * ent, gentity_t * target, int mode, const char *id, qboolean voiceonly)
{
  int             j;
  gentity_t      *other;

  if(g_gametype.integer < GT_TEAM && mode == SAY_TEAM)
  {
    mode = SAY_ALL;
  }

  if(target)
  {
    G_VoiceTo(ent, target, mode, id, voiceonly);
    return;
  }

  // echo the text to the console
  if(g_dedicated.integer)
  {
    G_Printf("voice: %s %s\n", ent->client->pers.netname, id);
  }

  // send it to all the apropriate clients
  for(j = 0; j < level.maxclients; j++)
  {
    other = &g_entities[j];
    G_VoiceTo(ent, other, mode, id, voiceonly);
  }
}

/*
==================
Cmd_Voice_f
==================
*/
static void Cmd_Voice_f(gentity_t * ent, int mode, qboolean arg0, qboolean voiceonly)
{
  char           *p;

  if(trap_Argc() < 2 && !arg0)
  {
    return;
  }

  if(arg0)
  {
    p = ConcatArgs(0);
  }
  else
  {
    p = ConcatArgs(1);
  }

  G_Voice(ent, NULL, mode, p, voiceonly);
}

/*
==================
Cmd_VoiceTell_f
==================
*/
static void Cmd_VoiceTell_f(gentity_t * ent, qboolean voiceonly)
{
  int             targetNum;
  gentity_t      *target;
  char           *id;
  char            arg[MAX_TOKEN_CHARS];

  if(trap_Argc() < 2)
  {
    return;
  }

  trap_Argv(1, arg, sizeof(arg));
  targetNum = atoi(arg);
  if(targetNum < 0 || targetNum >= level.maxclients)
  {
    return;
  }

  target = &g_entities[targetNum];
  if(!target || !target->inuse || !target->client)
  {
    return;
  }

  id = ConcatArgs(2);

  G_LogPrintf("vtell: %s to %s: %s\n", ent->client->pers.netname, target->client->pers.netname, id);
  G_Voice(ent, target, SAY_TELL, id, voiceonly);
  // don't tell to the player self if it was already directed to this player
  // also don't send the chat back to a bot
  if(ent != target && !(ent->r.svFlags & SVF_BOT))
  {
    G_Voice(ent, ent, SAY_TELL, id, voiceonly);
  }
}


/*
==================
Cmd_VoiceTaunt_f
==================
*/
static void Cmd_VoiceTaunt_f(gentity_t * ent)
{
  gentity_t      *who;
  int             i;

  if(!ent->client)
  {
    return;
  }

  // insult someone who just killed you
  if(ent->enemy && ent->enemy->client && ent->enemy->client->lastkilled_client == ent->s.number)
  {
    // i am a dead corpse
    if(!(ent->enemy->r.svFlags & SVF_BOT))
    {
      G_Voice(ent, ent->enemy, SAY_TELL, VOICECHAT_DEATHINSULT, qfalse);
    }
    if(!(ent->r.svFlags & SVF_BOT))
    {
      G_Voice(ent, ent, SAY_TELL, VOICECHAT_DEATHINSULT, qfalse);
    }
    ent->enemy = NULL;
    return;
  }
  // insult someone you just killed
  if(ent->client->lastkilled_client >= 0 && ent->client->lastkilled_client != ent->s.number)
  {
    who = g_entities + ent->client->lastkilled_client;
    if(who->client)
    {
      // who is the person I just killed
      if(who->client->lasthurt_mod == MOD_GAUNTLET)
      {
        if(!(who->r.svFlags & SVF_BOT))
        {
          G_Voice(ent, who, SAY_TELL, VOICECHAT_KILLGAUNTLET, qfalse);	// and I killed them with a gauntlet
        }
        if(!(ent->r.svFlags & SVF_BOT))
        {
          G_Voice(ent, ent, SAY_TELL, VOICECHAT_KILLGAUNTLET, qfalse);
        }
      }
      else
      {
        if(!(who->r.svFlags & SVF_BOT))
        {
          G_Voice(ent, who, SAY_TELL, VOICECHAT_KILLINSULT, qfalse);	// and I killed them with something else
        }
        if(!(ent->r.svFlags & SVF_BOT))
        {
          G_Voice(ent, ent, SAY_TELL, VOICECHAT_KILLINSULT, qfalse);
        }
      }
      ent->client->lastkilled_client = -1;
      return;
    }
  }

  if(g_gametype.integer >= GT_TEAM)
  {
    // praise a team mate who just got a reward
    for(i = 0; i < MAX_CLIENTS; i++)
    {
      who = g_entities + i;
      if(who->client && who != ent && who->client->sess.sessionTeam == ent->client->sess.sessionTeam)
      {
        if(who->client->rewardTime > level.time)
        {
          if(!(who->r.svFlags & SVF_BOT))
          {
            G_Voice(ent, who, SAY_TELL, VOICECHAT_PRAISE, qfalse);
          }
          if(!(ent->r.svFlags & SVF_BOT))
          {
            G_Voice(ent, ent, SAY_TELL, VOICECHAT_PRAISE, qfalse);
          }
          return;
        }
      }
    }
  }

  // just say something
  G_Voice(ent, NULL, SAY_ALL, VOICECHAT_TAUNT, qfalse);
}



static char    *gc_orders[] = {
  "hold your position",
  "hold this position",
  "come here",
  "cover me",
  "guard location",
  "search and destroy",
  "report"
};

void Cmd_GameCommand_f(gentity_t * ent)
{
  int             player;
  int             order;
  char            str[MAX_TOKEN_CHARS];

  trap_Argv(1, str, sizeof(str));
  player = atoi(str);
  trap_Argv(2, str, sizeof(str));
  order = atoi(str);

  if(player < 0 || player >= MAX_CLIENTS)
  {
    return;
  }
  if(order < 0 || order > sizeof(gc_orders) / sizeof(char *))
  {
    return;
  }
  G_Say(ent, &g_entities[player], SAY_TELL, gc_orders[order]);
  G_Say(ent, ent, SAY_TELL, gc_orders[order]);
}

/*
==================
Cmd_Where_f
==================
*/
void Cmd_Where_f(gentity_t * ent)
{
  trap_SendServerCommand(ent - g_entities, va("print \"%s\n\"", vtos(ent->s.origin)));
}

static const char *gameNames[] = {
  "Free For All",
  "Tournament",
  "Single Player",
  "Team Deathmatch",
  "Capture the Flag",
  "One Flag CTF",
  "Overload",
  "Harvester"
};

/*
==================
Cmd_CallVote_f
==================
*/
void Cmd_CallVote_f(gentity_t * ent)
{
  char           *c;
  int             i;
  char            arg1[MAX_STRING_TOKENS];
  char            arg2[MAX_STRING_TOKENS];

  if(!g_allowVote.integer)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Voting not allowed here.\n\"");
    return;
  }

  if(level.voteTime)
  {
    trap_SendServerCommand(ent - g_entities, "print \"A vote is already in progress.\n\"");
    return;
  }
  if(ent->client->pers.voteCount >= MAX_VOTE_COUNT)
  {
    trap_SendServerCommand(ent - g_entities, "print \"You have called the maximum number of votes.\n\"");
    return;
  }
  if(ent->client->sess.sessionTeam == TEAM_SPECTATOR)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Not allowed to call a vote as spectator.\n\"");
    return;
  }

  // make sure it is a valid command to vote on
  trap_Argv(1, arg1, sizeof(arg1));
  trap_Argv(2, arg2, sizeof(arg2));

  // check for command separators in arg2
  for(c = arg2; *c; ++c)
  {
    switch (*c)
    {
      case '\n':
      case '\r':
      case ';':
        trap_SendServerCommand(ent - g_entities, "print \"Invalid vote string.\n\"");
        return;
      default:
        break;
    }
  }

  if(!Q_stricmp(arg1, "map_restart"))
  {
  }
  else if(!Q_stricmp(arg1, "nextmap"))
  {
  }
  else if(!Q_stricmp(arg1, "map"))
  {
  }
  else if(!Q_stricmp(arg1, "g_gametype"))
  {
  }
  else if(!Q_stricmp(arg1, "kick"))
  {
  }
  else if(!Q_stricmp(arg1, "clientkick"))
  {
  }
  else if(!Q_stricmp(arg1, "g_doWarmup"))
  {
  }
  else if(!Q_stricmp(arg1, "timelimit"))
  {
  }
  else if(!Q_stricmp(arg1, "fraglimit"))
  {
  }
  else
  {
    trap_SendServerCommand(ent - g_entities, "print \"Invalid vote string.\n\"");
    trap_SendServerCommand(ent - g_entities,
                 "print \"Vote commands are: map_restart, nextmap, map <mapname>, g_gametype <n>, kick <player>, clientkick <clientnum>, g_doWarmup, timelimit <time>, fraglimit <frags>.\n\"");
    return;
  }

  // if there is still a vote to be executed
  if(level.voteExecuteTime)
  {
    level.voteExecuteTime = 0;
    trap_SendConsoleCommand(EXEC_APPEND, va("%s\n", level.voteString));
  }

  // special case for g_gametype, check for bad values
  if(!Q_stricmp(arg1, "g_gametype"))
  {
    i = atoi(arg2);
    if(i == GT_SINGLE_PLAYER || i < GT_FFA || i >= GT_MAX_GAME_TYPE)
    {
      trap_SendServerCommand(ent - g_entities, "print \"Invalid gametype.\n\"");
      return;
    }

    Com_sprintf(level.voteString, sizeof(level.voteString), "%s %d", arg1, i);
    Com_sprintf(level.voteDisplayString, sizeof(level.voteDisplayString), "%s %s", arg1, gameNames[i]);
  }
  else if(!Q_stricmp(arg1, "map"))
  {
    // special case for map changes, we want to reset the nextmap setting
    // this allows a player to change maps, but not upset the map rotation
    char            s[MAX_STRING_CHARS];

    trap_Cvar_VariableStringBuffer("nextmap", s, sizeof(s));
    if(*s)
    {
      Com_sprintf(level.voteString, sizeof(level.voteString), "%s %s; set nextmap \"%s\"", arg1, arg2, s);
    }
    else
    {
      Com_sprintf(level.voteString, sizeof(level.voteString), "%s %s", arg1, arg2);
    }
    Com_sprintf(level.voteDisplayString, sizeof(level.voteDisplayString), "%s", level.voteString);
  }
  else if(!Q_stricmp(arg1, "nextmap"))
  {
    char            s[MAX_STRING_CHARS];

    trap_Cvar_VariableStringBuffer("nextmap", s, sizeof(s));
    if(!*s)
    {
      trap_SendServerCommand(ent - g_entities, "print \"nextmap not set.\n\"");
      return;
    }
    Com_sprintf(level.voteString, sizeof(level.voteString), "vstr nextmap");
    Com_sprintf(level.voteDisplayString, sizeof(level.voteDisplayString), "%s", level.voteString);
  }
  else
  {
    Com_sprintf(level.voteString, sizeof(level.voteString), "%s \"%s\"", arg1, arg2);
    Com_sprintf(level.voteDisplayString, sizeof(level.voteDisplayString), "%s", level.voteString);
  }

  trap_SendServerCommand(-1, va("print \"%s called a vote.\n\"", ent->client->pers.netname));

  // start the voting, the caller autoamtically votes yes
  level.voteTime = level.time;
  level.voteYes = 1;
  level.voteNo = 0;

  for(i = 0; i < level.maxclients; i++)
  {
    level.clients[i].ps.eFlags &= ~EF_VOTED;
  }
  ent->client->ps.eFlags |= EF_VOTED;

  trap_SetConfigstring(CS_VOTE_TIME, va("%i", level.voteTime));
  trap_SetConfigstring(CS_VOTE_STRING, level.voteDisplayString);
  trap_SetConfigstring(CS_VOTE_YES, va("%i", level.voteYes));
  trap_SetConfigstring(CS_VOTE_NO, va("%i", level.voteNo));
}

/*
==================
Cmd_Vote_f
==================
*/
void Cmd_Vote_f(gentity_t * ent)
{
  char            msg[64];

  if(!level.voteTime)
  {
    trap_SendServerCommand(ent - g_entities, "print \"No vote in progress.\n\"");
    return;
  }
  if(ent->client->ps.eFlags & EF_VOTED)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Vote already cast.\n\"");
    return;
  }
  if(ent->client->sess.sessionTeam == TEAM_SPECTATOR)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Not allowed to vote as spectator.\n\"");
    return;
  }

  trap_SendServerCommand(ent - g_entities, "print \"Vote cast.\n\"");

  ent->client->ps.eFlags |= EF_VOTED;

  trap_Argv(1, msg, sizeof(msg));

  if(msg[0] == 'y' || msg[1] == 'Y' || msg[1] == '1')
  {
    level.voteYes++;
    trap_SetConfigstring(CS_VOTE_YES, va("%i", level.voteYes));
  }
  else
  {
    level.voteNo++;
    trap_SetConfigstring(CS_VOTE_NO, va("%i", level.voteNo));
  }

  // a majority will be determined in CheckVote, which will also account
  // for players entering or leaving
}

/*
==================
Cmd_CallTeamVote_f
==================
*/
void Cmd_CallTeamVote_f(gentity_t * ent)
{
  int             i, team, cs_offset;
  char            arg1[MAX_STRING_TOKENS];
  char            arg2[MAX_STRING_TOKENS];

  team = ent->client->sess.sessionTeam;
  if(team == TEAM_RED)
    cs_offset = 0;
  else if(team == TEAM_BLUE)
    cs_offset = 1;
  else
    return;

  if(!g_allowVote.integer)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Voting not allowed here.\n\"");
    return;
  }

  if(level.teamVoteTime[cs_offset])
  {
    trap_SendServerCommand(ent - g_entities, "print \"A team vote is already in progress.\n\"");
    return;
  }
  if(ent->client->pers.teamVoteCount >= MAX_VOTE_COUNT)
  {
    trap_SendServerCommand(ent - g_entities, "print \"You have called the maximum number of team votes.\n\"");
    return;
  }
  if(ent->client->sess.sessionTeam == TEAM_SPECTATOR)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Not allowed to call a vote as spectator.\n\"");
    return;
  }

  // make sure it is a valid command to vote on
  trap_Argv(1, arg1, sizeof(arg1));
  arg2[0] = '\0';
  for(i = 2; i < trap_Argc(); i++)
  {
    if(i > 2)
      strcat(arg2, " ");
    trap_Argv(i, &arg2[strlen(arg2)], sizeof(arg2) - strlen(arg2));
  }

  if(strchr(arg1, ';') || strchr(arg2, ';'))
  {
    trap_SendServerCommand(ent - g_entities, "print \"Invalid vote string.\n\"");
    return;
  }

  if(!Q_stricmp(arg1, "leader"))
  {
    char            netname[MAX_NETNAME], leader[MAX_NETNAME];

    if(!arg2[0])
    {
      i = ent->client->ps.clientNum;
    }
    else
    {
      // numeric values are just slot numbers
      for(i = 0; i < 3; i++)
      {
        if(!arg2[i] || arg2[i] < '0' || arg2[i] > '9')
          break;
      }
      if(i >= 3 || !arg2[i])
      {
        i = atoi(arg2);
        if(i < 0 || i >= level.maxclients)
        {
          trap_SendServerCommand(ent - g_entities, va("print \"Bad client slot: %i\n\"", i));
          return;
        }

        if(!g_entities[i].inuse)
        {
          trap_SendServerCommand(ent - g_entities, va("print \"Client %i is not active\n\"", i));
          return;
        }
      }
      else
      {
        Q_strncpyz(leader, arg2, sizeof(leader));
        Q_CleanStr(leader);
        for(i = 0; i < level.maxclients; i++)
        {
          if(level.clients[i].pers.connected == CON_DISCONNECTED)
            continue;
          if(level.clients[i].sess.sessionTeam != team)
            continue;
          Q_strncpyz(netname, level.clients[i].pers.netname, sizeof(netname));
          Q_CleanStr(netname);
          if(!Q_stricmp(netname, leader))
          {
            break;
          }
        }
        if(i >= level.maxclients)
        {
          trap_SendServerCommand(ent - g_entities, va("print \"%s is not a valid player on your team.\n\"", arg2));
          return;
        }
      }
    }
    Com_sprintf(arg2, sizeof(arg2), "%d", i);
  }
  else
  {
    trap_SendServerCommand(ent - g_entities, "print \"Invalid vote string.\n\"");
    trap_SendServerCommand(ent - g_entities, "print \"Team vote commands are: leader <player>.\n\"");
    return;
  }

  Com_sprintf(level.teamVoteString[cs_offset], sizeof(level.teamVoteString[cs_offset]), "%s %s", arg1, arg2);

  for(i = 0; i < level.maxclients; i++)
  {
    if(level.clients[i].pers.connected == CON_DISCONNECTED)
      continue;
    if(level.clients[i].sess.sessionTeam == team)
      trap_SendServerCommand(i, va("print \"%s called a team vote.\n\"", ent->client->pers.netname));
  }

  // start the voting, the caller autoamtically votes yes
  level.teamVoteTime[cs_offset] = level.time;
  level.teamVoteYes[cs_offset] = 1;
  level.teamVoteNo[cs_offset] = 0;

  for(i = 0; i < level.maxclients; i++)
  {
    if(level.clients[i].sess.sessionTeam == team)
      level.clients[i].ps.eFlags &= ~EF_TEAMVOTED;
  }
  ent->client->ps.eFlags |= EF_TEAMVOTED;

  trap_SetConfigstring(CS_TEAMVOTE_TIME + cs_offset, va("%i", level.teamVoteTime[cs_offset]));
  trap_SetConfigstring(CS_TEAMVOTE_STRING + cs_offset, level.teamVoteString[cs_offset]);
  trap_SetConfigstring(CS_TEAMVOTE_YES + cs_offset, va("%i", level.teamVoteYes[cs_offset]));
  trap_SetConfigstring(CS_TEAMVOTE_NO + cs_offset, va("%i", level.teamVoteNo[cs_offset]));
}

/*
==================
Cmd_TeamVote_f
==================
*/
void Cmd_TeamVote_f(gentity_t * ent)
{
  int             team, cs_offset;
  char            msg[64];

  team = ent->client->sess.sessionTeam;
  if(team == TEAM_RED)
    cs_offset = 0;
  else if(team == TEAM_BLUE)
    cs_offset = 1;
  else
    return;

  if(!level.teamVoteTime[cs_offset])
  {
    trap_SendServerCommand(ent - g_entities, "print \"No team vote in progress.\n\"");
    return;
  }
  if(ent->client->ps.eFlags & EF_TEAMVOTED)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Team vote already cast.\n\"");
    return;
  }
  if(ent->client->sess.sessionTeam == TEAM_SPECTATOR)
  {
    trap_SendServerCommand(ent - g_entities, "print \"Not allowed to vote as spectator.\n\"");
    return;
  }

  trap_SendServerCommand(ent - g_entities, "print \"Team vote cast.\n\"");

  ent->client->ps.eFlags |= EF_TEAMVOTED;

  trap_Argv(1, msg, sizeof(msg));

  if(msg[0] == 'y' || msg[1] == 'Y' || msg[1] == '1')
  {
    level.teamVoteYes[cs_offset]++;
    trap_SetConfigstring(CS_TEAMVOTE_YES + cs_offset, va("%i", level.teamVoteYes[cs_offset]));
  }
  else
  {
    level.teamVoteNo[cs_offset]++;
    trap_SetConfigstring(CS_TEAMVOTE_NO + cs_offset, va("%i", level.teamVoteNo[cs_offset]));
  }

  // a majority will be determined in TeamCheckVote, which will also account
  // for players entering or leaving
}


/*
=================
Cmd_SetViewpos_f
=================
*/
void Cmd_SetViewpos_f(gentity_t * ent)
{
  vec3_t          origin, angles;
  char            buffer[MAX_TOKEN_CHARS];
  int             i;

  if(!g_cheats.integer)
  {
    trap_SendServerCommand(ent - g_entities, va("print \"Cheats are not enabled on this server.\n\""));
    return;
  }
  if(trap_Argc() != 5)
  {
    trap_SendServerCommand(ent - g_entities, va("print \"usage: setviewpos x y z yaw\n\""));
    return;
  }

  VectorClear(angles);
  for(i = 0; i < 3; i++)
  {
    trap_Argv(i + 1, buffer, sizeof(buffer));
    origin[i] = atof(buffer);
  }

  trap_Argv(4, buffer, sizeof(buffer));
  angles[YAW] = atof(buffer);

  TeleportPlayer(ent, origin, angles);
}



/*
=================
Cmd_Stats_f
=================
*/
void Cmd_Stats_f(gentity_t * ent)
{
/*
  int max, n, i;

  max = trap_AAS_PointReachabilityAreaIndex( NULL );

  n = 0;
  for ( i = 0; i < max; i++ ) {
    if ( ent->client->areabits[i >> 3] & (1 << (i & 7)) )
      n++;
  }

  //trap_SendServerCommand( ent-g_entities, va("print \"visited %d of %d areas\n\"", n, max));
  trap_SendServerCommand( ent-g_entities, va("print \"%d%% level coverage\n\"", n * 100 / max));
*/
}

int reload_compare(const void* a, const void* b)
{
  return *(int*)b - *(int*)a;
}

void Cmd_Reload_f(gentity_t* ent)
{
  bool chambered = false;
  playerState_t* ps = &ent->client->ps;

  // Get the weapon attributes
  const hat::Weapon_attrs* attrs;
  if (!trap_GetWeaponAttrs(ps->clientNum, ps->weapon, (const void**)(&attrs))) {
    Com_Printf("WARNING: Cmd_Reload_f: Attempting to reload without a \
               reload time. Bad animation?");
  }

  // Adjust the times for the reload animation
  ps->weaponTime = ps->weaponTime < 0 ? 
    attrs->times.reload : ps->weaponTime + attrs->times.reload;

  // Start the reload animation
  ps->weaponstate = WEAPON_RELOADING;

  // Cycle our magazines
  int* ammo = ps->primary_ammo[ps->weapon];

  // If we have positive ammo, then we can assume a round is chambered
  if (ammo[0] > 0) {
    Com_Printf("NOTICE: Round was already chambered.\n");
    ammo[0]--;
    chambered = true;
  }

  Com_Printf("Before reload: ");
  for (int i = 0; i < MAX_MAGS; i++)
    Com_Printf("%d ", ammo[i]);
  Com_Printf("\nAfter reload: ");

  // We always pull the largest magazine first
  qsort(ammo, MAX_MAGS, sizeof(int), reload_compare);

  for (int i = 0; i < MAX_MAGS; i++)
    Com_Printf("%d ", ammo[i]);

  // If we had a round chambered, then add it back in.
  ammo[0] = chambered ? ammo[0] + 1 : ammo[0];

  if (ammo[0] == 0)
    Com_Printf("WARNING: Cycled to an empty magazine.\n");

  Com_Printf("\n");
}

void Cmd_Firemode_f(gentity_t* ent)
{
  playerState_t* ps = &ent->client->ps;

  // Get the weapon attributes
  const hat::Weapon_attrs* attrs;
  if (!trap_GetWeaponAttrs(ps->clientNum, ps->weapon, (const void**)(&attrs))) {
    Com_Printf("WARNING: Attempted to switch firemodes without a weapon?!");
  }

  int current_mode = ps->weapon_fire_mode[ps->weapon];
  int next_mode = 0;

  for (
    hat::Fire_mode::const_iterator cit = attrs->fire_modes.begin();
    cit != attrs->fire_modes.end();
  ++cit)
  {
    next_mode++;
    if (*cit == current_mode) {
      break;
    }
  }

  if (next_mode >= attrs->fire_modes.size()) {
    next_mode = 0;
  }

  Com_Printf("Switching firemodes from %d to %d\n", current_mode, attrs->fire_modes[next_mode]);

  ps->weapon_fire_mode[ps->weapon] = attrs->fire_modes[next_mode];
}

/*
=================
ClientCommand
=================
*/
void ClientCommand(int clientNum)
{
  gentity_t      *ent;
  char            cmd[MAX_TOKEN_CHARS];

  ent = g_entities + clientNum;
  if(!ent->client)
  {
    return;					// not fully in game yet
  }

  trap_Argv(0, cmd, sizeof(cmd));

#ifdef G_LUA

  if(Q_stricmp(cmd, "lua_status") == 0)
  {
    G_LuaStatus(ent);
    return;
  }

  // Lua API callbacks
  if(G_LuaHook_ClientCommand(clientNum, cmd))
  {
    return;
  }
#endif


#if defined(ACEBOT)
  if(ACECM_Commands(ent))
    return;
#endif

  if(Q_stricmp(cmd, "say") == 0)
  {
    Cmd_Say_f(ent, SAY_ALL, qfalse);
    return;
  }
  if(Q_stricmp(cmd, "say_team") == 0)
  {
    Cmd_Say_f(ent, SAY_TEAM, qfalse);
    return;
  }
  if(Q_stricmp(cmd, "tell") == 0)
  {
    Cmd_Tell_f(ent);
    return;
  }
  if(Q_stricmp(cmd, "vsay") == 0)
  {
    Cmd_Voice_f(ent, SAY_ALL, qfalse, qfalse);
    return;
  }
  if(Q_stricmp(cmd, "vsay_team") == 0)
  {
    Cmd_Voice_f(ent, SAY_TEAM, qfalse, qfalse);
    return;
  }
  if(Q_stricmp(cmd, "vtell") == 0)
  {
    Cmd_VoiceTell_f(ent, qfalse);
    return;
  }
  if(Q_stricmp(cmd, "vosay") == 0)
  {
    Cmd_Voice_f(ent, SAY_ALL, qfalse, qtrue);
    return;
  }
  if(Q_stricmp(cmd, "vosay_team") == 0)
  {
    Cmd_Voice_f(ent, SAY_TEAM, qfalse, qtrue);
    return;
  }
  if(Q_stricmp(cmd, "votell") == 0)
  {
    Cmd_VoiceTell_f(ent, qtrue);
    return;
  }
  if(Q_stricmp(cmd, "vtaunt") == 0)
  {
    Cmd_VoiceTaunt_f(ent);
    return;
  }
  if(Q_stricmp(cmd, "score") == 0)
  {
    Cmd_Score_f(ent);
    return;
  }

  // ignore all other commands when at intermission
  if(level.intermissiontime)
  {
    Cmd_Say_f(ent, qfalse, qtrue);
    return;
  }

  if(Q_stricmp(cmd, "reload") == 0)
    Cmd_Reload_f(ent);
  else if(Q_stricmp(cmd, "firemode") == 0)
    Cmd_Firemode_f(ent);
  else if(Q_stricmp(cmd, "give") == 0)
    Cmd_Give_f(ent);
  else if(Q_stricmp(cmd, "god") == 0)
    Cmd_God_f(ent);
  else if(Q_stricmp(cmd, "notarget") == 0)
    Cmd_Notarget_f(ent);
  else if(Q_stricmp(cmd, "noclip") == 0)
    Cmd_Noclip_f(ent);
  else if(Q_stricmp(cmd, "kill") == 0)
    Cmd_Kill_f(ent);
  else if(Q_stricmp(cmd, "teamtask") == 0)
    Cmd_TeamTask_f(ent);
  else if(Q_stricmp(cmd, "levelshot") == 0)
    Cmd_LevelShot_f(ent);
  else if(Q_stricmp(cmd, "follow") == 0)
    Cmd_Follow_f(ent);
  else if(Q_stricmp(cmd, "follownext") == 0)
    Cmd_FollowCycle_f(ent, 1);
  else if(Q_stricmp(cmd, "followprev") == 0)
    Cmd_FollowCycle_f(ent, -1);
  else if(Q_stricmp(cmd, "team") == 0)
    Cmd_Team_f(ent);
  else if(Q_stricmp(cmd, "where") == 0)
    Cmd_Where_f(ent);
  else if(Q_stricmp(cmd, "callvote") == 0)
    Cmd_CallVote_f(ent);
  else if(Q_stricmp(cmd, "vote") == 0)
    Cmd_Vote_f(ent);
  else if(Q_stricmp(cmd, "callteamvote") == 0)
    Cmd_CallTeamVote_f(ent);
  else if(Q_stricmp(cmd, "teamvote") == 0)
    Cmd_TeamVote_f(ent);
  else if(Q_stricmp(cmd, "gc") == 0)
    Cmd_GameCommand_f(ent);
  else if(Q_stricmp(cmd, "setviewpos") == 0)
    Cmd_SetViewpos_f(ent);
  else if(Q_stricmp(cmd, "stats") == 0)
    Cmd_Stats_f(ent);
  else
    trap_SendServerCommand(clientNum, va("print \"unknown cmd %s\n\"", cmd));
}
