diff options
Diffstat (limited to 'src/game/g_client.c')
| -rw-r--r-- | src/game/g_client.c | 1543 | 
1 files changed, 1543 insertions, 0 deletions
diff --git a/src/game/g_client.c b/src/game/g_client.c new file mode 100644 index 00000000..af9fdee7 --- /dev/null +++ b/src/game/g_client.c @@ -0,0 +1,1543 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +/* + *  Portions Copyright (C) 2000-2001 Tim Angus + * + *  This program 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, or (at your option) + *  any later version. + * + *  This program 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 this program; if not, write to the Free Software + *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +                +/*  To assertain which portions are licensed under the GPL and which are + *  licensed by Id Software, Inc. please run a diff between the equivalent + *  versions of the "Tremulous" modification and the unmodified "Quake3" + *  game source code. + */ + +#include "g_local.h" + +// g_client.c -- client functions that don't happen every frame + +static vec3_t playerMins = {-15, -15, -24}; +static vec3_t playerMaxs = {15, 15, 32}; + +/*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32) initial +potential spawning position for deathmatch games. +The first time a player enters the game, they will be at an 'initial' spot. +Targets will be fired when someone spawns in on them. +"nobots" will prevent bots from using this spot. +"nohumans" will prevent non-bots from using this spot. +*/ +void SP_info_player_deathmatch( gentity_t *ent ) { +  int   i; + +  G_SpawnInt( "nobots", "0", &i); +  if ( i ) { +    ent->flags |= FL_NO_BOTS; +  } +  G_SpawnInt( "nohumans", "0", &i ); +  if ( i ) { +    ent->flags |= FL_NO_HUMANS; +  } +} + +/*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32) +equivelant to info_player_deathmatch +*/ +void SP_info_player_start(gentity_t *ent) { +  ent->classname = "info_player_deathmatch"; +  SP_info_player_deathmatch( ent ); +} + +/*QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point.  Target an info_notnull for the view direction. +*/ +void SP_info_player_intermission( gentity_t *ent ) { + +} + +/*QUAKED info_droid_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point.  Target an info_notnull for the view direction. +*/ +void SP_info_droid_intermission( gentity_t *ent ) { + +} + +/*QUAKED info_human_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point.  Target an info_notnull for the view direction. +*/ +void SP_info_human_intermission( gentity_t *ent ) { + +} + + + +/* +======================================================================= + +  SelectSpawnPoint + +======================================================================= +*/ + +/* +================ +SpotWouldTelefrag + +================ +*/ +qboolean SpotWouldTelefrag( gentity_t *spot ) { +  int     i, num; +  int     touch[MAX_GENTITIES]; +  gentity_t *hit; +  vec3_t    mins, maxs; + +  VectorAdd( spot->s.origin, playerMins, mins ); +  VectorAdd( spot->s.origin, playerMaxs, maxs ); +  num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + +  for (i=0 ; i<num ; i++) { +    hit = &g_entities[touch[i]]; +    //if ( hit->client && hit->client->ps.stats[STAT_HEALTH] > 0 ) { +    if( hit->client ) { +      return qtrue; +    } + +  } + +  return qfalse; +} + +/* +================ +SelectNearestDeathmatchSpawnPoint + +Find the spot that we DON'T want to use +================ +*/ +#define MAX_SPAWN_POINTS  128 +gentity_t *SelectNearestDeathmatchSpawnPoint( vec3_t from ) { +  gentity_t *spot; +  vec3_t    delta; +  float   dist, nearestDist; +  gentity_t *nearestSpot; + +  nearestDist = 999999; +  nearestSpot = NULL; +  spot = NULL; + +  while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + +    VectorSubtract( spot->s.origin, from, delta ); +    dist = VectorLength( delta ); +    if ( dist < nearestDist ) { +      nearestDist = dist; +      nearestSpot = spot; +    } +  } + +  return nearestSpot; +} + + +/* +================ +SelectRandomDeathmatchSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +#define MAX_SPAWN_POINTS  128 +gentity_t *SelectRandomDeathmatchSpawnPoint( void ) { +  gentity_t *spot; +  int     count; +  int     selection; +  gentity_t *spots[MAX_SPAWN_POINTS]; + +  count = 0; +  spot = NULL; + +  while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { +    if ( SpotWouldTelefrag( spot ) ) { +      continue; +    } +    spots[ count ] = spot; +    count++; +  } + +  if ( !count ) { // no spots that won't telefrag +    return G_Find( NULL, FOFS(classname), "info_player_deathmatch"); +  } + +  selection = rand() % count; +  return spots[ selection ]; +} + + +/* +=========== +SelectRandomFurthestSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *SelectRandomFurthestSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) { +  gentity_t *spot; +  vec3_t    delta; +  float   dist; +  float   list_dist[64]; +  gentity_t *list_spot[64]; +  int     numSpots, rnd, i, j; + +  numSpots = 0; +  spot = NULL; + +  while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { +    if ( SpotWouldTelefrag( spot ) ) { +      continue; +    } +    VectorSubtract( spot->s.origin, avoidPoint, delta ); +    dist = VectorLength( delta ); +    for (i = 0; i < numSpots; i++) { +      if ( dist > list_dist[i] ) { +        if ( numSpots >= 64 ) +          numSpots = 64-1; +        for (j = numSpots; j > i; j--) { +          list_dist[j] = list_dist[j-1]; +          list_spot[j] = list_spot[j-1]; +        } +        list_dist[i] = dist; +        list_spot[i] = spot; +        numSpots++; +        if (numSpots > 64) +          numSpots = 64; +        break; +      } +    } +    if (i >= numSpots && numSpots < 64) { +      list_dist[numSpots] = dist; +      list_spot[numSpots] = spot; +      numSpots++; +    } +  } +  if (!numSpots) { +    spot = G_Find( NULL, FOFS(classname), "info_player_deathmatch"); +    if (!spot) +      G_Error( "Couldn't find a spawn point" ); +    VectorCopy (spot->s.origin, origin); +    origin[2] += 9; +    VectorCopy (spot->s.angles, angles); +    return spot; +  } + +  // select a random spot from the spawn points furthest away +  rnd = random() * (numSpots / 2); + +  VectorCopy (list_spot[rnd]->s.origin, origin); +  origin[2] += 9; +  VectorCopy (list_spot[rnd]->s.angles, angles); + +  return list_spot[rnd]; +} + + +/* +================ +SelectDroidSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +gentity_t *SelectDroidSpawnPoint( void ) { +  gentity_t *spot; +  int     count; +  int     selection; +  gentity_t *spots[MAX_SPAWN_POINTS]; + +  count = 0; +  spot = NULL; + +  while ((spot = G_Find (spot, FOFS(classname), "team_droid_spawn")) != NULL) { +    if ( SpotWouldTelefrag( spot ) || ( spot->health <= 0 ) ) { +      continue; +    } +    spots[ count ] = spot; +    count++; +  } + +  if ( !count ) { // no spots that won't telefrag +    spot = G_Find( NULL, FOFS(classname), "team_droid_spawn"); +    if( spot->health > 0 ) +      return spot; +    else +      return NULL; + +  } + +  selection = rand() % count; +  return spots[ selection ]; +} + + +/* +================ +SelectHumanSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +gentity_t *SelectHumanSpawnPoint( void ) { +  gentity_t *spot; +  int     count; +  int     selection; +  gentity_t *spots[MAX_SPAWN_POINTS]; + +  count = 0; +  spot = NULL; + +  while ((spot = G_Find (spot, FOFS(classname), "team_human_spawn")) != NULL) { +    if ( SpotWouldTelefrag( spot ) || ( spot->health <= 0 ) ) { +      continue; +    } +    spots[ count ] = spot; +    count++; +  } + +  if ( !count ) { // no spots that won't telefrag +    spot = G_Find( NULL, FOFS(classname), "team_human_spawn"); +    if( spot->health > 0 ) +      return spot; +    else +      return NULL; +  } + +  selection = rand() % count; +  return spots[ selection ]; +} + + +/* +=========== +SelectSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *SelectSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) { +  return SelectRandomFurthestSpawnPoint( avoidPoint, origin, angles ); + +  /* +  gentity_t *spot; +  gentity_t *nearestSpot; + +  nearestSpot = SelectNearestDeathmatchSpawnPoint( avoidPoint ); + +  spot = SelectRandomDeathmatchSpawnPoint ( ); +  if ( spot == nearestSpot ) { +    // roll again if it would be real close to point of death +    spot = SelectRandomDeathmatchSpawnPoint ( ); +    if ( spot == nearestSpot ) { +      // last try +      spot = SelectRandomDeathmatchSpawnPoint ( ); +    } +  } + +  // find a single player start spot +  if (!spot) { +    G_Error( "Couldn't find a spawn point" ); +  } + +  VectorCopy (spot->s.origin, origin); +  origin[2] += 9; +  VectorCopy (spot->s.angles, angles); + +  return spot; +  */ +} + + +/* +=========== +SelectTremulousSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *SelectTremulousSpawnPoint( int team, vec3_t origin, vec3_t angles ) +{ +  gentity_t *spot; + +  if( team == PTE_DROIDS ) +    spot = SelectDroidSpawnPoint( ); +  else if( team == PTE_HUMANS ) +    spot = SelectHumanSpawnPoint( ); + +  //no available spots +  if( !spot ) +  { +    return NULL; +  } + +  // find a single player start spot +  if (!spot) { +    //G_Error( "Couldn't find a spawn point" ); +  } + +  //TA: why isn't spot->s.origin being set? +  VectorCopy (spot->s.pos.trBase, origin); +  VectorCopy (spot->s.angles, angles); + +  if( team == PTE_DROIDS ) +    origin[2] += 40; +  else if( team == PTE_HUMANS ) +    origin[2] += 29; + +  return spot; +   +} + + +/* +=========== +SelectInitialSpawnPoint + +Try to find a spawn point marked 'initial', otherwise +use normal spawn selection. +============ +*/ +gentity_t *SelectInitialSpawnPoint( vec3_t origin, vec3_t angles ) { +  gentity_t *spot; + +  spot = NULL; +  while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { +    if ( spot->spawnflags & 1 ) { +      break; +    } +  } + +  if ( !spot || SpotWouldTelefrag( spot ) ) { +    return SelectSpawnPoint( vec3_origin, origin, angles ); +  } + +  VectorCopy (spot->s.origin, origin); +  origin[2] += 9; +  VectorCopy (spot->s.angles, angles); + +  return spot; +} + +/* +=========== +SelectSpectatorSpawnPoint + +============ +*/ +gentity_t *SelectSpectatorSpawnPoint( vec3_t origin, vec3_t angles ) { +  FindIntermissionPoint(); + +  VectorCopy( level.intermission_origin, origin ); +  VectorCopy( level.intermission_angle, angles ); + +  return NULL; +} + + +/* +=========== +SelectDroidLockSpawnPoint + +Try to find a spawn point for droid intermission otherwise +use normal intermission spawn. +============ +*/ +gentity_t *SelectDroidLockSpawnPoint( vec3_t origin, vec3_t angles ) { +  gentity_t *spot; + +  spot = NULL; +  spot = G_Find (spot, FOFS(classname), "info_droid_intermission"); + +  if ( !spot ) { +    return SelectSpectatorSpawnPoint( origin, angles ); +  } + +  VectorCopy (spot->s.origin, origin); +  VectorCopy (spot->s.angles, angles); + +  return spot; +} + + +/* +=========== +SelectHumanLockSpawnPoint + +Try to find a spawn point for human intermission otherwise +use normal intermission spawn. +============ +*/ +gentity_t *SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ) { +  gentity_t *spot; + +  spot = NULL; +  spot = G_Find (spot, FOFS(classname), "info_human_intermission"); + +  if ( !spot ) { +    return SelectSpectatorSpawnPoint( origin, angles ); +  } + +  VectorCopy (spot->s.origin, origin); +  VectorCopy (spot->s.angles, angles); + +  return spot; +} + + +/* +======================================================================= + +BODYQUE + +======================================================================= +*/ + +#if BODY_QUEUE_SIZE +/* +=============== +InitBodyQue +=============== +*/ +void InitBodyQue (void) { +  int   i; +  gentity_t *ent; + +  level.bodyQueIndex = 0; +  for (i=0; i<BODY_QUEUE_SIZE ; i++) { +    ent = G_Spawn(); +    ent->classname = "bodyque"; +    ent->neverFree = qtrue; +    level.bodyQue[i] = ent; +  } +} +#endif + +/* +============= +BodySink + +After sitting around for five seconds, fall into the ground and dissapear +============= +*/ +void BodySink( gentity_t *ent ) { +  if ( level.time - ent->timestamp > 6500 ) { +    // the body ques are never actually freed, they are just unlinked +    trap_UnlinkEntity( ent ); +    ent->physicsObject = qfalse; +    return; +  } +  ent->nextthink = level.time + 100; +  ent->s.pos.trBase[2] -= 1; +} + +/* +============= +CopyToBodyQue + +A player is respawning, so make an entity that looks +just like the existing corpse to leave behind. +============= +*/ +void CopyToBodyQue( gentity_t *ent ) { +  gentity_t   *body; +  int     contents; + +  //TA: not really the place for this.. but hey.. +  if( ent->client->torch != NULL && BG_activated( UP_TORCH, ent->client->ps.stats ) ) +  { +    G_FreeEntity( ent->client->torch ); +    trap_UnlinkEntity( ent->client->torch ); +    ent->client->torch = NULL; +  } + +  trap_UnlinkEntity (ent); + +  // if client is in a nodrop area, don't leave the body +  contents = trap_PointContents( ent->s.origin, -1 ); +  if ( contents & CONTENTS_NODROP ) { +    return; +  } +   +  body = G_Spawn( ); +  body->classname = "corpse"; +  body->s = ent->s; +  body->r.s = body->s; +  body->s.eFlags = EF_DEAD; +  body->s.eType = ET_CORPSE; +  body->s.number = body - g_entities; +  body->timestamp = level.time; +  body->physicsObject = qtrue; +  body->s.event = 0; +  body->r.contents = CONTENTS_BODY; +  body->clipmask = MASK_PLAYERSOLID; + +  switch ( body->s.legsAnim & ~ANIM_TOGGLEBIT ) { +  case BOTH_DEATH1: +  case BOTH_DEAD1: +    body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD1; +    break; +  case BOTH_DEATH2: +  case BOTH_DEAD2: +    body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD2; +    break; +  case BOTH_DEATH3: +  case BOTH_DEAD3: +  default: +    body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD3; +    break; +  } + +  //body->die = body_die; + +  // don't take more damage if already gibbed +  if ( ent->health <= GIB_HEALTH ) { +    body->takedamage = qfalse; +  } else { +    body->takedamage = qtrue; +  } + +  //make the make player entity disappear +  ent->takedamage = qfalse; +  ent->s.eType = ET_INVISIBLE; +  ent->r.contents = 0; +  ent->s.solid = 0; +  ent->r.s.solid = 0; +  body->health = ent->health = ent->client->ps.stats[STAT_HEALTH]; +  ent->health = ent->client->ps.stats[STAT_HEALTH] = GIB_HEALTH - 1; +   +  //FIXME: change body dimensions +  VectorSet( body->r.mins, -15, -15, -15 ); +  VectorSet( body->r.maxs, 15, 15, 15 ); +  VectorSet( body->r.absmin, -15, -15, -15 ); +  VectorSet( body->r.absmax, 15, 15, 15 ); + +  if ( body->s.groundEntityNum == ENTITYNUM_NONE ) +  { +    body->s.pos.trType = TR_GRAVITY; +    body->s.pos.trTime = level.time; +    VectorCopy( ent->client->ps.velocity, body->s.pos.trDelta ); +  } +  else +  { +    body->s.pos.trType = TR_STATIONARY; +  } +   +  body->s.pos.trTime = level.time; +             +  VectorCopy ( body->s.pos.trBase, body->r.currentOrigin ); +  trap_LinkEntity( body ); + +} + +//====================================================================== + + +/* +================== +SetClientViewAngle + +================== +*/ +void SetClientViewAngle( gentity_t *ent, vec3_t angle ) { +  int     i; + +  // set the delta angle +  for (i=0 ; i<3 ; i++) { +    int   cmdAngle; + +    cmdAngle = ANGLE2SHORT(angle[i]); +    ent->client->ps.delta_angles[i] = cmdAngle - ent->client->pers.cmd.angles[i]; +  } +  VectorCopy( angle, ent->s.angles ); +  VectorCopy (ent->s.angles, ent->client->ps.viewangles); +} + +/* +================ +respawn +================ +*/ +void respawn( gentity_t *ent ) { +  gentity_t *tent; + +  CopyToBodyQue (ent); + +  //TA: Clients can't respawn - they must go thru the class cmd +  ClientSpawn(ent); + +  //FIXME: need different spawn/respawn functions for different teams + +  // add a teleportation effect +  //tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN ); +  //tent->s.clientNum = ent->s.clientNum; +} + +/* +================ +TeamCount + +Returns number of players on a team +================ +*/ +team_t TeamCount( int ignoreClientNum, int team ) { +  int   i; +  int   count = 0; + +  for ( i = 0 ; i < level.maxclients ; i++ ) { +    if ( i == ignoreClientNum ) { +      continue; +    } +    if ( level.clients[i].pers.connected == CON_DISCONNECTED ) { +      continue; +    } +    if ( level.clients[i].sess.sessionTeam == team ) { +      count++; +    } +  } + +  return count; +} + + +/* +================ +TeamLeader + +Returns the client number of the team leader +================ +*/ +int TeamLeader( int team ) { +  int   i; + +  for ( i = 0 ; i < level.maxclients ; i++ ) { +    if ( level.clients[i].pers.connected == CON_DISCONNECTED ) { +      continue; +    } +    if ( level.clients[i].sess.sessionTeam == team ) { +      if ( level.clients[i].sess.teamLeader ) +        return i; +    } +  } + +  return -1; +} + + +/* +================ +PickTeam + +================ +*/ +team_t PickTeam( int ignoreClientNum ) { +  int   counts[TEAM_NUM_TEAMS]; + +  counts[TEAM_DROIDS] = TeamCount( ignoreClientNum, TEAM_DROIDS ); +  counts[TEAM_HUMANS] = TeamCount( ignoreClientNum, TEAM_HUMANS ); + +  if ( counts[TEAM_DROIDS] > counts[TEAM_HUMANS] ) { +    return TEAM_HUMANS; +  } +  if ( counts[TEAM_HUMANS] > counts[TEAM_DROIDS] ) { +    return TEAM_DROIDS; +  } +  // equal team count, so join the team with the lowest score +  if ( level.teamScores[TEAM_DROIDS] > level.teamScores[TEAM_HUMANS] ) { +    return TEAM_HUMANS; +  } +  return TEAM_DROIDS; +} + +/* +=========== +ForceClientSkin + +Forces a client's skin (for teamplay) +=========== +*/ +static void ForceClientSkin( gclient_t *client, char *model, const char *skin ) { +  char *p; + +  if ((p = Q_strrchr(model, '/')) != 0) { +    *p = 0; +  } + +  Q_strcat(model, MAX_QPATH, "/"); +  Q_strcat(model, MAX_QPATH, skin); +} + + +/* +=========== +ClientCheckName +============ +*/ +static void ClientCleanName( const char *in, char *out, int outSize ) { +  int   len, colorlessLen; +  char  ch; +  char  *p; +  int   spaces; + +  //save room for trailing null byte +  outSize--; + +  len = 0; +  colorlessLen = 0; +  p = out; +  *p = 0; +  spaces = 0; + +  while( 1 ) { +    ch = *in++; +    if( !ch ) { +      break; +    } + +    // don't allow leading spaces +    if( !*p && ch == ' ' ) { +      continue; +    } + +    // check colors +    if( ch == Q_COLOR_ESCAPE ) { +      // solo trailing carat is not a color prefix +      if( !*in ) { +        break; +      } + +      // don't allow black in a name, period +      if( ColorIndex(*in) == 0 ) { +        in++; +        continue; +      } + +      // make sure room in dest for both chars +      if( len > outSize - 2 ) { +        break; +      } + +      *out++ = ch; +      *out++ = *in++; +      len += 2; +      continue; +    } + +    // don't allow too many consecutive spaces +    if( ch == ' ' ) { +      spaces++; +      if( spaces > 3 ) { +        continue; +      } +    } +    else { +      spaces = 0; +    } + +    if( len > outSize - 1 ) { +      break; +    } + +    *out++ = ch; +    colorlessLen++; +    len++; +  } +  *out = 0; + +  // don't allow empty names +  if( *p == 0 || colorlessLen == 0 ) { +    Q_strncpyz( p, "UnnamedPlayer", outSize ); +  } +} + + +/* +=========== +ClientUserInfoChanged + +Called from ClientConnect when the player first connects and +directly by the server system when the player updates a userinfo variable. + +The game can override any of the settings and call trap_SetUserinfo +if desired. +============ +*/ +void ClientUserinfoChanged( int clientNum ) { +  gentity_t *ent; +  int   teamTask, teamLeader, team, health; +  char  *s; +  char  model[MAX_QPATH]; +  char  oldname[MAX_STRING_CHARS]; +  gclient_t *client; +  char  c1[MAX_INFO_STRING]; +  char  redTeam[MAX_INFO_STRING]; +  char  blueTeam[MAX_INFO_STRING]; +  char  userinfo[MAX_INFO_STRING]; + +  ent = g_entities + clientNum; +  client = ent->client; + +  trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + +  // check for malformed or illegal info strings +  if ( !Info_Validate(userinfo) ) { +    strcpy (userinfo, "\\name\\badinfo"); +  } + +  // check for local client +  s = Info_ValueForKey( userinfo, "ip" ); +  if ( !strcmp( s, "localhost" ) ) { +    client->pers.localClient = qtrue; +  } + +  // check the item prediction +  s = Info_ValueForKey( userinfo, "cg_predictItems" ); +  if ( !atoi( s ) ) { +    client->pers.predictItemPickup = qfalse; +  } else { +    client->pers.predictItemPickup = qtrue; +  } + +  // set name +  Q_strncpyz ( oldname, client->pers.netname, sizeof( oldname ) ); +  s = Info_ValueForKey (userinfo, "name"); +  ClientCleanName( s, client->pers.netname, sizeof(client->pers.netname) ); + +  if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { +    if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { +      Q_strncpyz( client->pers.netname, "scoreboard", sizeof(client->pers.netname) ); +    } +  } + +  if ( client->pers.connected == CON_CONNECTED ) { +    if ( strcmp( oldname, client->pers.netname ) ) { +      trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " renamed to %s\n\"", oldname, +        client->pers.netname) ); +    } +  } + +  // set max health +  health = atoi( Info_ValueForKey( userinfo, "handicap" ) ); +  client->pers.maxHealth = health; +  if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) { +    client->pers.maxHealth = 100; +  } +  //client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth; + +  // set model +  //Q_strncpyz( model, Info_ValueForKey (userinfo, "model"), sizeof( model ) ); +  switch( client->pers.pclass ) +  { +    case PCL_D_BASE: +      Q_strncpyz( model, "klesk", sizeof( model ) ); +      break; +    case PCL_D_BUILDER: +      Q_strncpyz( model, "lucy", sizeof( model ) ); +      break; +    case PCL_H_BASE: +      Q_strncpyz( model, "sarge", sizeof( model ) ); +      break; +    default: +      Q_strncpyz( model, "grunt", sizeof( model ) ); +  } + +  // team +  switch( client->sess.sessionTeam ) { +  case TEAM_HUMANS: +    ForceClientSkin(client, model, "red"); +    break; +  case TEAM_DROIDS: +    ForceClientSkin(client, model, "blue"); +    break; +  } +  if ( g_gametype.integer >= GT_TEAM && client->sess.sessionTeam == TEAM_SPECTATOR ) { +    // don't ever use a default skin in teamplay, it would just waste memory +    ForceClientSkin(client, model, "red"); +  } + + + +  // teamInfo +  s = Info_ValueForKey( userinfo, "teamoverlay" ); +  if ( ! *s || atoi( s ) != 0 ) { +    client->pers.teamInfo = qtrue; +  } else { +    client->pers.teamInfo = qfalse; +  } + +  // team task (0 = none, 1 = offence, 2 = defence) +  teamTask = atoi(Info_ValueForKey(userinfo, "teamtask")); +  // team Leader (1 = leader, 0 is normal player) +  teamLeader = client->sess.teamLeader; + +  // colors +  strcpy(c1, Info_ValueForKey( userinfo, "color" )); +  strcpy(redTeam, "humans"); +  strcpy(blueTeam, "droids"); +                 +  // send over a subset of the userinfo keys so other clients can +  // print scoreboards, display models, and play custom sounds +  if ( ent->r.svFlags & SVF_BOT ) { +    s = va("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i\\skill\\%s\\tt\\%d\\tl\\%d", +      client->pers.netname, client->sess.sessionTeam, model, model, c1, +      client->pers.maxHealth, client->sess.wins, client->sess.losses, +      Info_ValueForKey( userinfo, "skill" ), teamTask, teamLeader ); +  } else { +    s = va("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\g_redteam\\%s\\g_blueteam\\%s\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i\\tt\\%d\\tl\\%d", +      client->pers.netname, client->sess.sessionTeam, model, model, redTeam, blueTeam, c1, +      client->pers.maxHealth, client->sess.wins, client->sess.losses, teamTask, teamLeader); +  } + +  trap_SetConfigstring( CS_PLAYERS+clientNum, s ); + +  G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, s ); +} + + +/* +=========== +ClientConnect + +Called when a player begins connecting to the server. +Called again for every map change or tournement restart. + +The session information will be valid after exit. + +Return NULL if the client should be allowed, otherwise return +a string with the reason for denial. + +Otherwise, the client will be sent the current gamestate +and will eventually get to ClientBegin. + +firstTime will be qtrue the very first time a client connects +to the server machine, but qfalse on map changes and tournement +restarts. +============ +*/ +char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ) { +  char    *value; +  gclient_t *client; +  char    userinfo[MAX_INFO_STRING]; +  gentity_t *ent; + +  ent = &g_entities[ clientNum ]; + +  trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + +  // check to see if they are on the banned IP list +  value = Info_ValueForKey (userinfo, "ip"); +  if ( G_FilterPacket( value ) ) { +    return "Banned."; +  } + +  // check for a password +  value = Info_ValueForKey (userinfo, "password"); +  if ( g_password.string[0] && Q_stricmp( g_password.string, "none" ) && +    strcmp( g_password.string, value) != 0) { +    return "Invalid password"; +  } + +  // they can connect +  ent->client = level.clients + clientNum; +  client = ent->client; + +  memset( client, 0, sizeof(*client) ); + +  client->pers.connected = CON_CONNECTING; + +  // read or initialize the session data +  if ( firstTime || level.newSession ) { +    G_InitSessionData( client, userinfo ); +  } +  G_ReadSessionData( client ); + +  //TA: rip bots +  /*if( isBot ) { +    ent->r.svFlags |= SVF_BOT; +    ent->inuse = qtrue; +    if( !G_BotConnect( clientNum, !firstTime ) ) { +      return "BotConnectfailed"; +    } +  }*/ + +  // get and distribute relevent paramters +  G_LogPrintf( "ClientConnect: %i\n", clientNum ); +  ClientUserinfoChanged( clientNum ); + +  // don't do the "xxx connected" messages if they were caried over from previous level +  if ( firstTime ) { +    trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " connected\n\"", client->pers.netname) ); +  } + +  if ( g_gametype.integer >= GT_TEAM && +    client->sess.sessionTeam != TEAM_SPECTATOR ) { +    BroadcastTeamChange( client, -1 ); +  } + +  // count current clients and rank for scoreboard +  CalculateRanks(); + +  return NULL; +} + +/* +=========== +ClientBegin + +called when a client has finished connecting, and is ready +to be placed into the level.  This will happen every level load, +and on transition between teams, but doesn't happen on respawns +============ +*/ +void ClientBegin( int clientNum ) { +  gentity_t *ent; +  gclient_t *client; +  gentity_t *tent; +  int     flags; + +  ent = g_entities + clientNum; + +  //TA: rip bots +  /*if( ent->botDelayBegin ) { +    G_QueueBotBegin( clientNum ); +    ent->botDelayBegin = qfalse; +    return; +  }*/ + +  client = level.clients + clientNum; + +  if ( ent->r.linked ) { +    trap_UnlinkEntity( ent ); +  } +  G_InitGentity( ent ); +  ent->touch = 0; +  ent->pain = 0; +  ent->client = client; + +  client->pers.connected = CON_CONNECTED; +  client->pers.enterTime = level.time; +  client->pers.teamState.state = TEAM_BEGIN; + +  // save eflags around this, because changing teams will +  // cause this to happen with a valid entity, and we +  // want to make sure the teleport bit is set right +  // so the viewpoint doesn't interpolate through the +  // world to the new position +  flags = client->ps.eFlags; +  memset( &client->ps, 0, sizeof( client->ps ) ); +  client->ps.eFlags = flags; + +  // locate ent at a spawn point + +  ClientSpawn( ent ); + +  if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { +    // send event +    tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN ); +    tent->s.clientNum = ent->s.clientNum; + +    if ( g_gametype.integer != GT_TOURNAMENT ) { +      trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname) ); +    } +  } +  G_LogPrintf( "ClientBegin: %i\n", clientNum ); + +  // count current clients and rank for scoreboard +  CalculateRanks(); +} + +/* +=========== +ClientSpawn + +Called every time a client is placed fresh in the world: +after the first ClientBegin, and after each respawn +Initializes all non-persistant parts of playerState +============ +*/ +void ClientSpawn(gentity_t *ent) { +  int   index; +  vec3_t  spawn_origin, spawn_angles; +  gclient_t *client; +  int   i; +  clientPersistant_t  saved; +  clientSession_t   savedSess; +  int   persistant[MAX_PERSISTANT]; +  gentity_t *spawnPoint; +  int   flags; +  int   savedPing; +  int   ammoIndex, ammoSubIndex; +  int   teamLocal; +  int   accuracy_hits, accuracy_shots; +  int   savedEvents[MAX_PS_EVENTS]; +  int   eventSequence; +  char  userinfo[MAX_INFO_STRING]; + +  index = ent - g_entities; +  client = ent->client; + +  teamLocal = client->pers.pteam; + +  //TA: only start client if chosen a class and joined a team +  if( client->pers.pclass == 0 && teamLocal == 0 ) +  { +    client->sess.sessionTeam = TEAM_SPECTATOR; +    client->sess.spectatorState = SPECTATOR_FREE; +  } +  else if( client->pers.pclass == 0 ) +  { +    client->sess.sessionTeam = TEAM_SPECTATOR; +    client->sess.spectatorState = SPECTATOR_LOCKED; +  } + +  // find a spawn point +  // do it before setting health back up, so farthest +  // ranging doesn't count this client +  if ( client->sess.sessionTeam == TEAM_SPECTATOR ) +  { +    if( teamLocal == PTE_NONE ) +      spawnPoint = SelectSpectatorSpawnPoint ( spawn_origin, spawn_angles); +    else if( teamLocal == PTE_DROIDS ) +      spawnPoint = SelectDroidLockSpawnPoint ( spawn_origin, spawn_angles); +    else if( teamLocal == PTE_HUMANS ) +      spawnPoint = SelectHumanLockSpawnPoint ( spawn_origin, spawn_angles); +  } +  else +  { +    // don't spawn near existing origin if possible +    spawnPoint = SelectTremulousSpawnPoint( teamLocal, spawn_origin, spawn_angles ); + +    if( spawnPoint == NULL ) +    { +      trap_SendServerCommand( ent-g_entities, va("print \"No suitable spawns available\n\"" ) ); +      return; +    } +  } +  client->pers.teamState.state = TEAM_ACTIVE; + +  // toggle the teleport bit so the client knows to not lerp +  flags = ent->client->ps.eFlags & ( EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED ); +  flags ^= EF_TELEPORT_BIT; + +  // clear everything but the persistant data + +  saved = client->pers; +  savedSess = client->sess; +  savedPing = client->ps.ping; +  accuracy_hits = client->accuracy_hits; +  accuracy_shots = client->accuracy_shots; +  for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) { +    persistant[i] = client->ps.persistant[i]; +  } +  // also save the predictable events otherwise we might get double or dropped events +  for (i = 0; i < MAX_PS_EVENTS; i++) { +    savedEvents[i] = client->ps.events[i]; +  } +  eventSequence = client->ps.eventSequence; +  memset (client, 0, sizeof(*client)); +   +  client->pers = saved; +  client->sess = savedSess; +  client->ps.ping = savedPing; +  client->accuracy_hits = accuracy_hits; +  client->accuracy_shots = accuracy_shots; +  client->lastkilled_client = -1; +  for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) { +    client->ps.persistant[i] = persistant[i]; +  } +  for (i = 0; i < MAX_PS_EVENTS; i++) { +    client->ps.events[i] = savedEvents[i]; +  } +  client->ps.eventSequence = eventSequence; + +  if( client->sess.sessionTeam == TEAM_SPECTATOR ) +  { +    if( teamLocal == PTE_DROIDS ) +      G_AddEvent( ent, EV_MENU, MN_DROID ); +    else if( teamLocal == PTE_HUMANS ) +      G_AddEvent( ent, EV_MENU, MN_HUMAN ); +  } + +  // increment the spawncount so the client will detect the respawn +  client->ps.persistant[PERS_SPAWN_COUNT]++; +  client->ps.persistant[PERS_TEAM] = client->sess.sessionTeam; + +  client->airOutTime = level.time + 12000; + +  trap_GetUserinfo( index, userinfo, sizeof(userinfo) ); +  client->ps.eFlags = flags; + +  //Com_Printf( "ent->client->pers->pclass = %i\n", ent->client->pers.pclass ); + +  ent->s.groundEntityNum = ENTITYNUM_NONE; +  ent->client = &level.clients[index]; +  ent->takedamage = qtrue; +  ent->inuse = qtrue; +  ent->classname = "player"; +  ent->r.contents = CONTENTS_BODY; +  ent->clipmask = MASK_PLAYERSOLID; +  ent->die = player_die; +  ent->waterlevel = 0; +  ent->watertype = 0; +  ent->flags = 0; + +  client->ps.stats[ STAT_WEAPONS ] = 0; +  client->ps.stats[ STAT_WEAPONS2 ] = 0; + +  // clear entity values +  switch( ent->client->pers.pclass ) +  { +    case PCL_D_BUILDER: +      client->pers.maxHealth = 50; +      client->ps.stats[STAT_MAX_HEALTH] = 50; +      client->ps.stats[STAT_ARMOR] = 50; + +      client->ps.eFlags = flags; +       +      VectorCopy (playerMins, ent->r.mins); +      VectorCopy (playerMaxs, ent->r.maxs); + +      client->ps.clientNum = index; + +      BG_packWeapon( WP_ABUILD, client->ps.stats ); +      BG_packAmmoArray( WP_ABUILD, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); +       +      client->ps.stats[ STAT_ABILITIES ] |= SCA_TAKESFALLDAMAGE; +      BG_packAttributes( 80, 15, 350, client->ps.stats ); +      client->classSpeed = 0.5; +      break; + +    case PCL_D_BASE: +      client->pers.maxHealth = 25; +      client->ps.stats[STAT_MAX_HEALTH] = 25; +      client->ps.eFlags = flags; + +      VectorCopy (playerMins, ent->r.mins); +      VectorCopy (playerMaxs, ent->r.maxs); + +      client->ps.clientNum = index; + +      BG_packWeapon( WP_VENOM, client->ps.stats ); +      BG_packAmmoArray( WP_VENOM, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); + +      client->ps.stats[ STAT_ABILITIES ] |= SCA_WALLCLIMBER; +      client->ps.stats[ STAT_ABILITIES ] |= SCA_CANJUMP; +      client->ps.stats[ STAT_ABILITIES ] |= SCA_NOWEAPONDRIFT; +      BG_packAttributes( 140, 0, 25, client->ps.stats ); +      client->classSpeed = 2.0; +      break; + +    case PCL_H_BASE: +      client->pers.maxHealth = 100; +      client->ps.stats[STAT_MAX_HEALTH] = 100; +      client->ps.stats[STAT_ARMOR] = 50; +       +      client->ps.eFlags = flags; + +      VectorCopy (playerMins, ent->r.mins); +      VectorCopy (playerMaxs, ent->r.maxs); + +      client->ps.clientNum = index; + +      /*BG_packWeapon( WP_MACHINEGUN, client->ps.stats ); +      BG_packAmmoArray( WP_MACHINEGUN, client->ps.ammo, client->ps.powerups, CS_MG, 4, 4 );*/ +       +      client->ps.stats[ STAT_ABILITIES ] |= SCA_TAKESFALLDAMAGE; +      client->ps.stats[ STAT_ABILITIES ] |= SCA_CANJUMP; +      BG_packAttributes( 90, 2, 200, client->ps.stats ); +      client->classSpeed = 1.0; +      break; +       +    //eventually remove this case (or report an error) when all classes implemented +    default: +      client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth; +      client->ps.eFlags = flags; + +      VectorCopy (playerMins, ent->r.mins); +      VectorCopy (playerMaxs, ent->r.maxs); + +      client->ps.clientNum = index; + +      BG_packWeapon( WP_MACHINEGUN, client->ps.stats ); +      BG_packAmmoArray( WP_MACHINEGUN, client->ps.ammo, client->ps.powerups, 100, 0, 0 ); + +      BG_packWeapon( WP_GAUNTLET, client->ps.stats ); +      BG_packAmmoArray( WP_GAUNTLET, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); +       +      BG_packAmmoArray( WP_GRAPPLING_HOOK, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); + +      client->ps.stats[ STAT_ABILITIES ] |= SCA_TAKESFALLDAMAGE; +      client->ps.stats[ STAT_ABILITIES ] |= SCA_CANZOOM; +      client->ps.stats[ STAT_ABILITIES ] |= SCA_CANJUMP; +      BG_packAttributes( 90, 2, 200, client->ps.stats ); +      client->classSpeed = 1.0; + +  } + +  ent->client->ps.stats[ STAT_PCLASS ] = ent->client->pers.pclass; +  ent->client->ps.stats[ STAT_PTEAM ] = ent->client->pers.pteam; + + +  // health will count down towards max_health +  ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH]; //* 1.25; + +  G_SetOrigin( ent, spawn_origin ); +  VectorCopy( spawn_origin, client->ps.origin ); + +  // the respawned flag will be cleared after the attack and jump keys come up +  client->ps.pm_flags |= PMF_RESPAWNED; + +  trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd ); +  SetClientViewAngle( ent, spawn_angles ); + +  if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + +  } else { +    G_KillBox( ent ); +    trap_LinkEntity (ent); + +    // force the base weapon up +    client->ps.weapon = WP_NONE; +    client->ps.weaponstate = WEAPON_READY; + +  } + +  // don't allow full run speed for a bit +  client->ps.pm_flags |= PMF_TIME_KNOCKBACK; +  client->ps.pm_time = 100; + +  client->respawnTime = level.time; +  client->inactivityTime = level.time + g_inactivity.integer * 1000; +  client->latched_buttons = 0; + +  // set default animations +  client->ps.torsoAnim = TORSO_STAND; +  client->ps.legsAnim = LEGS_IDLE; + +  if ( level.intermissiontime ) { +    MoveClientToIntermission( ent ); +  } else { +    // fire the targets of the spawn point +    G_UseTargets( spawnPoint, ent ); + +    // select the highest weapon number available, after any +    // spawn given items have fired +    client->ps.weapon = 1; +    for ( i = WP_NUM_WEAPONS - 1 ; i > 0 ; i-- ) { +      if ( BG_gotWeapon( i, client->ps.stats ) ) { +        client->ps.weapon = i; +        break; +      } +    } +  } + +  // run a client frame to drop exactly to the floor, +  // initialize animations and other things +  client->ps.commandTime = level.time - 100; +  ent->client->pers.cmd.serverTime = level.time; +  ClientThink( ent-g_entities ); + +  // positively link the client, even if the command times are weird +  if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { +    BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); +    VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); +    trap_LinkEntity( ent ); +  } + +  //TA: must do this here so the number of active clients is calculated +  CalculateRanks(); + +  // run the presend to set anything else +  ClientEndFrame( ent ); + +  // clear entity state values +  BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); +} + + +/* +=========== +ClientDisconnect + +Called when a player drops from the server. +Will not be called between levels. + +This should NOT be called directly by any game logic, +call trap_DropClient(), which will call this and do +server system housekeeping. +============ +*/ +void ClientDisconnect( int clientNum ) { +  gentity_t *ent; +  gentity_t *tent; +  int     i; + +  ent = g_entities + clientNum; +  if ( !ent->client ) { +    return; +  } + +  // stop any following clients +  for ( i = 0 ; i < level.maxclients ; i++ ) { +    if ( level.clients[i].sess.sessionTeam == TEAM_SPECTATOR +      && level.clients[i].sess.spectatorState == SPECTATOR_FOLLOW +      && level.clients[i].sess.spectatorClient == clientNum ) { +      StopFollowing( &g_entities[i] ); +    } +  } + +  // send effect if they were completely connected +  if ( ent->client->pers.connected == CON_CONNECTED +    && ent->client->sess.sessionTeam != TEAM_SPECTATOR ) { +    tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); +    tent->s.clientNum = ent->s.clientNum; + +    // They don't get to take powerups with them! +    // Especially important for stuff like CTF flags +    TossClientItems ( ent ); +  } + +  G_LogPrintf( "ClientDisconnect: %i\n", clientNum ); + +  // if we are playing in tourney mode and losing, give a win to the other player +  if ( ( g_gametype.integer == GT_TOURNAMENT ) +    && !level.intermissiontime +    && !level.warmupTime && level.sortedClients[1] == clientNum ) { +    level.clients[ level.sortedClients[0] ].sess.wins++; +    ClientUserinfoChanged( level.sortedClients[0] ); +  } + +  trap_UnlinkEntity (ent); +  ent->s.modelindex = 0; +  ent->inuse = qfalse; +  ent->classname = "disconnected"; +  ent->client->pers.connected = CON_DISCONNECTED; +  ent->client->ps.persistant[PERS_TEAM] = TEAM_FREE; +  ent->client->sess.sessionTeam = TEAM_FREE; + +  trap_SetConfigstring( CS_PLAYERS + clientNum, ""); + +  CalculateRanks(); + +  //TA: rip bots +  /*if ( ent->r.svFlags & SVF_BOT ) { +    BotAIShutdownClient( clientNum ); +  }*/ +}  | 
