2
===========================================================================
3
Copyright (C) 1999-2005 Id Software, Inc.
5
This file is part of Quake III Arena source code.
7
Quake III Arena source code is free software; you can redistribute it
8
and/or modify it under the terms of the GNU General Public License as
9
published by the Free Software Foundation; either version 2 of the License,
10
or (at your option) any later version.
12
Quake III Arena source code is distributed in the hope that it will be
13
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
14
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
GNU General Public License for more details.
17
You should have received a copy of the GNU General Public License
18
along with Quake III Arena source code; if not, write to the Free Software
19
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20
===========================================================================
25
// g_client.c -- client functions that don't happen every frame
27
static vec3_t playerMins = {-15, -15, -24};
28
static vec3_t playerMaxs = {15, 15, 32};
30
/*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32) initial
31
potential spawning position for deathmatch games.
32
The first time a player enters the game, they will be at an 'initial' spot.
33
Targets will be fired when someone spawns in on them.
34
"nobots" will prevent bots from using this spot.
35
"nohumans" will prevent non-bots from using this spot.
37
void SP_info_player_deathmatch( gentity_t *ent ) {
40
G_SpawnInt( "nobots", "0", &i);
42
ent->flags |= FL_NO_BOTS;
44
G_SpawnInt( "nohumans", "0", &i );
46
ent->flags |= FL_NO_HUMANS;
50
/*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32)
51
equivelant to info_player_deathmatch
53
void SP_info_player_start(gentity_t *ent) {
54
ent->classname = "info_player_deathmatch";
55
SP_info_player_deathmatch( ent );
58
/*QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32)
59
The intermission will be viewed from this point. Target an info_notnull for the view direction.
61
void SP_info_player_intermission( gentity_t *ent ) {
68
=======================================================================
72
=======================================================================
81
qboolean SpotWouldTelefrag( gentity_t *spot ) {
83
int touch[MAX_GENTITIES];
87
VectorAdd( spot->s.origin, playerMins, mins );
88
VectorAdd( spot->s.origin, playerMaxs, maxs );
89
num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES );
91
for (i=0 ; i<num ; i++) {
92
hit = &g_entities[touch[i]];
93
//if ( hit->client && hit->client->ps.stats[STAT_HEALTH] > 0 ) {
105
SelectNearestDeathmatchSpawnPoint
107
Find the spot that we DON'T want to use
110
#define MAX_SPAWN_POINTS 128
111
gentity_t *SelectNearestDeathmatchSpawnPoint( vec3_t from ) {
114
float dist, nearestDist;
115
gentity_t *nearestSpot;
117
nearestDist = 999999;
121
while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) {
123
VectorSubtract( spot->s.origin, from, delta );
124
dist = VectorLength( delta );
125
if ( dist < nearestDist ) {
137
SelectRandomDeathmatchSpawnPoint
139
go to a random point that doesn't telefrag
142
#define MAX_SPAWN_POINTS 128
143
gentity_t *SelectRandomDeathmatchSpawnPoint( void ) {
147
gentity_t *spots[MAX_SPAWN_POINTS];
152
while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) {
153
if ( SpotWouldTelefrag( spot ) ) {
156
spots[ count ] = spot;
160
if ( !count ) { // no spots that won't telefrag
161
return G_Find( NULL, FOFS(classname), "info_player_deathmatch");
164
selection = rand() % count;
165
return spots[ selection ];
170
SelectRandomFurthestSpawnPoint
172
Chooses a player start, deathmatch start, etc
175
gentity_t *SelectRandomFurthestSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) {
180
gentity_t *list_spot[64];
181
int numSpots, rnd, i, j;
186
while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) {
187
if ( SpotWouldTelefrag( spot ) ) {
190
VectorSubtract( spot->s.origin, avoidPoint, delta );
191
dist = VectorLength( delta );
192
for (i = 0; i < numSpots; i++) {
193
if ( dist > list_dist[i] ) {
194
if ( numSpots >= 64 )
196
for (j = numSpots; j > i; j--) {
197
list_dist[j] = list_dist[j-1];
198
list_spot[j] = list_spot[j-1];
208
if (i >= numSpots && numSpots < 64) {
209
list_dist[numSpots] = dist;
210
list_spot[numSpots] = spot;
215
spot = G_Find( NULL, FOFS(classname), "info_player_deathmatch");
217
G_Error( "Couldn't find a spawn point" );
218
VectorCopy (spot->s.origin, origin);
220
VectorCopy (spot->s.angles, angles);
224
// select a random spot from the spawn points furthest away
225
rnd = random() * (numSpots / 2);
227
VectorCopy (list_spot[rnd]->s.origin, origin);
229
VectorCopy (list_spot[rnd]->s.angles, angles);
231
return list_spot[rnd];
238
Chooses a player start, deathmatch start, etc
241
gentity_t *SelectSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) {
242
return SelectRandomFurthestSpawnPoint( avoidPoint, origin, angles );
246
gentity_t *nearestSpot;
248
nearestSpot = SelectNearestDeathmatchSpawnPoint( avoidPoint );
250
spot = SelectRandomDeathmatchSpawnPoint ( );
251
if ( spot == nearestSpot ) {
252
// roll again if it would be real close to point of death
253
spot = SelectRandomDeathmatchSpawnPoint ( );
254
if ( spot == nearestSpot ) {
256
spot = SelectRandomDeathmatchSpawnPoint ( );
260
// find a single player start spot
262
G_Error( "Couldn't find a spawn point" );
265
VectorCopy (spot->s.origin, origin);
267
VectorCopy (spot->s.angles, angles);
275
SelectInitialSpawnPoint
277
Try to find a spawn point marked 'initial', otherwise
278
use normal spawn selection.
281
gentity_t *SelectInitialSpawnPoint( vec3_t origin, vec3_t angles ) {
285
while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) {
286
if ( spot->spawnflags & 1 ) {
291
if ( !spot || SpotWouldTelefrag( spot ) ) {
292
return SelectSpawnPoint( vec3_origin, origin, angles );
295
VectorCopy (spot->s.origin, origin);
297
VectorCopy (spot->s.angles, angles);
304
SelectSpectatorSpawnPoint
308
gentity_t *SelectSpectatorSpawnPoint( vec3_t origin, vec3_t angles ) {
309
FindIntermissionPoint();
311
VectorCopy( level.intermission_origin, origin );
312
VectorCopy( level.intermission_angle, angles );
318
=======================================================================
322
=======================================================================
330
void InitBodyQue (void) {
334
level.bodyQueIndex = 0;
335
for (i=0; i<BODY_QUEUE_SIZE ; i++) {
337
ent->classname = "bodyque";
338
ent->neverFree = qtrue;
339
level.bodyQue[i] = ent;
347
After sitting around for five seconds, fall into the ground and dissapear
350
void BodySink( gentity_t *ent ) {
351
if ( level.time - ent->timestamp > 6500 ) {
352
// the body ques are never actually freed, they are just unlinked
353
trap_UnlinkEntity( ent );
354
ent->physicsObject = qfalse;
357
ent->nextthink = level.time + 100;
358
ent->s.pos.trBase[2] -= 1;
365
A player is respawning, so make an entity that looks
366
just like the existing corpse to leave behind.
369
void CopyToBodyQue( gentity_t *ent ) {
377
trap_UnlinkEntity (ent);
379
// if client is in a nodrop area, don't leave the body
380
contents = trap_PointContents( ent->s.origin, -1 );
381
if ( contents & CONTENTS_NODROP ) {
385
// grab a body que and cycle to the next one
386
body = level.bodyQue[ level.bodyQueIndex ];
387
level.bodyQueIndex = (level.bodyQueIndex + 1) % BODY_QUEUE_SIZE;
389
trap_UnlinkEntity (body);
392
body->s.eFlags = EF_DEAD; // clear EF_TALK, etc
394
if ( ent->s.eFlags & EF_KAMIKAZE ) {
395
body->s.eFlags |= EF_KAMIKAZE;
397
// check if there is a kamikaze timer around for this owner
398
for (i = 0; i < MAX_GENTITIES; i++) {
402
if (e->activator != ent)
404
if (strcmp(e->classname, "kamikaze timer"))
411
body->s.powerups = 0; // clear powerups
412
body->s.loopSound = 0; // clear lava burning
413
body->s.number = body - g_entities;
414
body->timestamp = level.time;
415
body->physicsObject = qtrue;
416
body->physicsBounce = 0; // don't bounce
417
if ( body->s.groundEntityNum == ENTITYNUM_NONE ) {
418
body->s.pos.trType = TR_GRAVITY;
419
body->s.pos.trTime = level.time;
420
VectorCopy( ent->client->ps.velocity, body->s.pos.trDelta );
422
body->s.pos.trType = TR_STATIONARY;
426
// change the animation to the last-frame only, so the sequence
427
// doesn't repeat anew for the body
428
switch ( body->s.legsAnim & ~ANIM_TOGGLEBIT ) {
431
body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD1;
435
body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD2;
440
body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD3;
444
body->r.svFlags = ent->r.svFlags;
445
VectorCopy (ent->r.mins, body->r.mins);
446
VectorCopy (ent->r.maxs, body->r.maxs);
447
VectorCopy (ent->r.absmin, body->r.absmin);
448
VectorCopy (ent->r.absmax, body->r.absmax);
450
body->clipmask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP;
451
body->r.contents = CONTENTS_CORPSE;
452
body->r.ownerNum = ent->s.number;
454
body->nextthink = level.time + 5000;
455
body->think = BodySink;
457
body->die = body_die;
459
// don't take more damage if already gibbed
460
if ( ent->health <= GIB_HEALTH ) {
461
body->takedamage = qfalse;
463
body->takedamage = qtrue;
467
VectorCopy ( body->s.pos.trBase, body->r.currentOrigin );
468
trap_LinkEntity (body);
471
//======================================================================
480
void SetClientViewAngle( gentity_t *ent, vec3_t angle ) {
483
// set the delta angle
484
for (i=0 ; i<3 ; i++) {
487
cmdAngle = ANGLE2SHORT(angle[i]);
488
ent->client->ps.delta_angles[i] = cmdAngle - ent->client->pers.cmd.angles[i];
490
VectorCopy( angle, ent->s.angles );
491
VectorCopy (ent->s.angles, ent->client->ps.viewangles);
499
void respawn( gentity_t *ent ) {
505
// add a teleportation effect
506
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN );
507
tent->s.clientNum = ent->s.clientNum;
514
Returns number of players on a team
517
team_t TeamCount( int ignoreClientNum, int team ) {
521
for ( i = 0 ; i < level.maxclients ; i++ ) {
522
if ( i == ignoreClientNum ) {
525
if ( level.clients[i].pers.connected == CON_DISCONNECTED ) {
528
if ( level.clients[i].sess.sessionTeam == team ) {
540
Returns the client number of the team leader
543
int TeamLeader( int team ) {
546
for ( i = 0 ; i < level.maxclients ; i++ ) {
547
if ( level.clients[i].pers.connected == CON_DISCONNECTED ) {
550
if ( level.clients[i].sess.sessionTeam == team ) {
551
if ( level.clients[i].sess.teamLeader )
566
team_t PickTeam( int ignoreClientNum ) {
567
int counts[TEAM_NUM_TEAMS];
569
counts[TEAM_BLUE] = TeamCount( ignoreClientNum, TEAM_BLUE );
570
counts[TEAM_RED] = TeamCount( ignoreClientNum, TEAM_RED );
572
if ( counts[TEAM_BLUE] > counts[TEAM_RED] ) {
575
if ( counts[TEAM_RED] > counts[TEAM_BLUE] ) {
578
// equal team count, so join the team with the lowest score
579
if ( level.teamScores[TEAM_BLUE] > level.teamScores[TEAM_RED] ) {
589
Forces a client's skin (for teamplay)
593
static void ForceClientSkin( gclient_t *client, char *model, const char *skin ) {
596
if ((p = Q_strrchr(model, '/')) != 0) {
600
Q_strcat(model, MAX_QPATH, "/");
601
Q_strcat(model, MAX_QPATH, skin);
610
static void ClientCleanName( const char *in, char *out, int outSize ) {
611
int len, colorlessLen;
616
//save room for trailing null byte
631
// don't allow leading spaces
632
if( !*p && ch == ' ' ) {
637
if( ch == Q_COLOR_ESCAPE ) {
638
// solo trailing carat is not a color prefix
643
// don't allow black in a name, period
644
if( ColorIndex(*in) == 0 ) {
649
// make sure room in dest for both chars
650
if( len > outSize - 2 ) {
660
// don't allow too many consecutive spaces
671
if( len > outSize - 1 ) {
681
// don't allow empty names
682
if( *p == 0 || colorlessLen == 0 ) {
683
Q_strncpyz( p, "UnnamedPlayer", outSize );
690
ClientUserInfoChanged
692
Called from ClientConnect when the player first connects and
693
directly by the server system when the player updates a userinfo variable.
695
The game can override any of the settings and call trap_SetUserinfo
699
void ClientUserinfoChanged( int clientNum ) {
701
int teamTask, teamLeader, team, health;
703
char model[MAX_QPATH];
704
char headModel[MAX_QPATH];
705
char oldname[MAX_STRING_CHARS];
707
char c1[MAX_INFO_STRING];
708
char c2[MAX_INFO_STRING];
709
char redTeam[MAX_INFO_STRING];
710
char blueTeam[MAX_INFO_STRING];
711
char userinfo[MAX_INFO_STRING];
713
ent = g_entities + clientNum;
714
client = ent->client;
716
trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) );
718
// check for malformed or illegal info strings
719
if ( !Info_Validate(userinfo) ) {
720
strcpy (userinfo, "\\name\\badinfo");
723
// check for local client
724
s = Info_ValueForKey( userinfo, "ip" );
725
if ( !strcmp( s, "localhost" ) ) {
726
client->pers.localClient = qtrue;
729
// check the item prediction
730
s = Info_ValueForKey( userinfo, "cg_predictItems" );
732
client->pers.predictItemPickup = qfalse;
734
client->pers.predictItemPickup = qtrue;
738
Q_strncpyz ( oldname, client->pers.netname, sizeof( oldname ) );
739
s = Info_ValueForKey (userinfo, "name");
740
ClientCleanName( s, client->pers.netname, sizeof(client->pers.netname) );
742
if ( client->sess.sessionTeam == TEAM_SPECTATOR ) {
743
if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) {
744
Q_strncpyz( client->pers.netname, "scoreboard", sizeof(client->pers.netname) );
748
if ( client->pers.connected == CON_CONNECTED ) {
749
if ( strcmp( oldname, client->pers.netname ) ) {
750
trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " renamed to %s\n\"", oldname,
751
client->pers.netname) );
757
if (client->ps.powerups[PW_GUARD]) {
758
client->pers.maxHealth = 200;
760
health = atoi( Info_ValueForKey( userinfo, "handicap" ) );
761
client->pers.maxHealth = health;
762
if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) {
763
client->pers.maxHealth = 100;
767
health = atoi( Info_ValueForKey( userinfo, "handicap" ) );
768
client->pers.maxHealth = health;
769
if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) {
770
client->pers.maxHealth = 100;
773
client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth;
776
if( g_gametype.integer >= GT_TEAM ) {
777
Q_strncpyz( model, Info_ValueForKey (userinfo, "team_model"), sizeof( model ) );
778
Q_strncpyz( headModel, Info_ValueForKey (userinfo, "team_headmodel"), sizeof( headModel ) );
780
Q_strncpyz( model, Info_ValueForKey (userinfo, "model"), sizeof( model ) );
781
Q_strncpyz( headModel, Info_ValueForKey (userinfo, "headmodel"), sizeof( headModel ) );
784
// bots set their team a few frames later
785
if (g_gametype.integer >= GT_TEAM && g_entities[clientNum].r.svFlags & SVF_BOT) {
786
s = Info_ValueForKey( userinfo, "team" );
787
if ( !Q_stricmp( s, "red" ) || !Q_stricmp( s, "r" ) ) {
789
} else if ( !Q_stricmp( s, "blue" ) || !Q_stricmp( s, "b" ) ) {
792
// pick the team with the least number of players
793
team = PickTeam( clientNum );
797
team = client->sess.sessionTeam;
800
/* NOTE: all client side now
805
ForceClientSkin(client, model, "red");
806
// ForceClientSkin(client, headModel, "red");
809
ForceClientSkin(client, model, "blue");
810
// ForceClientSkin(client, headModel, "blue");
813
// don't ever use a default skin in teamplay, it would just waste memory
814
// however bots will always join a team but they spawn in as spectator
815
if ( g_gametype.integer >= GT_TEAM && team == TEAM_SPECTATOR) {
816
ForceClientSkin(client, model, "red");
817
// ForceClientSkin(client, headModel, "red");
822
if (g_gametype.integer >= GT_TEAM) {
823
client->pers.teamInfo = qtrue;
825
s = Info_ValueForKey( userinfo, "teamoverlay" );
826
if ( ! *s || atoi( s ) != 0 ) {
827
client->pers.teamInfo = qtrue;
829
client->pers.teamInfo = qfalse;
834
s = Info_ValueForKey( userinfo, "teamoverlay" );
835
if ( ! *s || atoi( s ) != 0 ) {
836
client->pers.teamInfo = qtrue;
838
client->pers.teamInfo = qfalse;
842
s = Info_ValueForKey( userinfo, "cg_pmove_fixed" );
843
if ( !*s || atoi( s ) == 0 ) {
844
client->pers.pmoveFixed = qfalse;
847
client->pers.pmoveFixed = qtrue;
851
// team task (0 = none, 1 = offence, 2 = defence)
852
teamTask = atoi(Info_ValueForKey(userinfo, "teamtask"));
853
// team Leader (1 = leader, 0 is normal player)
854
teamLeader = client->sess.teamLeader;
857
strcpy(c1, Info_ValueForKey( userinfo, "color1" ));
858
strcpy(c2, Info_ValueForKey( userinfo, "color2" ));
860
strcpy(redTeam, Info_ValueForKey( userinfo, "g_redteam" ));
861
strcpy(blueTeam, Info_ValueForKey( userinfo, "g_blueteam" ));
863
// send over a subset of the userinfo keys so other clients can
864
// print scoreboards, display models, and play custom sounds
865
if ( ent->r.svFlags & SVF_BOT ) {
866
s = va("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\c1\\%s\\c2\\%s\\hc\\%i\\w\\%i\\l\\%i\\skill\\%s\\tt\\%d\\tl\\%d",
867
client->pers.netname, team, model, headModel, c1, c2,
868
client->pers.maxHealth, client->sess.wins, client->sess.losses,
869
Info_ValueForKey( userinfo, "skill" ), teamTask, teamLeader );
871
s = va("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\g_redteam\\%s\\g_blueteam\\%s\\c1\\%s\\c2\\%s\\hc\\%i\\w\\%i\\l\\%i\\tt\\%d\\tl\\%d",
872
client->pers.netname, client->sess.sessionTeam, model, headModel, redTeam, blueTeam, c1, c2,
873
client->pers.maxHealth, client->sess.wins, client->sess.losses, teamTask, teamLeader);
876
trap_SetConfigstring( CS_PLAYERS+clientNum, s );
878
// this is not the userinfo, more like the configstring actually
879
G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, s );
887
Called when a player begins connecting to the server.
888
Called again for every map change or tournement restart.
890
The session information will be valid after exit.
892
Return NULL if the client should be allowed, otherwise return
893
a string with the reason for denial.
895
Otherwise, the client will be sent the current gamestate
896
and will eventually get to ClientBegin.
898
firstTime will be qtrue the very first time a client connects
899
to the server machine, but qfalse on map changes and tournement
903
char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ) {
907
char userinfo[MAX_INFO_STRING];
910
ent = &g_entities[ clientNum ];
912
trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) );
915
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=500
916
// recommanding PB based IP / GUID banning, the builtin system is pretty limited
917
// check to see if they are on the banned IP list
918
value = Info_ValueForKey (userinfo, "ip");
919
if ( G_FilterPacket( value ) ) {
920
return "You are banned from this server.";
923
// we don't check password for bots and local client
924
// NOTE: local client <-> "ip" "localhost"
925
// this means this client is not running in our current process
926
if ( !isBot && (strcmp(value, "localhost") != 0)) {
927
// check for a password
928
value = Info_ValueForKey (userinfo, "password");
929
if ( g_password.string[0] && Q_stricmp( g_password.string, "none" ) &&
930
strcmp( g_password.string, value) != 0) {
931
return "Invalid password";
936
ent->client = level.clients + clientNum;
937
client = ent->client;
939
// areabits = client->areabits;
941
memset( client, 0, sizeof(*client) );
943
client->pers.connected = CON_CONNECTING;
945
// read or initialize the session data
946
if ( firstTime || level.newSession ) {
947
G_InitSessionData( client, userinfo );
949
G_ReadSessionData( client );
952
ent->r.svFlags |= SVF_BOT;
954
if( !G_BotConnect( clientNum, !firstTime ) ) {
955
return "BotConnectfailed";
959
// get and distribute relevent paramters
960
G_LogPrintf( "ClientConnect: %i\n", clientNum );
961
ClientUserinfoChanged( clientNum );
963
// don't do the "xxx connected" messages if they were caried over from previous level
965
trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " connected\n\"", client->pers.netname) );
968
if ( g_gametype.integer >= GT_TEAM &&
969
client->sess.sessionTeam != TEAM_SPECTATOR ) {
970
BroadcastTeamChange( client, -1 );
973
// count current clients and rank for scoreboard
977
// client->areabits = areabits;
978
// if ( !client->areabits )
979
// client->areabits = G_Alloc( (trap_AAS_PointReachabilityAreaIndex( NULL ) + 7) / 8 );
988
called when a client has finished connecting, and is ready
989
to be placed into the level. This will happen every level load,
990
and on transition between teams, but doesn't happen on respawns
993
void ClientBegin( int clientNum ) {
999
ent = g_entities + clientNum;
1001
client = level.clients + clientNum;
1003
if ( ent->r.linked ) {
1004
trap_UnlinkEntity( ent );
1006
G_InitGentity( ent );
1009
ent->client = client;
1011
client->pers.connected = CON_CONNECTED;
1012
client->pers.enterTime = level.time;
1013
client->pers.teamState.state = TEAM_BEGIN;
1015
// save eflags around this, because changing teams will
1016
// cause this to happen with a valid entity, and we
1017
// want to make sure the teleport bit is set right
1018
// so the viewpoint doesn't interpolate through the
1019
// world to the new position
1020
flags = client->ps.eFlags;
1021
memset( &client->ps, 0, sizeof( client->ps ) );
1022
client->ps.eFlags = flags;
1024
// locate ent at a spawn point
1027
if ( client->sess.sessionTeam != TEAM_SPECTATOR ) {
1029
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN );
1030
tent->s.clientNum = ent->s.clientNum;
1032
if ( g_gametype.integer != GT_TOURNAMENT ) {
1033
trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname) );
1036
G_LogPrintf( "ClientBegin: %i\n", clientNum );
1038
// count current clients and rank for scoreboard
1046
Called every time a client is placed fresh in the world:
1047
after the first ClientBegin, and after each respawn
1048
Initializes all non-persistant parts of playerState
1051
void ClientSpawn(gentity_t *ent) {
1053
vec3_t spawn_origin, spawn_angles;
1056
clientPersistant_t saved;
1057
clientSession_t savedSess;
1058
int persistant[MAX_PERSISTANT];
1059
gentity_t *spawnPoint;
1062
// char *savedAreaBits;
1063
int accuracy_hits, accuracy_shots;
1065
char userinfo[MAX_INFO_STRING];
1067
index = ent - g_entities;
1068
client = ent->client;
1070
// find a spawn point
1071
// do it before setting health back up, so farthest
1072
// ranging doesn't count this client
1073
if ( client->sess.sessionTeam == TEAM_SPECTATOR ) {
1074
spawnPoint = SelectSpectatorSpawnPoint (
1075
spawn_origin, spawn_angles);
1076
} else if (g_gametype.integer >= GT_CTF ) {
1077
// all base oriented team games use the CTF spawn points
1078
spawnPoint = SelectCTFSpawnPoint (
1079
client->sess.sessionTeam,
1080
client->pers.teamState.state,
1081
spawn_origin, spawn_angles);
1084
// the first spawn should be at a good looking spot
1085
if ( !client->pers.initialSpawn && client->pers.localClient ) {
1086
client->pers.initialSpawn = qtrue;
1087
spawnPoint = SelectInitialSpawnPoint( spawn_origin, spawn_angles );
1089
// don't spawn near existing origin if possible
1090
spawnPoint = SelectSpawnPoint (
1092
spawn_origin, spawn_angles);
1095
// Tim needs to prevent bots from spawning at the initial point
1097
if ( ( spawnPoint->flags & FL_NO_BOTS ) && ( ent->r.svFlags & SVF_BOT ) ) {
1098
continue; // try again
1100
// just to be symetric, we have a nohumans option...
1101
if ( ( spawnPoint->flags & FL_NO_HUMANS ) && !( ent->r.svFlags & SVF_BOT ) ) {
1102
continue; // try again
1109
client->pers.teamState.state = TEAM_ACTIVE;
1111
// always clear the kamikaze flag
1112
ent->s.eFlags &= ~EF_KAMIKAZE;
1114
// toggle the teleport bit so the client knows to not lerp
1115
// and never clear the voted flag
1116
flags = ent->client->ps.eFlags & (EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED);
1117
flags ^= EF_TELEPORT_BIT;
1119
// clear everything but the persistant data
1121
saved = client->pers;
1122
savedSess = client->sess;
1123
savedPing = client->ps.ping;
1124
// savedAreaBits = client->areabits;
1125
accuracy_hits = client->accuracy_hits;
1126
accuracy_shots = client->accuracy_shots;
1127
for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) {
1128
persistant[i] = client->ps.persistant[i];
1130
eventSequence = client->ps.eventSequence;
1132
memset (client, 0, sizeof(*client)); // bk FIXME: Com_Memset?
1134
client->pers = saved;
1135
client->sess = savedSess;
1136
client->ps.ping = savedPing;
1137
// client->areabits = savedAreaBits;
1138
client->accuracy_hits = accuracy_hits;
1139
client->accuracy_shots = accuracy_shots;
1140
client->lastkilled_client = -1;
1142
for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) {
1143
client->ps.persistant[i] = persistant[i];
1145
client->ps.eventSequence = eventSequence;
1146
// increment the spawncount so the client will detect the respawn
1147
client->ps.persistant[PERS_SPAWN_COUNT]++;
1148
client->ps.persistant[PERS_TEAM] = client->sess.sessionTeam;
1150
client->airOutTime = level.time + 12000;
1152
trap_GetUserinfo( index, userinfo, sizeof(userinfo) );
1154
client->pers.maxHealth = atoi( Info_ValueForKey( userinfo, "handicap" ) );
1155
if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) {
1156
client->pers.maxHealth = 100;
1158
// clear entity values
1159
client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth;
1160
client->ps.eFlags = flags;
1162
ent->s.groundEntityNum = ENTITYNUM_NONE;
1163
ent->client = &level.clients[index];
1164
ent->takedamage = qtrue;
1166
ent->classname = "player";
1167
ent->r.contents = CONTENTS_BODY;
1168
ent->clipmask = MASK_PLAYERSOLID;
1169
ent->die = player_die;
1170
ent->waterlevel = 0;
1174
VectorCopy (playerMins, ent->r.mins);
1175
VectorCopy (playerMaxs, ent->r.maxs);
1177
client->ps.clientNum = index;
1179
client->ps.stats[STAT_WEAPONS] = ( 1 << WP_MACHINEGUN );
1180
if ( g_gametype.integer == GT_TEAM ) {
1181
client->ps.ammo[WP_MACHINEGUN] = 50;
1183
client->ps.ammo[WP_MACHINEGUN] = 100;
1186
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GAUNTLET );
1187
client->ps.ammo[WP_GAUNTLET] = -1;
1188
client->ps.ammo[WP_GRAPPLING_HOOK] = -1;
1190
// health will count down towards max_health
1191
ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH] + 25;
1193
G_SetOrigin( ent, spawn_origin );
1194
VectorCopy( spawn_origin, client->ps.origin );
1196
// the respawned flag will be cleared after the attack and jump keys come up
1197
client->ps.pm_flags |= PMF_RESPAWNED;
1199
trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd );
1200
SetClientViewAngle( ent, spawn_angles );
1202
if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) {
1206
trap_LinkEntity (ent);
1208
// force the base weapon up
1209
client->ps.weapon = WP_MACHINEGUN;
1210
client->ps.weaponstate = WEAPON_READY;
1214
// don't allow full run speed for a bit
1215
client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
1216
client->ps.pm_time = 100;
1218
client->respawnTime = level.time;
1219
client->inactivityTime = level.time + g_inactivity.integer * 1000;
1220
client->latched_buttons = 0;
1222
// set default animations
1223
client->ps.torsoAnim = TORSO_STAND;
1224
client->ps.legsAnim = LEGS_IDLE;
1226
if ( level.intermissiontime ) {
1227
MoveClientToIntermission( ent );
1229
// fire the targets of the spawn point
1230
G_UseTargets( spawnPoint, ent );
1232
// select the highest weapon number available, after any
1233
// spawn given items have fired
1234
client->ps.weapon = 1;
1235
for ( i = WP_NUM_WEAPONS - 1 ; i > 0 ; i-- ) {
1236
if ( client->ps.stats[STAT_WEAPONS] & ( 1 << i ) ) {
1237
client->ps.weapon = i;
1243
// run a client frame to drop exactly to the floor,
1244
// initialize animations and other things
1245
client->ps.commandTime = level.time - 100;
1246
ent->client->pers.cmd.serverTime = level.time;
1247
ClientThink( ent-g_entities );
1249
// positively link the client, even if the command times are weird
1250
if ( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) {
1251
BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue );
1252
VectorCopy( ent->client->ps.origin, ent->r.currentOrigin );
1253
trap_LinkEntity( ent );
1256
// run the presend to set anything else
1257
ClientEndFrame( ent );
1259
// clear entity state values
1260
BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue );
1268
Called when a player drops from the server.
1269
Will not be called between levels.
1271
This should NOT be called directly by any game logic,
1272
call trap_DropClient(), which will call this and do
1273
server system housekeeping.
1276
void ClientDisconnect( int clientNum ) {
1281
// cleanup if we are kicking a bot that
1282
// hasn't spawned yet
1283
G_RemoveQueuedBotBegin( clientNum );
1285
ent = g_entities + clientNum;
1286
if ( !ent->client ) {
1290
// stop any following clients
1291
for ( i = 0 ; i < level.maxclients ; i++ ) {
1292
if ( level.clients[i].sess.sessionTeam == TEAM_SPECTATOR
1293
&& level.clients[i].sess.spectatorState == SPECTATOR_FOLLOW
1294
&& level.clients[i].sess.spectatorClient == clientNum ) {
1295
StopFollowing( &g_entities[i] );
1299
// send effect if they were completely connected
1300
if ( ent->client->pers.connected == CON_CONNECTED
1301
&& ent->client->sess.sessionTeam != TEAM_SPECTATOR ) {
1302
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT );
1303
tent->s.clientNum = ent->s.clientNum;
1305
// They don't get to take powerups with them!
1306
// Especially important for stuff like CTF flags
1307
TossClientItems( ent );
1309
TossClientPersistantPowerups( ent );
1310
if( g_gametype.integer == GT_HARVESTER ) {
1311
TossClientCubes( ent );
1317
G_LogPrintf( "ClientDisconnect: %i\n", clientNum );
1319
// if we are playing in tourney mode and losing, give a win to the other player
1320
if ( (g_gametype.integer == GT_TOURNAMENT )
1321
&& !level.intermissiontime
1322
&& !level.warmupTime && level.sortedClients[1] == clientNum ) {
1323
level.clients[ level.sortedClients[0] ].sess.wins++;
1324
ClientUserinfoChanged( level.sortedClients[0] );
1327
if( g_gametype.integer == GT_TOURNAMENT &&
1328
ent->client->sess.sessionTeam == TEAM_FREE &&
1329
level.intermissiontime ) {
1331
trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" );
1332
level.restarted = qtrue;
1333
level.changemap = NULL;
1334
level.intermissiontime = 0;
1337
trap_UnlinkEntity (ent);
1338
ent->s.modelindex = 0;
1339
ent->inuse = qfalse;
1340
ent->classname = "disconnected";
1341
ent->client->pers.connected = CON_DISCONNECTED;
1342
ent->client->ps.persistant[PERS_TEAM] = TEAM_FREE;
1343
ent->client->sess.sessionTeam = TEAM_FREE;
1345
trap_SetConfigstring( CS_PLAYERS + clientNum, "");
1349
if ( ent->r.svFlags & SVF_BOT ) {
1350
BotAIShutdownClient( clientNum, qfalse );