1397 lines
36 KiB
C
1397 lines
36 KiB
C
// Copyright (C) 1999-2000 Id Software, Inc.
|
|
//
|
|
#include "g_local.h"
|
|
#include "g_groups.h"
|
|
|
|
extern char* BG_RegisterRace( const char *name );
|
|
|
|
// 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 ) {
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
=======================================================================
|
|
|
|
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 ) {
|
|
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 ];
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
SelectSpawnPoint
|
|
|
|
Chooses a player start, deathmatch start, etc
|
|
============
|
|
*/
|
|
gentity_t *SelectSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t 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;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
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;
|
|
}
|
|
|
|
/*
|
|
=======================================================================
|
|
|
|
BODYQUE
|
|
|
|
=======================================================================
|
|
*/
|
|
|
|
static int bodyFadeSound=0;
|
|
|
|
|
|
/*
|
|
===============
|
|
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;
|
|
}
|
|
|
|
if (bodyFadeSound == 0)
|
|
{ // Initialize this sound.
|
|
bodyFadeSound = G_SoundIndex("sound/enemies/borg/walkthroughfield.wav");
|
|
}
|
|
}
|
|
|
|
/*
|
|
=============
|
|
BodyRezOut
|
|
|
|
After sitting around for five seconds, fade out.
|
|
=============
|
|
*/
|
|
void BodyRezOut( gentity_t *ent )
|
|
{
|
|
if ( level.time - ent->timestamp >= 7500 ) {
|
|
// the body ques are never actually freed, they are just unlinked
|
|
trap_UnlinkEntity( ent );
|
|
ent->physicsObject = qfalse;
|
|
return;
|
|
}
|
|
|
|
ent->nextthink = level.time + 2500;
|
|
ent->s.time = level.time + 2500;
|
|
|
|
G_AddEvent(ent, EV_GENERAL_SOUND, bodyFadeSound);
|
|
}
|
|
|
|
/*
|
|
=============
|
|
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;
|
|
|
|
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 ) {
|
|
// ent->s.eFlags &= ~EF_NODRAW; // Just in case we died from a bottomless pit, reset EF_NODRAW
|
|
return;
|
|
}
|
|
|
|
// grab a body que and cycle to the next one
|
|
body = level.bodyQue[ level.bodyQueIndex ];
|
|
level.bodyQueIndex = (level.bodyQueIndex + 1) % BODY_QUEUE_SIZE;
|
|
|
|
trap_UnlinkEntity (body);
|
|
|
|
body->s = ent->s;
|
|
body->s.eFlags = EF_DEAD; // clear EF_TALK, etc
|
|
body->s.powerups = 0; // clear powerups
|
|
body->s.loopSound = 0; // clear lava burning
|
|
body->s.number = body - g_entities;
|
|
body->timestamp = level.time;
|
|
body->physicsObject = qtrue;
|
|
body->physicsBounce = 0; // don't bounce
|
|
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;
|
|
|
|
// change the animation to the last-frame only, so the sequence
|
|
// doesn't repeat anew for the body
|
|
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->r.svFlags = ent->r.svFlags;
|
|
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->clipmask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP;
|
|
body->r.contents = CONTENTS_CORPSE;
|
|
body->r.ownerNum = ent->r.ownerNum;
|
|
|
|
body->nextthink = level.time + 5000;
|
|
body->think = BodyRezOut;
|
|
|
|
body->die = body_die;
|
|
|
|
// if there shouldn't be a body, don't show one.
|
|
if (ent->client &&
|
|
((level.time - ent->client->ps.powerups[PW_DISINTEGRATE]) < 10000 ||
|
|
(level.time - ent->client->ps.powerups[PW_EXPLODE]) < 10000))
|
|
{
|
|
body->s.eFlags |= EF_NODRAW;
|
|
}
|
|
|
|
// don't take more damage if already gibbed
|
|
if ( ent->health <= GIB_HEALTH ) {
|
|
body->takedamage = qfalse;
|
|
} else {
|
|
body->takedamage = qtrue;
|
|
}
|
|
|
|
|
|
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);
|
|
ClientSpawn(ent);
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
PickTeam
|
|
|
|
================
|
|
*/
|
|
team_t PickTeam( int ignoreClientNum ) {
|
|
int counts[TEAM_NUM_TEAMS];
|
|
|
|
counts[TEAM_BLUE] = TeamCount( ignoreClientNum, TEAM_BLUE );
|
|
counts[TEAM_RED] = TeamCount( ignoreClientNum, TEAM_RED );
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
ForceClientSkin
|
|
|
|
Forces a client's skin (for teamplay)
|
|
===========
|
|
*/
|
|
void ForceClientSkin(char *model, const char *skin ) {
|
|
char *p;
|
|
|
|
if ((p = strchr(model, '/')) != NULL) {
|
|
*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, "RedShirt", outSize );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
legalSkin
|
|
|
|
Compare a list of races with an incoming race name.
|
|
Used to decide if in a CTF game where a race is specified for a given team if a skin is actually already legal.
|
|
===========
|
|
*/
|
|
qboolean legalSkin(char *race_list, char *race)
|
|
{
|
|
char current_race_name[125];
|
|
char *s = race_list;
|
|
char *max_place = race_list + strlen(race_list);
|
|
char *marker;
|
|
|
|
memset(current_race_name, 0, sizeof(current_race_name));
|
|
// look through the list till it's empty
|
|
while (s < max_place)
|
|
{
|
|
marker = s;
|
|
// figure out from where we are where the next ',' or 0 is
|
|
while (*s != ',' && *s != 0)
|
|
{
|
|
s++;
|
|
}
|
|
|
|
// copy just that name
|
|
Q_strncpyz(current_race_name, marker, (s-marker)+1);
|
|
|
|
// avoid the comma or increment us past the end of the string so we fail the main while loop
|
|
s++;
|
|
|
|
// compare and see if this race is the same as the one we want
|
|
if (!Q_stricmp(current_race_name, race))
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
randomSkin
|
|
|
|
given a race name, go find all the skins that use it, and randomly select one
|
|
===========
|
|
*/
|
|
|
|
void randomSkin(char* race, char* model, int current_team, int clientNum)
|
|
{
|
|
char skinsForRace[MAX_SKINS_FOR_RACE][128];
|
|
int howManySkins = 0;
|
|
int i,x;
|
|
int temp;
|
|
int skin_count_check;
|
|
char skinNamesAlreadyUsed[16][128];
|
|
int current_skin_count = 0;
|
|
gentity_t *ent = NULL;
|
|
char userinfo[MAX_INFO_STRING];
|
|
char temp_model[MAX_QPATH];
|
|
|
|
memset(skinsForRace, 0, sizeof(skinsForRace));
|
|
memset(skinNamesAlreadyUsed, 0, sizeof(skinNamesAlreadyUsed));
|
|
|
|
// first up, check to see if we want to select a skin from someone that's already playing on this guys team
|
|
skin_count_check = g_random_skin_limit.integer;
|
|
if (skin_count_check)
|
|
{
|
|
// sanity check the skins to compare against count
|
|
if (skin_count_check > 16)
|
|
{
|
|
skin_count_check = 16;
|
|
}
|
|
|
|
// now construct an array of the names already used
|
|
for (i=0; i<g_maxclients.integer; i++)
|
|
{
|
|
// did we find enough skins to grab a random one from yet?
|
|
if (current_skin_count == skin_count_check)
|
|
{
|
|
break;
|
|
}
|
|
|
|
ent = g_entities + i;
|
|
if (!ent->inuse || i == clientNum)
|
|
continue;
|
|
|
|
// no, so look at the next one, and see if it's in the list we are constructing
|
|
// same team?
|
|
if (ent->client && ent->client->sess.sessionTeam == current_team)
|
|
{
|
|
// so what's this clients model then?
|
|
trap_GetUserinfo( i, userinfo, sizeof( userinfo ) );
|
|
Q_strncpyz( temp_model, Info_ValueForKey (userinfo, "model"), sizeof( temp_model ) );
|
|
|
|
// check the name
|
|
for (x = 0; x< current_skin_count; x++)
|
|
{
|
|
// are we the same?
|
|
if (!Q_stricmp(skinNamesAlreadyUsed[x], temp_model))
|
|
{
|
|
// yeah - ok we already got this one
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ok, did we match anything?
|
|
if (x == current_skin_count)
|
|
{
|
|
// no - better add this name in
|
|
Q_strncpyz(skinNamesAlreadyUsed[current_skin_count], temp_model, sizeof(skinNamesAlreadyUsed[current_skin_count]));
|
|
current_skin_count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ok, array constructed. Did we get enough?
|
|
if (current_skin_count >= skin_count_check)
|
|
{
|
|
// yeah, we did - so select a skin from one of these then
|
|
temp = rand() % current_skin_count;
|
|
Q_strncpyz( model, skinNamesAlreadyUsed[temp], MAX_QPATH );
|
|
ForceClientSkin(model, "");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// search through each and every skin we can find
|
|
for (i=0; i<group_count && howManySkins < MAX_SKINS_FOR_RACE; i++)
|
|
{
|
|
|
|
// if this models race list contains the race we want, then add it to the list
|
|
if (legalSkin(group_list[i].text, race))
|
|
{
|
|
Q_strncpyz( skinsForRace[howManySkins++], group_list[i].name , 128 );
|
|
}
|
|
}
|
|
|
|
// set model to a random one
|
|
if (howManySkins)
|
|
{
|
|
temp = rand() % howManySkins;
|
|
Q_strncpyz( model, skinsForRace[temp], MAX_QPATH );
|
|
}
|
|
else
|
|
{
|
|
model[0] = 0;
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
===========
|
|
getNewSkin
|
|
|
|
Go away and actually get a random new skin based on a group name
|
|
============
|
|
*/
|
|
qboolean getNewSkin(char *group, char *model, char *color, gclient_t *client, int clientNum)
|
|
{
|
|
char *temp_string;
|
|
|
|
// go away and get what ever races this skin is attached to.
|
|
// remove blue or red name
|
|
ForceClientSkin(model, "");
|
|
|
|
temp_string = G_searchGroupList(model);
|
|
|
|
// are any of the races legal for this team race?
|
|
if (legalSkin(temp_string, group))
|
|
{
|
|
ForceClientSkin(model, color);
|
|
return qfalse;
|
|
}
|
|
|
|
//if we got this far, then we need to reset the skin to something appropriate
|
|
randomSkin(group, model, client->sess.sessionTeam, clientNum);
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
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;
|
|
char *s;
|
|
char model[MAX_QPATH];
|
|
char oldname[MAX_STRING_CHARS];
|
|
gclient_t *client;
|
|
char *c1;
|
|
char userinfo[MAX_INFO_STRING];
|
|
qboolean reset;
|
|
|
|
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
|
|
client->pers.maxHealth = atoi( Info_ValueForKey( userinfo, "handicap" ) );
|
|
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 ) );
|
|
|
|
// team
|
|
switch( client->sess.sessionTeam ) {
|
|
case TEAM_RED:
|
|
// decide if we are going to have to reset a skin cos it's not applicable to a race selected
|
|
if (g_gametype.integer < GT_TEAM || !Q_stricmp("", g_team_group_red.string))
|
|
{
|
|
ForceClientSkin(model, "red");
|
|
break;
|
|
}
|
|
// at this point, we are playing CTF and there IS a race specified for this game
|
|
else
|
|
{
|
|
// go away and get what ever races this skin is attached to.
|
|
reset = getNewSkin(g_team_group_red.string, model, "red", client, clientNum);
|
|
|
|
// did we get a model name back?
|
|
if (!model[0])
|
|
{
|
|
// no - this almost certainly means we had a bogus race is the g_team_group_team cvar
|
|
// so reset it to starfleet and try it again
|
|
Com_Printf("WARNING! - Red Group %s is unknown - resetting Red Group to Allow Any Group\n", g_team_group_red.string);
|
|
trap_Cvar_Set("g_team_group_red", "");
|
|
trap_Cvar_Register( &g_team_group_red, "g_team_group_red",
|
|
"", CVAR_LATCH);
|
|
|
|
// Since we are allow any group now, just get his normal model and carry on
|
|
Q_strncpyz( model, Info_ValueForKey (userinfo, "model"), sizeof( model ) );
|
|
ForceClientSkin(model, "red");
|
|
reset = qfalse;
|
|
}
|
|
|
|
if (reset)
|
|
{
|
|
trap_SendServerCommand( -1, va("print \"In-appropriate skin selected for %s on team %s\nSkin selection overridden from skin %s to skin %s\n\"",
|
|
client->pers.netname, g_team_group_red.string, Info_ValueForKey (userinfo, "model"), model));
|
|
ForceClientSkin(model, "red");
|
|
// change the value in out local copy, then update it on the server
|
|
Info_SetValueForKey(userinfo, "model", model);
|
|
trap_SetUserinfo(clientNum, userinfo);
|
|
}
|
|
break;
|
|
}
|
|
case TEAM_BLUE:
|
|
// decide if we are going to have to reset a skin cos it's not applicable to a race selected
|
|
if (g_gametype.integer < GT_TEAM || !Q_stricmp("", g_team_group_blue.string))
|
|
{
|
|
ForceClientSkin(model, "blue");
|
|
break;
|
|
}
|
|
// at this point, we are playing CTF and there IS a race specified for this game
|
|
else
|
|
{
|
|
// go away and get what ever races this skin is attached to.
|
|
reset = getNewSkin(g_team_group_blue.string, model, "blue", client, clientNum);
|
|
|
|
// did we get a model name back?
|
|
if (!model[0])
|
|
{
|
|
// no - this almost certainly means we had a bogus race is the g_team_group_team cvar
|
|
// so reset it to klingon and try it again
|
|
Com_Printf("WARNING! - Blue Group %s is unknown - resetting Blue Group to Allow Any Group\n", g_team_group_blue.string);
|
|
trap_Cvar_Set("g_team_group_blue", "");
|
|
trap_Cvar_Register( &g_team_group_blue, "g_team_group_blue",
|
|
"", CVAR_LATCH );
|
|
|
|
// Since we are allow any group now, just get his normal model and carry on
|
|
Q_strncpyz( model, Info_ValueForKey (userinfo, "model"), sizeof( model ) );
|
|
ForceClientSkin(model, "blue");
|
|
reset = qfalse;
|
|
}
|
|
|
|
if (reset)
|
|
{
|
|
trap_SendServerCommand( -1, va("print \"In-appropriate skin selected for %s on team %s\nSkin selection overridden from skin %s to skin %s\n\"",
|
|
client->pers.netname, g_team_group_blue.string, Info_ValueForKey (userinfo, "model"), model));
|
|
ForceClientSkin(model, "blue");
|
|
// change the value in out local copy, then update it on the server
|
|
Info_SetValueForKey(userinfo, "model", model);
|
|
trap_SetUserinfo(clientNum, userinfo);
|
|
}
|
|
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(model, "red");
|
|
}
|
|
|
|
|
|
// colors
|
|
c1 = Info_ValueForKey( userinfo, "color" );
|
|
|
|
// teamInfo
|
|
s = Info_ValueForKey( userinfo, "teamoverlay" );
|
|
if ( ! *s || atoi( s ) != 0 ) {
|
|
client->pers.teamInfo = qtrue;
|
|
} else {
|
|
client->pers.teamInfo = qfalse;
|
|
}
|
|
|
|
// 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\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i\\skill\\%s",
|
|
client->pers.netname, client->sess.sessionTeam, model, c1,
|
|
client->pers.maxHealth, client->sess.wins, client->sess.losses,
|
|
Info_ValueForKey( userinfo, "skill" ) );
|
|
} else {
|
|
s = va("n\\%s\\t\\%i\\model\\%s\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i",
|
|
client->pers.netname, client->sess.sessionTeam, model, c1,
|
|
client->pers.maxHealth, client->sess.wins, client->sess.losses );
|
|
}
|
|
|
|
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 );
|
|
|
|
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;
|
|
|
|
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 && g_holoIntro.integer==0)
|
|
{ // Don't use transporter FX for spectators or those watching the holodoors.
|
|
// 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();
|
|
|
|
// Use intro holodeck door if desired and we did not come from a restart
|
|
if (g_holoIntro.integer && !(ent->r.svFlags & SVF_BOT) && !(level.restarted) && !(g_restarted.integer))
|
|
{
|
|
// kef -- also, don't do this if we're in intermission
|
|
if (!level.intermissiontime)
|
|
{
|
|
client->ps.introTime = level.time + TIME_INTRO;
|
|
client->ps.pm_type = PM_FREEZE;
|
|
|
|
if (g_ghostRespawn.integer)
|
|
{
|
|
ent->client->ps.powerups[PW_GHOST] = level.time + (g_ghostRespawn.integer * 1000) + TIME_INTRO;
|
|
}
|
|
}
|
|
}
|
|
if (level.restarted || g_restarted.integer)
|
|
{
|
|
trap_Cvar_Set( "g_restarted", "0" );
|
|
level.restarted = qfalse;
|
|
}
|
|
|
|
// kef -- should reset all of our awards-related stuff
|
|
G_ClearClientLog(clientNum);
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
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;
|
|
|
|
index = ent - g_entities;
|
|
client = ent->client;
|
|
|
|
// 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 ) {
|
|
spawnPoint = SelectSpectatorSpawnPoint (
|
|
spawn_origin, spawn_angles);
|
|
} else if (g_gametype.integer == GT_CTF) {
|
|
spawnPoint = SelectCTFSpawnPoint (
|
|
client->sess.sessionTeam,
|
|
client->pers.teamState.state,
|
|
spawn_origin, spawn_angles);
|
|
} else {
|
|
do {
|
|
// the first spawn should be at a good looking spot
|
|
if ( !client->pers.initialSpawn && client->pers.localClient ) {
|
|
client->pers.initialSpawn = qtrue;
|
|
spawnPoint = SelectInitialSpawnPoint( spawn_origin, spawn_angles );
|
|
} else {
|
|
// don't spawn near existing origin if possible
|
|
spawnPoint = SelectSpawnPoint (
|
|
client->ps.origin,
|
|
spawn_origin, spawn_angles);
|
|
}
|
|
|
|
// Tim needs to prevent bots from spawning at the initial point
|
|
// on q3dm0...
|
|
if ( ( spawnPoint->flags & FL_NO_BOTS ) && ( ent->r.svFlags & SVF_BOT ) ) {
|
|
continue; // try again
|
|
}
|
|
// just to be symetric, we have a nohumans option...
|
|
if ( ( spawnPoint->flags & FL_NO_HUMANS ) && !( ent->r.svFlags & SVF_BOT ) ) {
|
|
continue; // try again
|
|
}
|
|
|
|
break;
|
|
|
|
} while ( 1 );
|
|
}
|
|
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;
|
|
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];
|
|
}
|
|
memset (client, 0, sizeof(*client));
|
|
|
|
client->pers = saved;
|
|
client->sess = savedSess;
|
|
client->ps.ping = savedPing;
|
|
for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) {
|
|
client->ps.persistant[i] = persistant[i];
|
|
}
|
|
|
|
// 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;
|
|
|
|
// clear entity values
|
|
client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth;
|
|
client->ps.eFlags = flags;
|
|
client->streakCount = 0;
|
|
|
|
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;
|
|
|
|
client->ps.stats[STAT_WEAPONS] = ( 1 << WP_PHASER );
|
|
client->ps.ammo[WP_PHASER] = PHASER_AMMO_MAX;
|
|
|
|
// health will count down towards max_health
|
|
ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH] * 1.25;
|
|
|
|
// Start with a small amount of armor as well.
|
|
client->ps.stats[STAT_ARMOR] = client->ps.stats[STAT_MAX_HEALTH] * 0.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 ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) {
|
|
|
|
} else {
|
|
G_KillBox( ent );
|
|
trap_LinkEntity (ent);
|
|
|
|
// force the base weapon up
|
|
client->ps.weapon = WP_PHASER;
|
|
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;
|
|
|
|
// If ghosting is engaged, allow the player to be invulnerable briefly.
|
|
if (g_ghostRespawn.integer)
|
|
{
|
|
ent->client->ps.powerups[PW_GHOST] = level.time + (g_ghostRespawn.integer * 1000);
|
|
}
|
|
|
|
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 ( client->ps.stats[STAT_WEAPONS] & ( 1 << i ) ) {
|
|
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 ( ent->client->sess.sessionTeam != 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 );
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
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();
|
|
|
|
if ( ent->r.svFlags & SVF_BOT ) {
|
|
BotAIShutdownClient( clientNum );
|
|
}
|
|
|
|
// kef -- if this guy contributed to any of our kills/deaths/weapons logs, clean 'em out
|
|
G_ClearClientLog(clientNum);
|
|
}
|
|
|
|
|