1842 lines
44 KiB
C
1842 lines
44 KiB
C
|
// Copyright (C) 2001-2002 Raven Software
|
||
|
//
|
||
|
#include "g_local.h"
|
||
|
|
||
|
// g_client.c -- client functions that don't happen every frame
|
||
|
|
||
|
static vec3_t playerMins = {-15, -15, -46};
|
||
|
static vec3_t playerMaxs = {15, 15, 48};
|
||
|
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_AddClientSpawn
|
||
|
|
||
|
adds a spawnpoint to the spawnpoint array using the given entity for
|
||
|
origin and angles as well as the team for filtering teams.
|
||
|
================
|
||
|
*/
|
||
|
void G_AddClientSpawn ( gentity_t* ent, team_t team )
|
||
|
{
|
||
|
static vec3_t mins = {-15,-15,-45};
|
||
|
static vec3_t maxs = {15,15,46};
|
||
|
vec3_t end;
|
||
|
trace_t tr;
|
||
|
|
||
|
// Drop it to the ground, and if it starts solid just throw it out
|
||
|
VectorCopy ( ent->s.origin, end );
|
||
|
end[2] -= 1024;
|
||
|
|
||
|
tr.fraction = 0.0f;
|
||
|
trap_Trace ( &tr, ent->s.origin, mins, maxs, end, ent->s.number, MASK_SOLID );
|
||
|
|
||
|
// We are only looking for terrain collisions at this point
|
||
|
if ( tr.contents & CONTENTS_TERRAIN )
|
||
|
{
|
||
|
// If its in the ground then throw it awway
|
||
|
if ( tr.startsolid )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
// Drop it down to the ground now
|
||
|
else if ( tr.fraction < 1.0f && tr.fraction > 0.0f )
|
||
|
{
|
||
|
VectorCopy ( tr.endpos, ent->s.origin );
|
||
|
ent->s.origin[2] += 1.0f;
|
||
|
tr.startsolid = qfalse;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( tr.startsolid )
|
||
|
{
|
||
|
Com_Printf ( S_COLOR_YELLOW "WARNING: gametype_player starting in solid at %.2f,%.2f,%.2f\n", ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] );
|
||
|
}
|
||
|
|
||
|
level.spawns[level.spawnCount].team = team;
|
||
|
|
||
|
// Release the entity and store the spawn in its own array
|
||
|
VectorCopy ( ent->s.origin, level.spawns[level.spawnCount].origin );
|
||
|
VectorCopy ( ent->s.angles, level.spawns[level.spawnCount].angles );
|
||
|
|
||
|
// Increase the spawn count
|
||
|
level.spawnCount++;
|
||
|
}
|
||
|
|
||
|
/*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -46) (16 16 48) 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.
|
||
|
*/
|
||
|
void SP_info_player_deathmatch( gentity_t *ent )
|
||
|
{
|
||
|
// Cant take any more spawns!!
|
||
|
if ( level.spawnCount >= MAX_SPAWNS )
|
||
|
{
|
||
|
G_FreeEntity ( ent );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
G_AddClientSpawn ( ent, TEAM_FREE );
|
||
|
|
||
|
G_FreeEntity ( ent );
|
||
|
}
|
||
|
|
||
|
/*QUAKED info_player_intermission (1 0 1) (-16 -16 -46) (16 16 48)
|
||
|
The intermission will be viewed from this point. Target an info_notnull for the view direction.
|
||
|
*/
|
||
|
void SP_info_player_intermission( gentity_t *ent )
|
||
|
{
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_SpotWouldTelefrag
|
||
|
================
|
||
|
*/
|
||
|
qboolean G_SpotWouldTelefrag( gspawn_t* spawn )
|
||
|
{
|
||
|
int i, num;
|
||
|
int touch[MAX_GENTITIES];
|
||
|
gentity_t *hit;
|
||
|
vec3_t mins, maxs;
|
||
|
|
||
|
VectorAdd( spawn->origin, playerMins, mins );
|
||
|
VectorAdd( spawn->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)
|
||
|
{
|
||
|
if ( G_IsClientSpectating ( hit->client ) )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( G_IsClientDead ( hit->client ) )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
return qtrue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_SelectRandomSpawnPoint
|
||
|
|
||
|
go to a random point that doesn't telefrag
|
||
|
================
|
||
|
*/
|
||
|
gspawn_t* G_SelectRandomSpawnPoint ( team_t team )
|
||
|
{
|
||
|
int i;
|
||
|
int count;
|
||
|
int tfcount;
|
||
|
gspawn_t *spawns[MAX_SPAWNS];
|
||
|
gspawn_t *tfspawns[MAX_SPAWNS];
|
||
|
|
||
|
count = 0;
|
||
|
tfcount = 0;
|
||
|
|
||
|
for ( i = 0; i < level.spawnCount; i ++ )
|
||
|
{
|
||
|
gspawn_t* spawn = &level.spawns[i];
|
||
|
|
||
|
if ( team != -1 && team != spawn->team )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( G_SpotWouldTelefrag( spawn ) )
|
||
|
{
|
||
|
tfspawns[tfcount++] = spawn;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
spawns[ count++ ] = spawn;
|
||
|
}
|
||
|
|
||
|
// no spots that won't telefrag so just pick one that will
|
||
|
if ( !count )
|
||
|
{
|
||
|
// No telefrag spots, just return NULL since there is no more to find
|
||
|
if ( !tfcount )
|
||
|
{
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
// telefrag someone
|
||
|
return tfspawns[ rand() % tfcount ];
|
||
|
}
|
||
|
|
||
|
return spawns[ rand() % count ];
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
G_SelectRandomSafeSpawnPoint
|
||
|
|
||
|
Select a random spawn point that is safe for the client to spawn at. A safe spawn point
|
||
|
is one that is at least a certain distance from another client.
|
||
|
============
|
||
|
*/
|
||
|
gspawn_t* G_SelectRandomSafeSpawnPoint ( team_t team, float safeDistance )
|
||
|
{
|
||
|
gspawn_t* spawns[MAX_SPAWNS];
|
||
|
float safeDistanceSquared;
|
||
|
int count;
|
||
|
int i;
|
||
|
|
||
|
// Square the distance for faster comparisons
|
||
|
safeDistanceSquared = safeDistance * safeDistance;
|
||
|
|
||
|
// Build a list of spawns
|
||
|
for ( i = 0, count = 0; i < level.spawnCount; i ++ )
|
||
|
{
|
||
|
gspawn_t* spawn = &level.spawns[i];
|
||
|
int j;
|
||
|
|
||
|
// Ensure the team matches
|
||
|
if ( team != -1 && team != spawn->team )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Make sure this spot wont kill another player
|
||
|
if ( G_SpotWouldTelefrag( spawn ) )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Loop through connected clients
|
||
|
for ( j = 0; j < level.numConnectedClients && count < MAX_SPAWNS; j ++ )
|
||
|
{
|
||
|
gentity_t* other = &g_entities[level.sortedClients[j]];
|
||
|
vec3_t diff;
|
||
|
|
||
|
if ( other->client->pers.connected != CON_CONNECTED )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Skip clients that are spectating or dead
|
||
|
if ( G_IsClientSpectating ( other->client ) || G_IsClientDead ( other->client ) )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// on safe team, dont count this guy
|
||
|
if ( level.gametypeData->teams && team == other->client->sess.team )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
VectorSubtract ( other->r.currentOrigin, spawn->origin, diff );
|
||
|
|
||
|
// Far enough away to qualify
|
||
|
if ( VectorLengthSquared ( diff ) < safeDistanceSquared )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we didnt go through the whole list of clients then we must
|
||
|
// have hit one that was too close. But if we did go through teh whole
|
||
|
// list then this spawn point is good to go
|
||
|
if ( j >= level.numConnectedClients )
|
||
|
{
|
||
|
spawns[count++] = spawn;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Nothing found, try it at half the safe distance
|
||
|
if ( !count )
|
||
|
{
|
||
|
// Gotta stop somewhere
|
||
|
if ( safeDistance / 2 < 250 )
|
||
|
{
|
||
|
return G_SelectRandomSpawnPoint ( team );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return G_SelectRandomSafeSpawnPoint ( team, safeDistance / 2 );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Spawn them at one of the spots
|
||
|
return spawns[ rand() % count ];
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
G_SelectSpectatorSpawnPoint
|
||
|
============
|
||
|
*/
|
||
|
gspawn_t* G_SelectSpectatorSpawnPoint( void )
|
||
|
{
|
||
|
static gspawn_t spawn;
|
||
|
|
||
|
FindIntermissionPoint();
|
||
|
|
||
|
VectorCopy( level.intermission_origin, spawn.origin );
|
||
|
VectorCopy( level.intermission_angle, spawn.angles );
|
||
|
|
||
|
return &spawn;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===============
|
||
|
G_InitBodyQueue
|
||
|
===============
|
||
|
*/
|
||
|
void G_InitBodyQueue (void)
|
||
|
{
|
||
|
gentity_t *ent;
|
||
|
int max;
|
||
|
|
||
|
if ( level.gametypeData->respawnType == RT_NONE )
|
||
|
{
|
||
|
level.bodySinkTime = 0;
|
||
|
max = BODY_QUEUE_SIZE_MAX;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
level.bodySinkTime = BODY_SINK_DELAY;
|
||
|
max = BODY_QUEUE_SIZE;
|
||
|
}
|
||
|
|
||
|
level.bodyQueIndex = 0;
|
||
|
for ( level.bodyQueSize = 0;
|
||
|
level.bodyQueSize < max && level.bodyQueSize < level.maxclients;
|
||
|
level.bodyQueSize++)
|
||
|
{
|
||
|
ent = G_Spawn();
|
||
|
ent->classname = "bodyque";
|
||
|
ent->neverFree = qtrue;
|
||
|
level.bodyQue[level.bodyQueSize] = ent;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
=============
|
||
|
BodySink
|
||
|
|
||
|
After sitting around for five seconds, fall into the ground and dissapear
|
||
|
=============
|
||
|
*/
|
||
|
void BodySink( gentity_t *ent )
|
||
|
{
|
||
|
if ( level.time - ent->timestamp > level.bodySinkTime + BODY_SINK_TIME )
|
||
|
{
|
||
|
// the body ques are never actually freed, they are just unlinked
|
||
|
trap_UnlinkEntity( ent );
|
||
|
ent->physicsObject = qfalse;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ent->s.eFlags |= EF_NOSHADOW;
|
||
|
|
||
|
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, int hitLocation, vec3_t direction )
|
||
|
{
|
||
|
gentity_t *body;
|
||
|
int contents;
|
||
|
int parm;
|
||
|
|
||
|
trap_UnlinkEntity (ent);
|
||
|
|
||
|
// if client is in a nodrop area, don't leave the body
|
||
|
contents = trap_PointContents( ent->r.currentOrigin, -1 );
|
||
|
if ( contents & CONTENTS_NODROP )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// grab a body que and cycle to the next one
|
||
|
body = level.bodyQue[ level.bodyQueIndex ];
|
||
|
level.bodyQueIndex = (level.bodyQueIndex + 1) % level.bodyQueSize;
|
||
|
|
||
|
trap_UnlinkEntity (body);
|
||
|
|
||
|
body->s = ent->s;
|
||
|
body->s.eType = ET_BODY;
|
||
|
body->s.eFlags = EF_DEAD;
|
||
|
body->s.gametypeitems = 0;
|
||
|
body->s.loopSound = 0;
|
||
|
body->s.number = body - g_entities;
|
||
|
body->timestamp = level.time;
|
||
|
body->physicsObject = qtrue;
|
||
|
body->physicsBounce = 0;
|
||
|
body->s.otherEntityNum = ent->s.clientNum;
|
||
|
|
||
|
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.event = 0;
|
||
|
|
||
|
parm = (DirToByte( direction )&0xFF);
|
||
|
parm += (hitLocation<<8);
|
||
|
G_AddEvent(body, EV_BODY_QUEUE_COPY, parm);
|
||
|
|
||
|
body->r.svFlags = ent->r.svFlags | SVF_BROADCAST;
|
||
|
VectorCopy (ent->r.mins, body->r.mins);
|
||
|
VectorCopy (ent->r.maxs, body->r.maxs);
|
||
|
VectorCopy (ent->r.absmin, body->r.absmin);
|
||
|
VectorCopy (ent->r.absmax, body->r.absmax);
|
||
|
|
||
|
body->s.torsoAnim = body->s.legsAnim = ent->client->ps.legsAnim & ~ANIM_TOGGLEBIT;
|
||
|
|
||
|
body->clipmask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP;
|
||
|
body->r.contents = 0; // CONTENTS_CORPSE;
|
||
|
body->r.ownerNum = ent->s.number;
|
||
|
|
||
|
if ( level.bodySinkTime )
|
||
|
{
|
||
|
body->nextthink = level.time + level.bodySinkTime;
|
||
|
body->think = BodySink;
|
||
|
body->s.time2 = 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Store the time the body was spawned so the client can make them
|
||
|
// dissapear if need be.
|
||
|
body->s.time2 = level.time;
|
||
|
}
|
||
|
|
||
|
body->die = body_die;
|
||
|
body->takedamage = qtrue;
|
||
|
|
||
|
body->s.apos.trBase[PITCH] = 0;
|
||
|
|
||
|
body->s.pos.trBase[2] = ent->client->ps.origin[2];
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_SetRespawnTimer
|
||
|
================
|
||
|
*/
|
||
|
void G_SetRespawnTimer ( gentity_t* ent )
|
||
|
{
|
||
|
// Start a new respawn interval if the old one has passed
|
||
|
if ( level.time > level.gametypeRespawnTime[ent->client->sess.team] )
|
||
|
{
|
||
|
level.gametypeRespawnTime[ent->client->sess.team] = level.time + g_respawnInterval.integer * 1000;
|
||
|
}
|
||
|
|
||
|
// start the interval if its not already started
|
||
|
ent->client->ps.respawnTimer = level.gametypeRespawnTime[ent->client->sess.team] + 1000;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
respawn
|
||
|
================
|
||
|
*/
|
||
|
void respawn( gentity_t *ent )
|
||
|
{
|
||
|
gentity_t *tent;
|
||
|
qboolean ghost = qfalse;
|
||
|
|
||
|
// No respawning when intermission is queued
|
||
|
if ( level.intermissionQueued )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// When we get here the user has just accepted their fate and now
|
||
|
// needs to wait for the ability to respawn
|
||
|
switch ( level.gametypeData->respawnType )
|
||
|
{
|
||
|
case RT_INTERVAL:
|
||
|
G_SetRespawnTimer ( ent );
|
||
|
ghost = qtrue;
|
||
|
break;
|
||
|
|
||
|
case RT_NONE:
|
||
|
|
||
|
// Turn into a ghost
|
||
|
ghost = qtrue;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// If they are a ghost then give a health point, but dont respawn
|
||
|
if ( ghost )
|
||
|
{
|
||
|
G_StartGhosting ( ent );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
trap_UnlinkEntity (ent);
|
||
|
ClientSpawn(ent);
|
||
|
|
||
|
// Add a teleportation effect.
|
||
|
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN );
|
||
|
tent->s.clientNum = ent->s.clientNum;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_GhostCount
|
||
|
|
||
|
Returns number of ghosts on a team, if -1 is given for a team all ghosts in the game
|
||
|
are returned instead
|
||
|
================
|
||
|
*/
|
||
|
int G_GhostCount ( team_t team )
|
||
|
{
|
||
|
int i;
|
||
|
int count;
|
||
|
|
||
|
for ( i = 0, count = 0; i < level.numConnectedClients; i ++ )
|
||
|
{
|
||
|
if (g_entities[level.sortedClients[i]].client->pers.connected != CON_CONNECTED )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( g_entities[level.sortedClients[i]].client->sess.ghost )
|
||
|
{
|
||
|
if ( team != -1 && team != g_entities[level.sortedClients[i]].client->sess.ghost )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
count ++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_IsClientDead
|
||
|
|
||
|
Returns qtrue if the client is dead and qfalse if not
|
||
|
================
|
||
|
*/
|
||
|
qboolean G_IsClientDead ( gclient_t* client )
|
||
|
{
|
||
|
if ( client->ps.stats[STAT_HEALTH] <= 0 )
|
||
|
{
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
if ( client->ps.pm_type == PM_DEAD )
|
||
|
{
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
if ( client->sess.ghost )
|
||
|
{
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
G_IsClientSpectating
|
||
|
|
||
|
Returns qtrue if the client is spectating and qfalse if not
|
||
|
================
|
||
|
*/
|
||
|
qboolean G_IsClientSpectating ( gclient_t* client )
|
||
|
{
|
||
|
if ( client->pers.connected != CON_CONNECTED )
|
||
|
{
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
if ( client->sess.team == TEAM_SPECTATOR )
|
||
|
{
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
if ( client->sess.ghost )
|
||
|
{
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
TeamCount
|
||
|
|
||
|
Returns number of players on a team
|
||
|
================
|
||
|
*/
|
||
|
int TeamCount( int ignoreClientNum, team_t team, int *alive )
|
||
|
{
|
||
|
int i;
|
||
|
int count = 0;
|
||
|
|
||
|
if ( alive )
|
||
|
{
|
||
|
*alive = 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.team == team )
|
||
|
{
|
||
|
if ( !level.clients[i].sess.ghost && alive )
|
||
|
{
|
||
|
(*alive)++;
|
||
|
}
|
||
|
|
||
|
count++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
PickTeam
|
||
|
================
|
||
|
*/
|
||
|
team_t PickTeam( int ignoreClientNum )
|
||
|
{
|
||
|
int counts[TEAM_NUM_TEAMS];
|
||
|
|
||
|
counts[TEAM_BLUE] = TeamCount( ignoreClientNum, TEAM_BLUE, NULL );
|
||
|
counts[TEAM_RED] = TeamCount( ignoreClientNum, TEAM_RED, NULL );
|
||
|
|
||
|
if ( counts[TEAM_BLUE] > counts[TEAM_RED] )
|
||
|
{
|
||
|
return TEAM_RED;
|
||
|
}
|
||
|
|
||
|
if ( counts[TEAM_RED] > counts[TEAM_BLUE] )
|
||
|
{
|
||
|
return TEAM_BLUE;
|
||
|
}
|
||
|
|
||
|
// equal team count, so join the team with the lowest score
|
||
|
if ( level.teamScores[TEAM_BLUE] > level.teamScores[TEAM_RED] )
|
||
|
{
|
||
|
return TEAM_RED;
|
||
|
}
|
||
|
|
||
|
return TEAM_BLUE;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
G_ClientCleanName
|
||
|
============
|
||
|
*/
|
||
|
void G_ClientCleanName ( const char *in, char *out, int outSize, qboolean colors )
|
||
|
{
|
||
|
int len;
|
||
|
int 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( !colors || 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;
|
||
|
|
||
|
// Trim whitespace off the end of the name
|
||
|
for ( out --; out >= p && (*out == ' ' || *out == '\t'); out -- )
|
||
|
{
|
||
|
*out = 0;
|
||
|
}
|
||
|
|
||
|
// don't allow empty names
|
||
|
if( *p == 0 || colorlessLen == 0 )
|
||
|
{
|
||
|
Q_strncpyz( p, "UnnamedPlayer", outSize );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
Updates the clients current outfittin
|
||
|
===========
|
||
|
*/
|
||
|
void G_UpdateOutfitting ( int clientNum )
|
||
|
{
|
||
|
gentity_t *ent;
|
||
|
gclient_t *client;
|
||
|
int group;
|
||
|
int ammoIndex;
|
||
|
int idle;
|
||
|
|
||
|
int equipWeapon;
|
||
|
int equipWeaponGroup;
|
||
|
|
||
|
ent = g_entities + clientNum;
|
||
|
client = ent->client;
|
||
|
|
||
|
// No can do if
|
||
|
if ( client->noOutfittingChange )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Clear all ammo, clips, and weapons
|
||
|
client->ps.stats[STAT_WEAPONS] = 0;
|
||
|
memset ( client->ps.ammo, 0, sizeof(client->ps.ammo) );
|
||
|
memset ( client->ps.clip, 0, sizeof(client->ps.clip) );
|
||
|
|
||
|
// Everyone gets some knives
|
||
|
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_KNIFE );
|
||
|
ammoIndex=weaponData[WP_KNIFE].attack[ATTACK_NORMAL].ammoIndex;
|
||
|
client->ps.clip[ATTACK_NORMAL][WP_KNIFE]=weaponData[WP_KNIFE].attack[ATTACK_NORMAL].clipSize;
|
||
|
client->ps.firemode[WP_KNIFE] = BG_FindFireMode ( WP_KNIFE, ATTACK_NORMAL, WP_FIREMODE_AUTO );
|
||
|
|
||
|
if ( BG_IsWeaponAvailableForOutfitting ( WP_KNIFE, 2 ) )
|
||
|
{
|
||
|
client->ps.ammo[ammoIndex]=ammoData[ammoIndex].max;
|
||
|
}
|
||
|
|
||
|
equipWeapon = WP_KNIFE;
|
||
|
equipWeaponGroup = OUTFITTING_GROUP_KNIFE;
|
||
|
|
||
|
// Give all the outfitting groups to the player
|
||
|
for ( group = 0; group < OUTFITTING_GROUP_ACCESSORY; group ++ )
|
||
|
{
|
||
|
gitem_t* item;
|
||
|
int ammoIndex;
|
||
|
|
||
|
// Nothing available in this group
|
||
|
if ( client->pers.outfitting.items[group] == -1 )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Grab the item that represents the weapon
|
||
|
item = &bg_itemlist[bg_outfittingGroups[group][client->pers.outfitting.items[group]]];
|
||
|
|
||
|
client->ps.stats[STAT_WEAPONS] |= (1 << item->giTag);
|
||
|
ammoIndex = weaponData[item->giTag].attack[ATTACK_NORMAL].ammoIndex;
|
||
|
client->ps.ammo[ammoIndex] += weaponData[item->giTag].attack[ATTACK_NORMAL].extraClips * weaponData[item->giTag].attack[ATTACK_NORMAL].clipSize;
|
||
|
client->ps.clip[ATTACK_NORMAL][item->giTag] = weaponData[item->giTag].attack[ATTACK_NORMAL].clipSize;
|
||
|
|
||
|
// Lower group numbers are bigger guns
|
||
|
if ( group < equipWeaponGroup )
|
||
|
{
|
||
|
equipWeaponGroup = group;
|
||
|
equipWeapon = item->giTag;
|
||
|
}
|
||
|
|
||
|
// alt-fire ammo
|
||
|
ammoIndex = weaponData[item->giTag].attack[ATTACK_ALTERNATE].ammoIndex;
|
||
|
if ( weaponData[item->giTag].attack[ATTACK_ALTERNATE].fireAmount && AMMO_NONE != ammoIndex )
|
||
|
{
|
||
|
client->ps.clip[ATTACK_ALTERNATE][item->giTag] = weaponData[item->giTag].attack[ATTACK_ALTERNATE].clipSize;
|
||
|
client->ps.ammo[ammoIndex] += weaponData[item->giTag].attack[ATTACK_ALTERNATE].extraClips * weaponData[item->giTag].attack[ATTACK_ALTERNATE].clipSize;
|
||
|
}
|
||
|
|
||
|
// Set the default firemode for this weapon
|
||
|
if ( client->ps.firemode[item->giTag] == WP_FIREMODE_NONE )
|
||
|
{
|
||
|
client->ps.firemode[item->giTag] = BG_FindFireMode ( item->giTag, ATTACK_NORMAL, WP_FIREMODE_AUTO );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
client->ps.weapon = equipWeapon;
|
||
|
client->ps.weaponstate = WEAPON_READY; //WEAPON_SPAWNING;
|
||
|
client->ps.weaponTime = 0;
|
||
|
client->ps.weaponAnimTime = 0;
|
||
|
|
||
|
// Bot clients cant use the spawning state
|
||
|
#ifdef _SOF2_BOTS
|
||
|
if ( ent->r.svFlags & SVF_BOT )
|
||
|
{
|
||
|
client->ps.weaponstate = WEAPON_READY;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
// Default to auto (or next available fire mode).
|
||
|
BG_GetInviewAnim(client->ps.weapon,"idle",&idle);
|
||
|
client->ps.weaponAnimId = idle;
|
||
|
client->ps.weaponAnimIdChoice = 0;
|
||
|
client->ps.weaponCallbackStep = 0;
|
||
|
|
||
|
// Armor?
|
||
|
client->ps.stats[STAT_ARMOR] = 0;
|
||
|
client->ps.stats[STAT_GOGGLES] = GOGGLES_NONE;
|
||
|
switch ( bg_outfittingGroups[OUTFITTING_GROUP_ACCESSORY][client->pers.outfitting.items[OUTFITTING_GROUP_ACCESSORY]] )
|
||
|
{
|
||
|
default:
|
||
|
case MODELINDEX_ARMOR:
|
||
|
client->ps.stats[STAT_ARMOR] = MAX_HEALTH;
|
||
|
break;
|
||
|
|
||
|
case MODELINDEX_THERMAL:
|
||
|
client->ps.stats[STAT_GOGGLES] = GOGGLES_INFRARED;
|
||
|
break;
|
||
|
|
||
|
case MODELINDEX_NIGHTVISION:
|
||
|
client->ps.stats[STAT_GOGGLES] = GOGGLES_NIGHTVISION;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Stuff which grenade is being used into stats for later use by
|
||
|
// the backpack code
|
||
|
client->ps.stats[STAT_OUTFIT_GRENADE] = bg_itemlist[bg_outfittingGroups[OUTFITTING_GROUP_GRENADE][client->pers.outfitting.items[OUTFITTING_GROUP_GRENADE]]].giTag;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
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 team;
|
||
|
int health;
|
||
|
char *s;
|
||
|
gclient_t *client;
|
||
|
char oldname[MAX_STRING_CHARS];
|
||
|
char userinfo[MAX_INFO_STRING];
|
||
|
TIdentity *oldidentity;
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// Is anti-lag turned on?
|
||
|
s = Info_ValueForKey ( userinfo, "cg_antiLag" );
|
||
|
client->pers.antiLag = atoi( s )?qtrue:qfalse;
|
||
|
|
||
|
// Is auto-reload turned on?
|
||
|
s = Info_ValueForKey ( userinfo, "cg_autoReload" );
|
||
|
client->pers.autoReload = atoi( s )?qtrue:qfalse;
|
||
|
if ( client->pers.autoReload )
|
||
|
{
|
||
|
client->ps.pm_flags |= PMF_AUTORELOAD;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
client->ps.pm_flags &= ~PMF_AUTORELOAD;
|
||
|
}
|
||
|
|
||
|
// set name
|
||
|
Q_strncpyz ( oldname, client->pers.netname, sizeof( oldname ) );
|
||
|
s = Info_ValueForKey (userinfo, "name");
|
||
|
G_ClientCleanName( s, client->pers.netname, sizeof(client->pers.netname), level.gametypeData->teams?qfalse:qtrue );
|
||
|
|
||
|
if ( client->sess.team == TEAM_SPECTATOR )
|
||
|
{
|
||
|
if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD )
|
||
|
{
|
||
|
Q_strncpyz( client->pers.netname, "scoreboard", sizeof(client->pers.netname) );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// set max health
|
||
|
health = atoi( Info_ValueForKey( userinfo, "handicap" ) );
|
||
|
|
||
|
// bots set their team a few frames later
|
||
|
if ( level.gametypeData->teams && (g_entities[clientNum].r.svFlags & SVF_BOT))
|
||
|
{
|
||
|
s = Info_ValueForKey( userinfo, "team" );
|
||
|
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 );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
team = client->sess.team;
|
||
|
}
|
||
|
|
||
|
// Enforce the identities
|
||
|
oldidentity = client->pers.identity;
|
||
|
|
||
|
if( level.gametypeData->teams )
|
||
|
{
|
||
|
s = Info_ValueForKey ( userinfo, "team_identity" );
|
||
|
|
||
|
// Lookup the identity by name and if it cant be found then pick a random one
|
||
|
client->pers.identity = BG_FindIdentity ( s );
|
||
|
|
||
|
if ( team != TEAM_SPECTATOR )
|
||
|
{
|
||
|
// No identity or a team mismatch means they dont get to be that skin
|
||
|
if ( !client->pers.identity || Q_stricmp ( level.gametypeTeam[team], client->pers.identity->mTeam ) )
|
||
|
{
|
||
|
// Get first matching team identity
|
||
|
client->pers.identity = BG_FindTeamIdentity ( level.gametypeTeam[team], -1 );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Spectators are going to have to choose one of the two team skins and
|
||
|
// the chance of them having the proper one in team_identity is slim, so just
|
||
|
// give them a model they may use later
|
||
|
client->pers.identity = BG_FindTeamIdentity ( level.gametypeTeam[TEAM_RED], 0 );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
s = Info_ValueForKey ( userinfo, "identity" );
|
||
|
|
||
|
// Lookup the identity by name and if it cant be found then pick a random one
|
||
|
client->pers.identity = BG_FindIdentity ( s );
|
||
|
}
|
||
|
|
||
|
// If the identity wasnt in the list then just give them the first identity. We could
|
||
|
// be fancy here and give them a random one, but this way you get less unwanted models
|
||
|
// loaded
|
||
|
if ( !client->pers.identity )
|
||
|
{
|
||
|
client->pers.identity = &bg_identities[0];
|
||
|
}
|
||
|
|
||
|
// Report the identity change
|
||
|
if ( client->pers.connected == CON_CONNECTED )
|
||
|
{
|
||
|
if ( client->pers.identity && oldidentity && client->pers.identity != oldidentity && team != TEAM_SPECTATOR )
|
||
|
{
|
||
|
trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " has changed identities\n\"", client->pers.netname ) );
|
||
|
}
|
||
|
|
||
|
// If the client is changing their name then handle some delayed name changes
|
||
|
if ( strcmp( oldname, client->pers.netname ) )
|
||
|
{
|
||
|
// Dont let them change their name too much
|
||
|
if ( level.time - client->pers.netnameTime < 5000 )
|
||
|
{
|
||
|
trap_SendServerCommand ( client - &level.clients[0], "print \"You must wait 5 seconds before changing your name again.\n\"" );
|
||
|
strcpy ( client->pers.netname, oldname );
|
||
|
}
|
||
|
// voting clients cannot change their names
|
||
|
else if ( (level.voteTime || level.voteExecuteTime) && strstr ( level.voteDisplayString, oldname ) )
|
||
|
{
|
||
|
trap_SendServerCommand ( client - &level.clients[0], "print \"You are not allowed to change your name while there is an active vote against you.\n\"" );
|
||
|
strcpy ( client->pers.netname, oldname );
|
||
|
}
|
||
|
// If they are a ghost or spectating in an inf game their name is deferred
|
||
|
else if ( level.gametypeData->respawnType == RT_NONE && (client->sess.ghost || G_IsClientDead ( client ) ) )
|
||
|
{
|
||
|
trap_SendServerCommand ( client - &level.clients[0], "print \"Name changes while dead will be deferred until you spawn again.\n\"" );
|
||
|
strcpy ( client->pers.deferredname, client->pers.netname );
|
||
|
strcpy ( client->pers.netname, oldname );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
trap_SendServerCommand( -1, va("print \"%s renamed to %s\n\"", oldname, client->pers.netname) );
|
||
|
client->pers.netnameTime = level.time;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Outfitting if pickups are disabled
|
||
|
if ( level.pickupsDisabled )
|
||
|
{
|
||
|
// Parse out the new outfitting
|
||
|
BG_DecompressOutfitting ( Info_ValueForKey ( userinfo, "outfitting" ), &client->pers.outfitting );
|
||
|
G_UpdateOutfitting ( clientNum );
|
||
|
}
|
||
|
|
||
|
// 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\\identity\\%s\\skill\\%s",
|
||
|
client->pers.netname, team, client->pers.identity->mName,
|
||
|
Info_ValueForKey( userinfo, "skill" ) );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
s = va("n\\%s\\t\\%i\\identity\\%s",
|
||
|
client->pers.netname, team, client->pers.identity->mName );
|
||
|
}
|
||
|
|
||
|
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 map 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 map
|
||
|
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.";
|
||
|
}
|
||
|
|
||
|
// we don't check password for bots and local client
|
||
|
// NOTE: local client <-> "ip" "localhost"
|
||
|
// this means this client is not running in our current process
|
||
|
if ( !( isBot ) && (strcmp(value, "localhost") != 0))
|
||
|
{
|
||
|
// 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 va("Invalid password: %s", value );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// they can connect
|
||
|
ent->client = level.clients + clientNum;
|
||
|
client = ent->client;
|
||
|
|
||
|
memset( client, 0, sizeof(*client) );
|
||
|
|
||
|
client->pers.connected = CON_CONNECTING;
|
||
|
|
||
|
client->sess.team = TEAM_SPECTATOR;
|
||
|
|
||
|
// read or initialize the session data
|
||
|
if ( firstTime || level.newSession )
|
||
|
{
|
||
|
G_InitSessionData( client, userinfo );
|
||
|
}
|
||
|
|
||
|
G_ReadSessionData( client );
|
||
|
|
||
|
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 " is connecting...\n\"", client->pers.netname) );
|
||
|
}
|
||
|
|
||
|
// Broadcast team change if not going to spectator
|
||
|
if ( level.gametypeData->teams && client->sess.team != TEAM_SPECTATOR )
|
||
|
{
|
||
|
BroadcastTeamChange( client, -1 );
|
||
|
}
|
||
|
|
||
|
// count current clients and rank for scoreboard
|
||
|
CalculateRanks();
|
||
|
|
||
|
// Make sure they are unlinked
|
||
|
trap_UnlinkEntity ( ent );
|
||
|
|
||
|
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;
|
||
|
int spawncount;
|
||
|
|
||
|
ent = g_entities + clientNum;
|
||
|
|
||
|
client = level.clients + clientNum;
|
||
|
|
||
|
if ( ent->r.linked )
|
||
|
{
|
||
|
trap_UnlinkEntity( ent );
|
||
|
}
|
||
|
|
||
|
// Run a gametype check just in case the game hasnt started yet
|
||
|
if ( !level.gametypeStartTime )
|
||
|
{
|
||
|
CheckGametype ( );
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
spawncount = client->ps.persistant[PERS_SPAWN_COUNT];
|
||
|
memset( &client->ps, 0, sizeof( client->ps ) );
|
||
|
client->ps.eFlags = flags;
|
||
|
client->ps.persistant[PERS_SPAWN_COUNT] = spawncount;
|
||
|
|
||
|
// locate ent at a spawn point
|
||
|
ClientSpawn( ent );
|
||
|
|
||
|
if ( client->sess.team != TEAM_SPECTATOR )
|
||
|
{
|
||
|
// send event
|
||
|
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN );
|
||
|
tent->s.clientNum = ent->s.clientNum;
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
G_SelectClientSpawnPoint
|
||
|
|
||
|
Selects a spawn point for the given client entity
|
||
|
============
|
||
|
*/
|
||
|
gspawn_t* G_SelectClientSpawnPoint ( gentity_t* ent )
|
||
|
{
|
||
|
gclient_t* client = ent->client;
|
||
|
gspawn_t* spawnPoint;
|
||
|
|
||
|
// find a spawn point
|
||
|
// do it before setting health back up, so farthest
|
||
|
// ranging doesn't count this client
|
||
|
if ( client->sess.team == TEAM_SPECTATOR )
|
||
|
{
|
||
|
spawnPoint = G_SelectSpectatorSpawnPoint ( );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if ( level.gametypeData->teams && level.gametypeData->respawnType != RT_NORMAL )
|
||
|
{
|
||
|
// Dont bother selecting a safe spawn on non-respawn games, the map creator should
|
||
|
// have done this for us.
|
||
|
if ( level.gametypeData->respawnType == RT_NONE )
|
||
|
{
|
||
|
spawnPoint = G_SelectRandomSpawnPoint ( ent->client->sess.team );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
spawnPoint = G_SelectRandomSafeSpawnPoint ( ent->client->sess.team, 1500 );
|
||
|
}
|
||
|
|
||
|
if ( !spawnPoint )
|
||
|
{
|
||
|
// don't spawn near other players if possible
|
||
|
spawnPoint = G_SelectRandomSpawnPoint ( ent->client->sess.team );
|
||
|
}
|
||
|
|
||
|
// Spawn at any deathmatch spawn, telefrag if needed
|
||
|
if ( !spawnPoint )
|
||
|
{
|
||
|
spawnPoint = G_SelectRandomSpawnPoint ( TEAM_FREE );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Try deathmatch spawns first
|
||
|
spawnPoint = G_SelectRandomSafeSpawnPoint ( TEAM_FREE, 1500 );
|
||
|
|
||
|
// If none found use any spawn
|
||
|
if ( !spawnPoint )
|
||
|
{
|
||
|
spawnPoint = G_SelectRandomSafeSpawnPoint ( -1, 1500 );
|
||
|
}
|
||
|
|
||
|
// Spawn at any deathmatch spawn, telefrag if needed
|
||
|
if ( !spawnPoint )
|
||
|
{
|
||
|
spawnPoint = G_SelectRandomSpawnPoint ( TEAM_FREE );
|
||
|
}
|
||
|
|
||
|
// Spawn at any gametype spawn, telefrag if needed
|
||
|
if ( !spawnPoint )
|
||
|
{
|
||
|
spawnPoint = G_SelectRandomSpawnPoint ( -1 );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return spawnPoint;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
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;
|
||
|
vec3_t spawn_angles;
|
||
|
gclient_t *client;
|
||
|
int i;
|
||
|
clientPersistant_t saved;
|
||
|
clientSession_t savedSess;
|
||
|
int persistant[MAX_PERSISTANT];
|
||
|
gspawn_t *spawnPoint;
|
||
|
int flags;
|
||
|
int savedPing;
|
||
|
int eventSequence;
|
||
|
char userinfo[MAX_INFO_STRING];
|
||
|
int start_ammo_type;
|
||
|
int ammoIndex;
|
||
|
int idle;
|
||
|
|
||
|
index = ent - g_entities;
|
||
|
client = ent->client;
|
||
|
|
||
|
// Where do we spawn?
|
||
|
spawnPoint = G_SelectClientSpawnPoint ( ent );
|
||
|
if ( spawnPoint )
|
||
|
{
|
||
|
VectorCopy ( spawnPoint->angles, spawn_angles );
|
||
|
VectorCopy ( spawnPoint->origin, spawn_origin );
|
||
|
spawn_origin[2] += 9;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
SetTeam ( ent, "s", NULL );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
client->pers.teamState.state = TEAM_ACTIVE;
|
||
|
|
||
|
// toggle the teleport bit so the client knows to not lerp
|
||
|
// and never clear the voted flag
|
||
|
flags = ent->client->ps.eFlags & (EF_TELEPORT_BIT | EF_VOTED);
|
||
|
flags ^= EF_TELEPORT_BIT;
|
||
|
|
||
|
// clear everything but the persistant data
|
||
|
saved = client->pers;
|
||
|
savedSess = client->sess;
|
||
|
savedPing = client->ps.ping;
|
||
|
for ( i = 0 ; i < MAX_PERSISTANT ; i++ )
|
||
|
{
|
||
|
persistant[i] = client->ps.persistant[i];
|
||
|
}
|
||
|
eventSequence = client->ps.eventSequence;
|
||
|
|
||
|
memset (client, 0, sizeof(*client));
|
||
|
|
||
|
client->pers = saved;
|
||
|
client->sess = savedSess;
|
||
|
client->ps.ping = savedPing;
|
||
|
client->lastkilled_client = -1;
|
||
|
|
||
|
for ( i = 0 ; i < MAX_PERSISTANT ; i++ )
|
||
|
{
|
||
|
client->ps.persistant[i] = persistant[i];
|
||
|
}
|
||
|
client->ps.eventSequence = eventSequence;
|
||
|
|
||
|
// increment the spawncount so the client will detect the respawn
|
||
|
client->ps.persistant[PERS_SPAWN_COUNT]++;
|
||
|
client->ps.persistant[PERS_TEAM] = client->sess.team;
|
||
|
client->ps.persistant[PERS_ATTACKER] = ENTITYNUM_WORLD;
|
||
|
|
||
|
client->airOutTime = level.time + 12000;
|
||
|
|
||
|
trap_GetUserinfo( index, userinfo, sizeof(userinfo) );
|
||
|
|
||
|
// clear entity values
|
||
|
client->ps.eFlags = flags;
|
||
|
|
||
|
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;
|
||
|
|
||
|
VectorCopy (playerMins, ent->r.mins);
|
||
|
VectorCopy (playerMaxs, ent->r.maxs);
|
||
|
|
||
|
client->ps.clientNum = index;
|
||
|
|
||
|
// Bring back the saved firemodes
|
||
|
memcpy ( client->ps.firemode, client->pers.firemode, sizeof(client->ps.firemode) );
|
||
|
|
||
|
//give default weapons
|
||
|
client->ps.stats[STAT_WEAPONS] = ( 1 << WP_NONE );
|
||
|
|
||
|
client->noOutfittingChange = qfalse;
|
||
|
|
||
|
// Give the client their weapons depending on whether or not pickups are enabled
|
||
|
if ( level.pickupsDisabled )
|
||
|
{
|
||
|
G_UpdateOutfitting ( ent->s.number );
|
||
|
|
||
|
// Prevent the client from picking up a whole bunch of stuff
|
||
|
client->ps.pm_flags |= PMF_LIMITED_INVENTORY;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Knife.
|
||
|
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_KNIFE );
|
||
|
ammoIndex=weaponData[WP_KNIFE].attack[ATTACK_NORMAL].ammoIndex;
|
||
|
client->ps.ammo[ammoIndex]=ammoData[ammoIndex].max;
|
||
|
client->ps.clip[ATTACK_NORMAL][WP_KNIFE]=weaponData[WP_KNIFE].attack[ATTACK_NORMAL].clipSize;
|
||
|
client->ps.firemode[WP_KNIFE] = BG_FindFireMode ( WP_KNIFE, ATTACK_NORMAL, WP_FIREMODE_AUTO );
|
||
|
|
||
|
// Set up some weapons and ammo for the player.
|
||
|
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_USSOCOM_PISTOL );
|
||
|
start_ammo_type = weaponData[WP_USSOCOM_PISTOL].attack[ATTACK_NORMAL].ammoIndex;
|
||
|
client->ps.ammo[start_ammo_type] = weaponData[WP_USSOCOM_PISTOL].attack[ATTACK_NORMAL].clipSize;
|
||
|
client->ps.clip[ATTACK_NORMAL][WP_USSOCOM_PISTOL] = weaponData[WP_USSOCOM_PISTOL].attack[ATTACK_NORMAL].clipSize;
|
||
|
client->ps.firemode[WP_USSOCOM_PISTOL] = BG_FindFireMode ( WP_USSOCOM_PISTOL, ATTACK_NORMAL, WP_FIREMODE_AUTO );
|
||
|
|
||
|
// alt-fire ammo
|
||
|
start_ammo_type = weaponData[WP_USSOCOM_PISTOL].attack[ATTACK_ALTERNATE].ammoIndex;
|
||
|
if (AMMO_NONE != start_ammo_type)
|
||
|
{
|
||
|
client->ps.ammo[start_ammo_type] = ammoData[start_ammo_type].max;
|
||
|
}
|
||
|
|
||
|
// Everyone gets full armor in deathmatch
|
||
|
client->ps.stats[STAT_ARMOR] = MAX_HEALTH;
|
||
|
}
|
||
|
|
||
|
client->ps.stats[STAT_HEALTH] = ent->health = MAX_HEALTH;
|
||
|
|
||
|
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;
|
||
|
if ( client->pers.autoReload )
|
||
|
{
|
||
|
client->ps.pm_flags |= PMF_AUTORELOAD;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
client->ps.pm_flags &= ~PMF_AUTORELOAD;
|
||
|
}
|
||
|
|
||
|
trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd );
|
||
|
SetClientViewAngle( ent, spawn_angles );
|
||
|
|
||
|
if ( ent->client->sess.team != TEAM_SPECTATOR )
|
||
|
{
|
||
|
G_KillBox( ent );
|
||
|
trap_LinkEntity (ent);
|
||
|
|
||
|
// force the base weapon up
|
||
|
if ( !level.pickupsDisabled )
|
||
|
{
|
||
|
client->ps.weapon = WP_USSOCOM_PISTOL;
|
||
|
client->ps.weaponstate = WEAPON_RAISING;
|
||
|
client->ps.weaponTime = 500;
|
||
|
|
||
|
// Default to auto (or next available fire mode).
|
||
|
client->ps.firemode[client->ps.weapon] = BG_FindFireMode ( client->ps.weapon, ATTACK_NORMAL, WP_FIREMODE_AUTO );
|
||
|
BG_GetInviewAnim(client->ps.weapon,"idle",&idle);
|
||
|
client->ps.weaponAnimId = idle;
|
||
|
client->ps.weaponAnimIdChoice = 0;
|
||
|
client->ps.weaponCallbackStep = 0;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
client->ps.weapon = WP_KNIFE;
|
||
|
BG_GetInviewAnim(client->ps.weapon,"idle",&idle);
|
||
|
client->ps.weaponAnimId = idle;
|
||
|
}
|
||
|
|
||
|
// 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->invulnerableTime = level.time;
|
||
|
client->ps.eFlags |= EF_INVULNERABLE;
|
||
|
client->inactivityTime = level.time + g_inactivity.integer * 1000;
|
||
|
client->latched_buttons = 0;
|
||
|
|
||
|
// set default animations
|
||
|
client->ps.weaponstate = WEAPON_READY;
|
||
|
client->ps.torsoAnim = -1;
|
||
|
client->ps.legsAnim = LEGS_IDLE;
|
||
|
|
||
|
// Not on a ladder
|
||
|
client->ps.ladder = -1;
|
||
|
|
||
|
// Not leaning
|
||
|
client->ps.leanTime = LEAN_TIME;
|
||
|
|
||
|
if ( level.intermissiontime )
|
||
|
{
|
||
|
MoveClientToIntermission( ent );
|
||
|
}
|
||
|
|
||
|
// 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 ( ent->client->sess.team != TEAM_SPECTATOR )
|
||
|
{
|
||
|
BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue );
|
||
|
VectorCopy( ent->client->ps.origin, ent->r.currentOrigin );
|
||
|
trap_LinkEntity( ent );
|
||
|
}
|
||
|
|
||
|
// run the presend to set anything else
|
||
|
ClientEndFrame( ent );
|
||
|
|
||
|
// clear entity state values
|
||
|
BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue );
|
||
|
|
||
|
// Frozen?
|
||
|
if ( level.gametypeDelayTime > level.time )
|
||
|
{
|
||
|
ent->client->ps.stats[STAT_FROZEN] = level.gametypeDelayTime - level.time;
|
||
|
}
|
||
|
|
||
|
// Handle a deferred name change
|
||
|
if ( client->pers.deferredname[0] )
|
||
|
{
|
||
|
trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " renamed to %s\n\"", client->pers.netname, client->pers.deferredname) );
|
||
|
strcpy ( client->pers.netname, client->pers.deferredname );
|
||
|
client->pers.deferredname[0] = '\0';
|
||
|
client->pers.netnameTime = level.time;
|
||
|
ClientUserinfoChanged ( client->ps.clientNum );
|
||
|
}
|
||
|
|
||
|
// Update the time when other people can join the game
|
||
|
if ( !level.gametypeJoinTime && level.gametypeData->teams )
|
||
|
{
|
||
|
// As soon as both teams have people on them the counter starts
|
||
|
if ( TeamCount ( -1, TEAM_RED, NULL ) && TeamCount ( -1, TEAM_BLUE, NULL ) )
|
||
|
{
|
||
|
level.gametypeJoinTime = level.time;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
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;
|
||
|
|
||
|
// cleanup if we are kicking a bot that
|
||
|
// hasn't spawned yet
|
||
|
G_RemoveQueuedBotBegin( clientNum );
|
||
|
|
||
|
ent = g_entities + clientNum;
|
||
|
if ( !ent->client )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// stop any following clients
|
||
|
for ( i = 0 ; i < level.maxclients ; i++ )
|
||
|
{
|
||
|
if ( G_IsClientSpectating ( &level.clients[i] ) &&
|
||
|
level.clients[i].sess.spectatorState == SPECTATOR_FOLLOW &&
|
||
|
level.clients[i].sess.spectatorClient == clientNum )
|
||
|
{
|
||
|
G_StopFollowing( &g_entities[i] );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// send effect if they were completely connected
|
||
|
if ( ent->client->pers.connected == CON_CONNECTED &&
|
||
|
!G_IsClientSpectating ( ent->client ) &&
|
||
|
!G_IsClientDead ( ent->client ) )
|
||
|
{
|
||
|
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT );
|
||
|
tent->s.clientNum = ent->s.clientNum;
|
||
|
|
||
|
// Dont drop weapons
|
||
|
ent->client->ps.stats[STAT_WEAPONS] = 0;
|
||
|
|
||
|
// Get rid of things that need to drop
|
||
|
TossClientItems( ent );
|
||
|
}
|
||
|
|
||
|
G_LogPrintf( "ClientDisconnect: %i\n", clientNum );
|
||
|
|
||
|
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.team = TEAM_FREE;
|
||
|
|
||
|
trap_SetConfigstring( CS_PLAYERS + clientNum, "");
|
||
|
|
||
|
CalculateRanks();
|
||
|
|
||
|
#ifdef _SOF2_BOTS
|
||
|
if ( ent->r.svFlags & SVF_BOT )
|
||
|
{
|
||
|
BotAIShutdownClient( clientNum, qfalse );
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
G_UpdateClientAnimations
|
||
|
|
||
|
Updates the animation information for the client
|
||
|
============
|
||
|
*/
|
||
|
void G_UpdateClientAnimations ( gentity_t* ent )
|
||
|
{
|
||
|
gclient_t* client;
|
||
|
|
||
|
client = ent->client;
|
||
|
|
||
|
// Check for anim change in the legs
|
||
|
if ( client->legs.anim != ent->s.legsAnim )
|
||
|
{
|
||
|
client->legs.anim = ent->s.legsAnim;
|
||
|
client->legs.animTime = level.time;
|
||
|
}
|
||
|
|
||
|
// Check for anim change in the torso
|
||
|
if ( client->torso.anim != ent->s.torsoAnim )
|
||
|
{
|
||
|
client->torso.anim = ent->s.torsoAnim;
|
||
|
client->torso.animTime = level.time;
|
||
|
}
|
||
|
|
||
|
// Force the legs and torso to stay aligned for now to ensure the client
|
||
|
// and server are in sync with the angles.
|
||
|
// TODO: Come up with a way to keep these in sync on both client and server
|
||
|
client->torso.yawing = qtrue;
|
||
|
client->torso.pitching = qtrue;
|
||
|
client->legs.yawing = qtrue;
|
||
|
|
||
|
// Calculate the real torso and leg angles
|
||
|
BG_PlayerAngles ( ent->client->ps.viewangles,
|
||
|
NULL,
|
||
|
|
||
|
ent->client->ghoulLegsAngles,
|
||
|
ent->client->ghoulLowerTorsoAngles,
|
||
|
ent->client->ghoulUpperTorsoAngles,
|
||
|
ent->client->ghoulHeadAngles,
|
||
|
|
||
|
(float)(ent->client->ps.leanTime - LEAN_TIME) / LEAN_TIME * LEAN_OFFSET,
|
||
|
|
||
|
0,
|
||
|
0,
|
||
|
level.time,
|
||
|
|
||
|
&client->torso,
|
||
|
&client->legs,
|
||
|
|
||
|
level.time - level.previousTime,
|
||
|
|
||
|
client->ps.velocity,
|
||
|
|
||
|
qfalse,
|
||
|
ent->s.angles2[YAW],
|
||
|
NULL );
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
===========
|
||
|
G_FindNearbyClient
|
||
|
|
||
|
Locates a client thats near the given origin
|
||
|
============
|
||
|
*/
|
||
|
gentity_t* G_FindNearbyClient ( vec3_t origin, team_t team, float radius, gentity_t* ignore )
|
||
|
{
|
||
|
int i;
|
||
|
|
||
|
for ( i = 0; i < level.numConnectedClients; i ++ )
|
||
|
{
|
||
|
gentity_t* other = &g_entities[ level.sortedClients[i] ];
|
||
|
vec3_t diff;
|
||
|
|
||
|
if ( other->client->pers.connected != CON_CONNECTED )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( other == ignore )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( G_IsClientSpectating ( other->client ) || G_IsClientDead ( other->client ) )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( other->client->sess.team != team )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// See if this client is close enough to yell sniper
|
||
|
VectorSubtract ( other->r.currentOrigin, origin, diff );
|
||
|
if ( VectorLengthSquared ( diff ) < radius * radius )
|
||
|
{
|
||
|
return other;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|