// 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 ; iclient) { 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 ]; } i = rand() % count; return spawns[ i ]; } /* =========== 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; } if ( *in != Q_COLOR_ESCAPE ) { // 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; } else { *out++ = ch; *out++ = ch; in++; 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 ); } } // Disable zooming client->ps.zoomFov = 0; client->ps.zoomTime = 0; client->ps.pm_flags &= ~(PMF_ZOOM_FLAGS); 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; } /* =========== G_FindClientByName Looks up a player by its case insensitive name ============ */ gclient_t* G_FindClientByName ( const char* name, int ignoreNum ) { int i; for ( i = 0; i < level.numConnectedClients; i ++ ) { gentity_t* ent = &g_entities[level.sortedClients[i]]; if ( level.sortedClients[i] == ignoreNum ) { continue; } if ( Q_stricmp ( name, ent->client->pers.netname ) == 0 ) { return ent->client; } } return NULL; } /* =========== 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]; char origname[MAX_NETNAME]; int namecount = 1; 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) ); } } // See if we need to find a new name because the one they chose is taken Q_strncpyz ( origname, client->pers.netname, MAX_NETNAME - 5 ); while ( G_FindClientByName ( client->pers.netname, clientNum ) ) { Com_sprintf ( client->pers.netname, MAX_NETNAME, "%s(%d)", origname, namecount ); namecount++; } // 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]; char ip[128]; char guid[64]; gentity_t *ent; ent = &g_entities[ clientNum ]; trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); value = Info_ValueForKey (userinfo, "cl_guid"); Com_sprintf ( guid, sizeof(guid), value ); // check to see if they are on the banned IP list value = Info_ValueForKey (userinfo, "ip"); Com_sprintf ( ip, sizeof(ip), value ); // 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(ip, "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 - %s [%s]\n", clientNum, ip, guid ); 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 ent->s.number = clientNum; 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 ); // See if we should spawn as a ghost if ( client->sess.team != TEAM_SPECTATOR && level.gametypeData->respawnType == RT_NONE ) { // If there are ghosts already then spawn as a ghost because // the game is already in progress. if ( !level.warmupTime && (level.gametypeJoinTime && (level.time - level.gametypeJoinTime) > (g_roundjointime.integer * 1000)) ) { G_StartGhosting ( ent ); } } // 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; ent->s.number = 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 ( index ); // 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; client->ps.weaponAnimIdChoice = 0; client->ps.weaponCallbackStep = 0; client->ps.weaponCallbackTime = 0; // Not on a ladder client->ps.ladder = -1; // Not leaning client->ps.leanTime = LEAN_TIME; if ( level.intermissiontime ) { MoveClientToIntermission( ent ); } // Frozen? if ( level.gametypeDelayTime > level.time ) { ent->client->ps.stats[STAT_FROZEN] = level.gametypeDelayTime - level.time; } // 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 ); // 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; } /* =========== G_IgnoreClientChat Instructs all chat to be ignored by the given ============ */ void G_IgnoreClientChat ( int ignorer, int ignoree, qboolean ignore ) { // Cant ignore yourself if ( ignorer == ignoree ) { return; } // If there is no client connected then dont bother if ( g_entities[ignoree].client->pers.connected != CON_CONNECTED ) { return; } if ( ignore ) { g_entities[ignoree].client->sess.chatIgnoreClients[ignorer/32] |= (1<<(ignorer%32)); } else { g_entities[ignoree].client->sess.chatIgnoreClients[ignorer/32] &= ~(1<<(ignorer%32)); } } /* =========== G_IsClientChatIgnored Checks to see if the given client is being ignored by a specific client ============ */ qboolean G_IsClientChatIgnored ( int ignorer, int ignoree ) { if ( g_entities[ignoree].client->sess.chatIgnoreClients[ignorer/32] & (1<<(ignorer%32)) ) { return qtrue; } return qfalse; }