void ctrl_relay_act( gentity_t *self, gentity_t*, gentity_t *activator ) { if (!self->enabled) return; if ( !self->config.wait.time ) { G_EventFireEntity( self, activator, ON_ACT ); } else { self->nextthink = VariatedLevelTime( self->config.wait ); self->think = think_fireOnActDelayed; self->activator = activator; } }
void G_CallEntity(gentity_t *targetedEntity, gentityCall_t *call) { if ( g_debugEntities.integer > 1 ) { G_Printf(S_DEBUG "[%s] %s calling %s %s:%s\n", etos( call->activator ), etos( call->caller ), call->definition ? call->definition->event : "onUnknown", etos( targetedEntity ), call->definition && call->definition->action ? call->definition->action : "default"); } targetedEntity->callIn = *call; if((!targetedEntity->handleCall || !targetedEntity->handleCall(targetedEntity, call)) && call->definition) { switch (call->definition->actionType) { case ECA_NOP: break; case ECA_CUSTOM: if ( g_debugEntities.integer > -1 ) { G_Printf(S_WARNING "Unknown action \"%s\" for %s\n", call->definition->action, etos(targetedEntity)); } break; case ECA_FREE: G_FreeEntity(targetedEntity); return; //we have to handle notification differently in the free-case case ECA_PROPAGATE: G_FireEntity( targetedEntity, call->activator); break; case ECA_ENABLE: if(!targetedEntity->enabled) //only fire an event if we weren't already enabled { targetedEntity->enabled = true; G_EventFireEntity( targetedEntity, call->activator, ON_ENABLE ); } break; case ECA_DISABLE: if(targetedEntity->enabled) //only fire an event if we weren't already disabled { targetedEntity->enabled = false; G_EventFireEntity( targetedEntity, call->activator, ON_DISABLE ); } break; case ECA_TOGGLE: targetedEntity->enabled = !targetedEntity->enabled; G_EventFireEntity( targetedEntity, call->activator, targetedEntity->enabled ? ON_ENABLE : ON_DISABLE ); break; case ECA_USE: if (!targetedEntity->use) { if(g_debugEntities.integer >= 0) G_Printf(S_WARNING "calling :use on %s, which has no use function!\n", etos(targetedEntity)); break; } if(!call->activator || !call->activator->client) { if(g_debugEntities.integer >= 0) G_Printf(S_WARNING "calling %s:use, without a client as activator.\n", etos(targetedEntity)); break; } targetedEntity->use(targetedEntity, call->caller, call->activator); break; case ECA_RESET: if (targetedEntity->reset) { targetedEntity->reset(targetedEntity); G_EventFireEntity( targetedEntity, call->activator, ON_RESET ); } break; case ECA_ACT: G_HandleActCall( targetedEntity, call ); break; default: if (targetedEntity->act) targetedEntity->act(targetedEntity, call->caller, call->activator); break; } } if(targetedEntity->notifyHandler) targetedEntity->notifyHandler( targetedEntity, call ); targetedEntity->callIn = NULL_CALL; /**< not called anymore */ }
void G_FireEntity( gentity_t *self, gentity_t *activator ) { G_EventFireEntity( self, activator, ON_DEFAULT ); }
void think_fireOnActDelayed( gentity_t *self ) { G_EventFireEntity( self, self->activator, ON_ACT ); }
void HealthComponent::HandleDamage(float amount, gentity_t* source, Util::optional<Vec3> location, Util::optional<Vec3> direction, int flags, meansOfDeath_t meansOfDeath) { if (health <= 0.0f) return; if (amount <= 0.0f) return; gclient_t *client = entity.oldEnt->client; // Check for immunity. if (entity.oldEnt->flags & FL_GODMODE) return; if (client) { if (client->noclip) return; if (client->sess.spectatorState != SPECTATOR_NOT) return; } // Set source to world if missing. if (!source) source = &g_entities[ENTITYNUM_WORLD]; // Don't handle ET_MOVER w/o die or pain function. // TODO: Handle mover special casing in a dedicated component. if (entity.oldEnt->s.eType == entityType_t::ET_MOVER && !(entity.oldEnt->die || entity.oldEnt->pain)) { // Special case for ET_MOVER with act function in initial position. if ((entity.oldEnt->moverState == MOVER_POS1 || entity.oldEnt->moverState == ROTATOR_POS1) && entity.oldEnt->act) { entity.oldEnt->act(entity.oldEnt, source, source); } return; } // Check for protection. if (!(flags & DAMAGE_NO_PROTECTION)) { // Check for protection from friendly damage. if (entity.oldEnt != source && G_OnSameTeam(entity.oldEnt, source)) { // Check if friendly fire has been disabled. if (!g_friendlyFire.integer) return; // Never do friendly damage on movement attacks. switch (meansOfDeath) { case MOD_LEVEL3_POUNCE: case MOD_LEVEL4_TRAMPLE: return; default: break; } // If dretchpunt is enabled and this is a dretch, do dretchpunt instead of damage. // TODO: Add a message for pushing. if (g_dretchPunt.integer && client && client->ps.stats[STAT_CLASS] == PCL_ALIEN_LEVEL0) { vec3_t dir, push; VectorSubtract(entity.oldEnt->r.currentOrigin, source->r.currentOrigin, dir); VectorNormalizeFast(dir); VectorScale(dir, (amount * 10.0f), push); push[ 2 ] = 64.0f; VectorAdd( client->ps.velocity, push, client->ps.velocity ); return; } } // Check for protection from friendly buildable damage. Never protect from building actions. // TODO: Use DAMAGE_NO_PROTECTION flag instead of listing means of death here. if (entity.oldEnt->s.eType == entityType_t::ET_BUILDABLE && source->client && meansOfDeath != MOD_DECONSTRUCT && meansOfDeath != MOD_SUICIDE && meansOfDeath != MOD_REPLACE) { if (G_OnSameTeam(entity.oldEnt, source) && !g_friendlyBuildableFire.integer) { return; } } } float take = amount; // Apply damage modifiers. if (!(flags & DAMAGE_PURE)) { entity.ApplyDamageModifier(take, location, direction, flags, meansOfDeath); } // Update combat timers. // TODO: Add a message to update combat timers. if (client && source->client && entity.oldEnt != source) { client->lastCombatTime = entity.oldEnt->client->lastCombatTime = level.time; } if (client) { // Save damage w/o armor modifier. client->damage_received += (int)(amount + 0.5f); // Save damage direction. if (direction) { VectorCopy(direction.value().Data(), client->damage_from); client->damage_fromWorld = false; } else { VectorCopy(entity.oldEnt->r.currentOrigin, client->damage_from); client->damage_fromWorld = true; } // Drain jetpack fuel. // TODO: Have another component handle jetpack fuel drain. client->ps.stats[STAT_FUEL] = std::max(0, client->ps.stats[STAT_FUEL] - (int)(amount + 0.5f) * JETPACK_FUEL_PER_DMG); // If boosted poison every attack. // TODO: Add a poison message and a PoisonableComponent. if (source->client && (source->client->ps.stats[STAT_STATE] & SS_BOOSTED) && client->pers.team == TEAM_HUMANS && client->poisonImmunityTime < level.time) { switch (meansOfDeath) { case MOD_POISON: case MOD_LEVEL2_ZAP: break; default: client->ps.stats[STAT_STATE] |= SS_POISONED; client->lastPoisonTime = level.time; client->lastPoisonClient = source; break; } } } healthLogger.Notice("Taking damage: %3.1f (%3.1f → %3.1f)", take, health, health - take); // Do the damage. health -= take; // Update team overlay info. if (client) client->pers.infoChangeTime = level.time; // TODO: Move lastDamageTime to HealthComponent. entity.oldEnt->lastDamageTime = level.time; // HACK: gentity_t.nextRegenTime only affects alien clients. // TODO: Catch damage message in a new RegenerationComponent. entity.oldEnt->nextRegenTime = level.time + ALIEN_CLIENT_REGEN_WAIT; // Handle non-self damage. if (entity.oldEnt != source) { float loss = take; if (health < 0.0f) loss += health; // TODO: Use ClientComponent. if (source->client) { // Add to the attacker's account on the target. // TODO: Move damage account array to HealthComponent. entity.oldEnt->credits[source->client->ps.clientNum].value += loss; entity.oldEnt->credits[source->client->ps.clientNum].time = level.time; entity.oldEnt->credits[source->client->ps.clientNum].team = (team_t)source->client->pers.team; } } // Handle death. // TODO: Send a Die/Pain message and handle details where appropriate. if (health <= 0) { healthLogger.Notice("Dying with %.1f health.", health); // Disable knockback. if (client) entity.oldEnt->flags |= FL_NO_KNOCKBACK; // Call legacy die function. if (entity.oldEnt->die) entity.oldEnt->die(entity.oldEnt, source, source, meansOfDeath); // Send die message. entity.Die(source, meansOfDeath); // Trigger ON_DIE event. if(!client) G_EventFireEntity(entity.oldEnt, source, ON_DIE); } else if (entity.oldEnt->pain) { entity.oldEnt->pain(entity.oldEnt, source, (int)std::ceil(take)); } if (entity.oldEnt != source && source->client) { bool lethal = (health <= 0); CombatFeedback::HitNotify(source, entity.oldEnt, location, take, meansOfDeath, lethal); } }
// TODO: Clean this mess further (split into helper functions) void G_Damage( gentity_t *target, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, vec3_t point, int damage, int damageFlags, int mod ) { gclient_t *client; int take, loss; int knockback; float modifier; if ( !target || !target->takedamage || target->health <= 0 || level.intermissionQueued ) { return; } client = target->client; // don't handle noclip clients if ( client && client->noclip ) { return; } // set inflictor to world if missing if ( !inflictor ) { inflictor = &g_entities[ ENTITYNUM_WORLD ]; } // set attacker to world if missing if ( !attacker ) { attacker = &g_entities[ ENTITYNUM_WORLD ]; } // don't handle ET_MOVER w/o die or pain function if ( target->s.eType == ET_MOVER && !( target->die || target->pain ) ) { // special case for ET_MOVER with act function in initial position if ( ( target->moverState == MOVER_POS1 || target->moverState == ROTATOR_POS1 ) && target->act ) { target->act( target, inflictor, attacker ); } return; } // do knockback against clients if ( client && !( damageFlags & DAMAGE_NO_KNOCKBACK ) && dir ) { // scale knockback by weapon if ( inflictor->s.weapon != WP_NONE ) { knockback = ( int )( ( float )damage * BG_Weapon( inflictor->s.weapon )->knockbackScale ); } else { knockback = damage; } // apply generic damage to knockback modifier knockback *= DAMAGE_TO_KNOCKBACK; // HACK: Too much knockback from falling makes you bounce and looks silly if ( mod == MOD_FALLING ) { knockback = MIN( knockback, MAX_FALLDMG_KNOCKBACK ); } G_KnockbackByDir( target, dir, knockback, qfalse ); } else { // damage knockback gets saved, so initialize it here knockback = 0; } // godmode prevents damage if ( target->flags & FL_GODMODE ) { return; } // check for protection if ( !( damageFlags & DAMAGE_NO_PROTECTION ) ) { // check for protection from friendly damage if ( target != attacker && G_OnSameTeam( target, attacker ) ) { // check if friendly fire has been disabled if ( !g_friendlyFire.integer ) { return; } // don't do friendly damage on movement attacks switch ( mod ) { case MOD_LEVEL3_POUNCE: case MOD_LEVEL4_TRAMPLE: return; default: break; } // if dretchpunt is enabled and this is a dretch, do dretchpunt instead of damage if ( g_dretchPunt.integer && target->client && ( target->client->ps.stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL0 || target->client->ps.stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL0_UPG ) ) { vec3_t dir, push; VectorSubtract( target->r.currentOrigin, attacker->r.currentOrigin, dir ); VectorNormalizeFast( dir ); VectorScale( dir, ( damage * 10.0f ), push ); push[ 2 ] = 64.0f; VectorAdd( target->client->ps.velocity, push, target->client->ps.velocity ); return; } } // for buildables, never protect from damage dealt by building actions if ( target->s.eType == ET_BUILDABLE && attacker->client && mod != MOD_DECONSTRUCT && mod != MOD_SUICIDE && mod != MOD_REPLACE && mod != MOD_NOCREEP ) { // check for protection from friendly buildable damage if ( G_OnSameTeam( target, attacker ) && !g_friendlyBuildableFire.integer ) { return; } } } // update combat timers if ( target->client && attacker->client && target != attacker ) { target->client->lastCombatTime = level.time; attacker->client->lastCombatTime = level.time; } if ( client ) { // save damage (w/o armor modifier), knockback client->damage_received += damage; client->damage_knockback += knockback; // save damage direction if ( dir ) { VectorCopy( dir, client->damage_from ); client->damage_fromWorld = qfalse; } else { VectorCopy( target->r.currentOrigin, client->damage_from ); client->damage_fromWorld = qtrue; } // drain jetpack fuel client->ps.stats[ STAT_FUEL ] -= damage * JETPACK_FUEL_PER_DMG; if ( client->ps.stats[ STAT_FUEL ] < 0 ) { client->ps.stats[ STAT_FUEL ] = 0; } // apply damage modifier modifier = CalcDamageModifier( point, target, (class_t) client->ps.stats[ STAT_CLASS ], damageFlags ); take = ( int )( ( float )damage * modifier + 0.5f ); // if boosted poison every attack if ( attacker->client && ( attacker->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) && target->client->pers.team == TEAM_HUMANS && target->client->poisonImmunityTime < level.time ) { switch ( mod ) { case MOD_POISON: case MOD_LEVEL1_PCLOUD: case MOD_LEVEL2_ZAP: break; default: target->client->ps.stats[ STAT_STATE ] |= SS_POISONED; target->client->lastPoisonTime = level.time; target->client->lastPoisonClient = attacker; } } } else { take = damage; } // make sure damage is done if ( take < 1 ) { take = 1; } if ( g_debugDamage.integer > 0 ) { G_Printf( "G_Damage: %3i (%3i → %3i)\n", take, target->health, target->health - take ); } // do the damage target->health = target->health - take; if ( target->client ) { target->client->ps.stats[ STAT_HEALTH ] = target->health; target->client->pers.infoChangeTime = level.time; // ? } target->lastDamageTime = level.time; // TODO: gentity_t->nextRegenTime only affects alien clients, remove it and use lastDamageTime // Optionally (if needed for some reason), move into client struct and add "Alien" to name target->nextRegenTime = level.time + ALIEN_CLIENT_REGEN_WAIT; // handle non-self damage if ( attacker != target ) { if ( target->health < 0 ) { loss = ( take + target->health ); } else { loss = take; } if ( attacker->client ) { // add to the attacker's account on the target target->credits[ attacker->client->ps.clientNum ] += ( float )loss; // notify the attacker of a hit NotifyClientOfHit( attacker ); } // update buildable stats if ( attacker->s.eType == ET_BUILDABLE && attacker->health > 0 ) { attacker->buildableStatsTotal += loss; } } // handle dying target if ( target->health <= 0 ) { // set no knockback flag for clients if ( client ) { target->flags |= FL_NO_KNOCKBACK; } // cap negative health if ( target->health < -999 ) { target->health = -999; } // call die function if ( target->die ) { target->die( target, inflictor, attacker, mod ); } // update buildable stats if ( attacker->s.eType == ET_BUILDABLE && attacker->health > 0 ) { attacker->buildableStatsCount++; } // for non-client victims, fire ON_DIE event if( !target->client ) { G_EventFireEntity( target, attacker, ON_DIE ); } return; } else if ( target->pain ) { target->pain( target, attacker, take ); } }
/* =========== ClientSpawn Called every time a client is placed fresh in the world: after the first ClientBegin, and after each respawn and evolve Initializes all non-persistent parts of playerState ============ */ void ClientSpawn( gentity_t *ent, gentity_t *spawn, const vec3_t origin, const vec3_t angles ) { int index; vec3_t spawn_origin, spawn_angles; gclient_t *client; int i; clientPersistant_t saved; clientSession_t savedSess; bool savedNoclip, savedCliprcontents; int persistant[ MAX_PERSISTANT ]; gentity_t *spawnPoint = nullptr; int flags; int savedPing; int teamLocal; int eventSequence; char userinfo[ MAX_INFO_STRING ]; vec3_t up = { 0.0f, 0.0f, 1.0f }; int maxAmmo, maxClips; weapon_t weapon; ClientSpawnCBSE(ent, ent == spawn); index = ent - g_entities; client = ent->client; teamLocal = client->pers.team; //if client is dead and following teammate, stop following before spawning if ( client->sess.spectatorClient != -1 ) { client->sess.spectatorClient = -1; client->sess.spectatorState = SPECTATOR_FREE; } // only start client if chosen a class and joined a team if ( client->pers.classSelection == PCL_NONE && teamLocal == TEAM_NONE ) { client->sess.spectatorState = SPECTATOR_FREE; } else if ( client->pers.classSelection == PCL_NONE ) { client->sess.spectatorState = SPECTATOR_LOCKED; } // if client is dead and following teammate, stop following before spawning if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { G_StopFollowing( ent ); } if ( origin != nullptr ) { VectorCopy( origin, spawn_origin ); } if ( angles != nullptr ) { VectorCopy( angles, spawn_angles ); } // find a spawn point // do it before setting health back up, so farthest // ranging doesn't count this client if ( client->sess.spectatorState != SPECTATOR_NOT ) { if ( teamLocal == TEAM_NONE ) { spawnPoint = G_SelectSpectatorSpawnPoint( spawn_origin, spawn_angles ); } else if ( teamLocal == TEAM_ALIENS ) { spawnPoint = G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); } else if ( teamLocal == TEAM_HUMANS ) { spawnPoint = G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); } } else { if ( spawn == nullptr ) { Com_Error(errorParm_t::ERR_DROP, "ClientSpawn: spawn is NULL" ); } spawnPoint = spawn; if ( spawnPoint->s.eType == entityType_t::ET_BUILDABLE ) { G_SetBuildableAnim( spawnPoint, BANIM_SPAWN1, true ); if ( spawnPoint->buildableTeam == TEAM_ALIENS ) { spawnPoint->clientSpawnTime = ALIEN_SPAWN_REPEAT_TIME; } else if ( spawnPoint->buildableTeam == TEAM_HUMANS ) { spawnPoint->clientSpawnTime = HUMAN_SPAWN_REPEAT_TIME; } } } // toggle the teleport bit so the client knows to not lerp flags = ( ent->client->ps.eFlags & EF_TELEPORT_BIT ) ^ EF_TELEPORT_BIT; G_UnlaggedClear( ent ); // clear everything but the persistent data saved = client->pers; savedSess = client->sess; savedPing = client->ps.ping; savedNoclip = client->noclip; savedCliprcontents = client->cliprcontents; 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->noclip = savedNoclip; client->cliprcontents = savedCliprcontents; 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_SPECSTATE ] = client->sess.spectatorState; client->airOutTime = level.time + 12000; trap_GetUserinfo( index, userinfo, sizeof( userinfo ) ); client->ps.eFlags = flags; //Log::Notice( "ent->client->pers->pclass = %i\n", ent->client->pers.classSelection ); ent->s.groundEntityNum = ENTITYNUM_NONE; ent->client = &level.clients[ index ]; ent->classname = S_PLAYER_CLASSNAME; if ( client->noclip ) { client->cliprcontents = CONTENTS_BODY; } else { ent->r.contents = CONTENTS_BODY; } ent->clipmask = MASK_PLAYERSOLID; ent->die = G_PlayerDie; ent->waterlevel = 0; ent->watertype = 0; ent->flags &= FL_GODMODE | FL_NOTARGET; // calculate each client's acceleration ent->evaluateAcceleration = true; client->ps.stats[ STAT_MISC ] = 0; client->ps.eFlags = flags; client->ps.clientNum = index; BG_ClassBoundingBox( ent->client->pers.classSelection, ent->r.mins, ent->r.maxs, nullptr, nullptr, nullptr ); // clear entity values if ( ent->client->pers.classSelection == PCL_HUMAN_NAKED ) { BG_AddUpgradeToInventory( UP_MEDKIT, client->ps.stats ); weapon = client->pers.humanItemSelection; } else if ( client->sess.spectatorState == SPECTATOR_NOT ) { weapon = BG_Class( ent->client->pers.classSelection )->startWeapon; } else { weapon = WP_NONE; } maxAmmo = BG_Weapon( weapon )->maxAmmo; maxClips = BG_Weapon( weapon )->maxClips; client->ps.stats[ STAT_WEAPON ] = weapon; client->ps.ammo = maxAmmo; client->ps.clips = maxClips; // We just spawned, not changing weapons client->ps.persistant[ PERS_NEWWEAPON ] = 0; client->ps.persistant[ PERS_TEAM ] = client->pers.team; // TODO: Check whether stats can be cleared at once instead of per field client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX; client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_MAX; client->ps.stats[ STAT_CLASS ] = ent->client->pers.classSelection; client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; client->ps.stats[ STAT_PREDICTION ] = 0; client->ps.stats[ STAT_STATE ] = 0; VectorSet( client->ps.grapplePoint, 0.0f, 0.0f, 1.0f ); //clear the credits array // TODO: Handle in HealthComponent or ClientComponent. for ( i = 0; i < MAX_CLIENTS; i++ ) { ent->credits[ i ].value = 0.0f; ent->credits[ i ].time = 0; ent->credits[ i ].team = TEAM_NONE; } G_SetOrigin( ent, spawn_origin ); VectorCopy( spawn_origin, client->ps.origin ); //give aliens some spawn velocity if ( client->sess.spectatorState == SPECTATOR_NOT && client->pers.team == TEAM_ALIENS ) { if ( ent == spawn ) { //evolution particle system G_AddPredictableEvent( ent, EV_ALIEN_EVOLVE, DirToByte( up ) ); } else { spawn_angles[ YAW ] += 180.0f; AngleNormalize360( spawn_angles[ YAW ] ); if ( spawnPoint->s.origin2[ 2 ] > 0.0f ) { vec3_t forward, dir; AngleVectors( spawn_angles, forward, nullptr, nullptr ); VectorAdd( spawnPoint->s.origin2, forward, dir ); VectorNormalize( dir ); VectorScale( dir, BG_Class( ent->client->pers.classSelection )->jumpMagnitude, client->ps.velocity ); } G_AddPredictableEvent( ent, EV_PLAYER_RESPAWN, 0 ); } } else if ( client->sess.spectatorState == SPECTATOR_NOT && client->pers.team == TEAM_HUMANS ) { spawn_angles[ YAW ] += 180.0f; AngleNormalize360( spawn_angles[ YAW ] ); } // 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 ); G_SetClientViewAngle( ent, spawn_angles ); if ( client->sess.spectatorState == SPECTATOR_NOT ) { trap_LinkEntity( ent ); // force the base weapon up if ( client->pers.team == TEAM_HUMANS ) { G_ForceWeaponChange( ent, weapon ); } client->ps.weaponstate = WEAPON_READY; } // don't allow full run speed for a bit client->ps.pm_flags |= PMF_TIME_KNOCKBACK; client->ps.pm_time = 100; client->respawnTime = level.time; ent->nextRegenTime = level.time; client->inactivityTime = level.time + g_inactivity.integer * 1000; usercmdClearButtons( client->latched_buttons ); // 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 if ( !spawn && spawnPoint ) { G_EventFireEntity( spawnPoint, ent, ON_SPAWN ); } // select the highest weapon number available, after any // spawn given items have fired client->ps.weapon = 1; for ( i = WP_NUM_WEAPONS - 1; i > 0; i-- ) { if ( BG_InventoryContainsWeapon( i, client->ps.stats ) ) { client->ps.weapon = i; break; } } } // run a client frame to drop exactly to the floor, // initialize animations and other things client->ps.commandTime = level.time - 100; ent->client->pers.cmd.serverTime = level.time; ClientThink( ent - g_entities ); // positively link the client, even if the command times are weird if ( client->sess.spectatorState == SPECTATOR_NOT ) { BG_PlayerStateToEntityState( &client->ps, &ent->s, true ); VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); trap_LinkEntity( ent ); } // must do this here so the number of active clients is calculated CalculateRanks(); // run the presend to set anything else ClientEndFrame( ent ); // clear entity state values BG_PlayerStateToEntityState( &client->ps, &ent->s, true ); client->pers.infoChangeTime = level.time; // (re)tag the client for its team Beacon::DeleteTags( ent ); Beacon::Tag( ent, (team_t)ent->client->ps.persistant[ PERS_TEAM ], true ); }