Index: src/game/g_local.h =================================================================== --- src/game/g_local.h (revision 935) +++ src/game/g_local.h (working copy) @@ -343,9 +343,10 @@ int nameChangeTime; int nameChanges; - // used to save persistant[] values while in SPECTATOR_FOLLOW mode - int savedScore; - int savedCredit; + // used to save playerState_t values while in SPECTATOR_FOLLOW mode + int score; + int credit; + int ping; vec3_t lastDeathLocation; char guid[ 33 ]; @@ -542,6 +543,7 @@ int framenum; int time; // in msec int previousTime; // so movers can back up when blocked + int frameMsec; // trap_Milliseconds() at end frame int startTime; // level.time the map was started @@ -673,6 +675,7 @@ // g_cmds.c // void Cmd_Score_f( gentity_t *ent ); +void G_StopFromFollowing( gentity_t *ent ); void G_StopFollowing( gentity_t *ent ); qboolean G_FollowNewClient( gentity_t *ent, int dir ); void Cmd_Follow_f( gentity_t *ent, qboolean toggle ); @@ -888,9 +891,11 @@ // void G_AddCreditToClient( gclient_t *client, short credit, qboolean cap ); team_t TeamCount( int ignoreClientNum, int team ); -void SetClientViewAngle( gentity_t *ent, vec3_t angle ); -gentity_t *SelectTremulousSpawnPoint( pTeam_t team, vec3_t preference, vec3_t origin, vec3_t angles ); -gentity_t *SelectSpawnPoint( vec3_t avoidPoint, vec3_t origin, vec3_t angles ); +void G_SetClientViewAngle( gentity_t *ent, vec3_t angle ); +gentity_t *G_SelectTremulousSpawnPoint( pTeam_t team, vec3_t preference, vec3_t origin, vec3_t angles ); +gentity_t *G_SelectSpawnPoint( vec3_t avoidPoint, vec3_t origin, vec3_t angles ); +gentity_t *G_SelectAlienLockSpawnPoint( vec3_t origin, vec3_t angles ); +gentity_t *G_SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ); void SpawnCorpse( gentity_t *ent ); void respawn( gentity_t *ent ); void BeginIntermission( void ); Index: src/game/g_combat.c =================================================================== --- src/game/g_combat.c (revision 935) +++ src/game/g_combat.c (working copy) @@ -147,16 +147,7 @@ return; // stop any following clients - for( i = 0; i < level.maxclients; i++ ) - { - if( level.clients[ i ].sess.sessionTeam == TEAM_SPECTATOR && - level.clients[ i ].sess.spectatorState == SPECTATOR_FOLLOW && - level.clients[ i ].sess.spectatorClient == self->client->ps.clientNum ) - { - if( !G_FollowNewClient( &g_entities[ i ], 1 ) ) - G_StopFollowing( &g_entities[ i ] ); - } - } + G_StopFromFollowing( self ); self->client->ps.pm_type = PM_DEAD; self->suicideTime = 0; Index: src/game/g_active.c =================================================================== --- src/game/g_active.c (revision 935) +++ src/game/g_active.c (working copy) @@ -375,12 +375,18 @@ { pmove_t pm; gclient_t *client; + qboolean attack1, attack3; client = ent->client; client->oldbuttons = client->buttons; client->buttons = ucmd->buttons; + attack1 = ( ( client->buttons & BUTTON_ATTACK ) && + !( client->oldbuttons & BUTTON_ATTACK ) ); + attack3 = ( ( client->buttons & BUTTON_USE_HOLDABLE ) && + !( client->oldbuttons & BUTTON_USE_HOLDABLE ) ); + if( client->sess.spectatorState != SPECTATOR_FOLLOW ) { if( client->sess.spectatorState == SPECTATOR_LOCKED ) @@ -413,29 +419,26 @@ G_TouchTriggers( ent ); trap_UnlinkEntity( ent ); - if( ( client->buttons & BUTTON_ATTACK ) && !( client->oldbuttons & BUTTON_ATTACK ) ) + if( ( attack1 || attack3 ) && ( client->ps.pm_flags & PMF_QUEUED ) ) { - //if waiting in a queue remove from the queue - if( client->ps.pm_flags & PMF_QUEUED ) - { - if( client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) - G_RemoveFromSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); - else if( client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) - G_RemoveFromSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + if( client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + G_RemoveFromSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + else if( client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_RemoveFromSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); - client->pers.classSelection = PCL_NONE; - client->ps.stats[ STAT_PCLASS ] = PCL_NONE; - } - else if( client->pers.classSelection == PCL_NONE ) - { - if( client->pers.teamSelection == PTE_NONE ) - G_TriggerMenu( client->ps.clientNum, MN_TEAM ); - else if( client->pers.teamSelection == PTE_ALIENS ) - G_TriggerMenu( client->ps.clientNum, MN_A_CLASS ); - else if( client->pers.teamSelection == PTE_HUMANS ) - G_TriggerMenu( client->ps.clientNum, MN_H_SPAWN ); - } + client->pers.classSelection = PCL_NONE; + client->ps.stats[ STAT_PCLASS ] = PCL_NONE; } + + if( attack1 && client->pers.classSelection == PCL_NONE ) + { + if( client->pers.teamSelection == PTE_NONE ) + G_TriggerMenu( client->ps.clientNum, MN_TEAM ); + else if( client->pers.teamSelection == PTE_ALIENS ) + G_TriggerMenu( client->ps.clientNum, MN_A_CLASS ); + else if( client->pers.teamSelection == PTE_HUMANS ) + G_TriggerMenu( client->ps.clientNum, MN_H_SPAWN ); + } //set the queue position for the client side if( client->ps.pm_flags & PMF_QUEUED ) @@ -452,9 +455,22 @@ } } } - - if( ( client->buttons & BUTTON_USE_HOLDABLE ) && !( client->oldbuttons & BUTTON_USE_HOLDABLE ) ) + else if( attack1 && ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + { + G_StopFollowing( ent ); + client->pers.classSelection = PCL_NONE; + if( client->pers.teamSelection == PTE_NONE ) + G_TriggerMenu( ent-g_entities, MN_TEAM ); + else if( client->pers.teamSelection == PTE_ALIENS ) + G_TriggerMenu( ent-g_entities, MN_A_CLASS ); + else if( client->pers.teamSelection == PTE_HUMANS ) + G_TriggerMenu( ent-g_entities, MN_H_SPAWN ); + } + + if( attack3 ) + { Cmd_Follow_f( ent, qtrue ); + } } @@ -1297,6 +1313,19 @@ // G_Printf("serverTime >>>>>\n" ); } + // ucmd->serverTime is a client predicted value, but it works for making a + // replacement for client->ps.ping when in SPECTATOR_FOLLOW + client->pers.ping = level.time - ucmd->serverTime; + + // account for the one frame of delay on client side + client->pers.ping -= level.time - level.previousTime; + + // account for the time that's elapsed since the last ClientEndFrame() + client->pers.ping += trap_Milliseconds( ) - level.frameMsec; + + if( client->pers.ping < 0 ) + client->pers.ping = 0; + msec = ucmd->serverTime - client->ps.commandTime; // following others may result in bad times, but we still want // to check for follow toggles @@ -1802,6 +1831,10 @@ pers = &ent->client->pers; + // save a copy of certain playerState values in case of SPECTATOR_FOLLOW + pers->score = ent->client->ps.persistant[ PERS_SCORE ]; + pers->credit = ent->client->ps.persistant[ PERS_CREDIT ]; + // // If the end of unit layout is displayed, don't give // the player any normal movement attributes Index: src/game/g_buildable.c =================================================================== --- src/game/g_buildable.c (revision 935) +++ src/game/g_buildable.c (working copy) @@ -1157,7 +1157,7 @@ G_SetOrigin( player, origin ); VectorCopy( origin, player->client->ps.origin ); VectorCopy( vec3_origin, player->client->ps.velocity ); - SetClientViewAngle( player, angles ); + G_SetClientViewAngle( player, angles ); } if( tr.fraction < 1.0f ) @@ -1237,7 +1237,7 @@ G_SetOrigin( activator, hovelOrigin ); VectorCopy( hovelOrigin, activator->client->ps.origin ); - SetClientViewAngle( activator, hovelAngles ); + G_SetClientViewAngle( activator, hovelAngles ); } } } @@ -1312,7 +1312,7 @@ G_SetOrigin( builder, newOrigin ); VectorCopy( newOrigin, builder->client->ps.origin ); - SetClientViewAngle( builder, newAngles ); + G_SetClientViewAngle( builder, newAngles ); //client leaves hovel builder->client->ps.stats[ STAT_STATE ] &= ~SS_HOVELING; Index: src/game/g_main.c =================================================================== --- src/game/g_main.c (revision 935) +++ src/game/g_main.c (working copy) @@ -969,7 +969,7 @@ clientNum = G_PeekSpawnQueue( sq ); ent = &g_entities[ clientNum ]; - if( ( spawn = SelectTremulousSpawnPoint( team, + if( ( spawn = G_SelectTremulousSpawnPoint( team, ent->client->pers.lastDeathLocation, spawn_origin, spawn_angles ) ) ) { @@ -1451,7 +1451,7 @@ if( !ent ) { // the map creator forgot to put in an intermission point... - SelectSpawnPoint( vec3_origin, level.intermission_origin, level.intermission_angle ); + G_SelectSpawnPoint( vec3_origin, level.intermission_origin, level.intermission_angle ); } else { @@ -2295,5 +2295,7 @@ trap_Cvar_Set( "g_listEntity", "0" ); } + + level.frameMsec = trap_Milliseconds( ); } Index: src/game/g_misc.c =================================================================== --- src/game/g_misc.c (revision 935) +++ src/game/g_misc.c (working copy) @@ -89,7 +89,7 @@ G_UnlaggedClear( player ); // set angles - SetClientViewAngle( player, angles ); + G_SetClientViewAngle( player, angles ); // kill anything at the destination if( player->client->sess.sessionTeam != TEAM_SPECTATOR ) Index: src/game/g_client.c =================================================================== --- src/game/g_client.c (revision 935) +++ src/game/g_client.c (working copy) @@ -124,13 +124,17 @@ if( client->ps.persistant[ PERS_CREDIT ] < 0 ) client->ps.persistant[ PERS_CREDIT ] = 0; + + // if not a following spec, keep pers.credit in sync for next ClientSpawn() + if( client->sess.sessionTeam != TEAM_SPECTATOR ) + client->pers.credit = client->ps.persistant[ PERS_CREDIT ]; } /* ======================================================================= - SelectSpawnPoint + G_SelectSpawnPoint ======================================================================= */ @@ -165,13 +169,13 @@ /* ================ -SelectNearestDeathmatchSpawnPoint +G_SelectNearestDeathmatchSpawnPoint Find the spot that we DON'T want to use ================ */ #define MAX_SPAWN_POINTS 128 -gentity_t *SelectNearestDeathmatchSpawnPoint( vec3_t from ) +gentity_t *G_SelectNearestDeathmatchSpawnPoint( vec3_t from ) { gentity_t *spot; vec3_t delta; @@ -200,13 +204,13 @@ /* ================ -SelectRandomDeathmatchSpawnPoint +G_SelectRandomDeathmatchSpawnPoint go to a random point that doesn't telefrag ================ */ #define MAX_SPAWN_POINTS 128 -gentity_t *SelectRandomDeathmatchSpawnPoint( void ) +gentity_t *G_SelectRandomDeathmatchSpawnPoint( void ) { gentity_t *spot; int count; @@ -235,12 +239,12 @@ /* =========== -SelectRandomFurthestSpawnPoint +G_SelectRandomFurthestSpawnPoint Chooses a player start, deathmatch start, etc ============ */ -gentity_t *SelectRandomFurthestSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) +gentity_t *G_SelectRandomFurthestSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) { gentity_t *spot; vec3_t delta; @@ -318,12 +322,12 @@ /* ================ -SelectAlienSpawnPoint +G_SelectAlienSpawnPoint go to a random point that doesn't telefrag ================ */ -gentity_t *SelectAlienSpawnPoint( vec3_t preference ) +gentity_t *G_SelectAlienSpawnPoint( vec3_t preference ) { gentity_t *spot; int count; @@ -367,12 +371,12 @@ /* ================ -SelectHumanSpawnPoint +G_SelectHumanSpawnPoint go to a random point that doesn't telefrag ================ */ -gentity_t *SelectHumanSpawnPoint( vec3_t preference ) +gentity_t *G_SelectHumanSpawnPoint( vec3_t preference ) { gentity_t *spot; int count; @@ -416,32 +420,32 @@ /* =========== -SelectSpawnPoint +G_SelectSpawnPoint Chooses a player start, deathmatch start, etc ============ */ -gentity_t *SelectSpawnPoint( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) +gentity_t *G_SelectSpawnPoint( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) { - return SelectRandomFurthestSpawnPoint( avoidPoint, origin, angles ); + return G_SelectRandomFurthestSpawnPoint( avoidPoint, origin, angles ); } /* =========== -SelectTremulousSpawnPoint +G_SelectTremulousSpawnPoint Chooses a player start, deathmatch start, etc ============ */ -gentity_t *SelectTremulousSpawnPoint( pTeam_t team, vec3_t preference, vec3_t origin, vec3_t angles ) +gentity_t *G_SelectTremulousSpawnPoint( pTeam_t team, vec3_t preference, vec3_t origin, vec3_t angles ) { gentity_t *spot = NULL; if( team == PTE_ALIENS ) - spot = SelectAlienSpawnPoint( preference ); + spot = G_SelectAlienSpawnPoint( preference ); else if( team == PTE_HUMANS ) - spot = SelectHumanSpawnPoint( preference ); + spot = G_SelectHumanSpawnPoint( preference ); //no available spots if( !spot ) @@ -462,13 +466,13 @@ /* =========== -SelectInitialSpawnPoint +G_SelectInitialSpawnPoint Try to find a spawn point marked 'initial', otherwise use normal spawn selection. ============ */ -gentity_t *SelectInitialSpawnPoint( vec3_t origin, vec3_t angles ) +gentity_t *G_SelectInitialSpawnPoint( vec3_t origin, vec3_t angles ) { gentity_t *spot; @@ -481,7 +485,7 @@ if( !spot || SpotWouldTelefrag( spot ) ) { - return SelectSpawnPoint( vec3_origin, origin, angles ); + return G_SelectSpawnPoint( vec3_origin, origin, angles ); } VectorCopy( spot->s.origin, origin ); @@ -493,11 +497,11 @@ /* =========== -SelectSpectatorSpawnPoint +G_SelectSpectatorSpawnPoint ============ */ -gentity_t *SelectSpectatorSpawnPoint( vec3_t origin, vec3_t angles ) +gentity_t *G_SelectSpectatorSpawnPoint( vec3_t origin, vec3_t angles ) { FindIntermissionPoint( ); @@ -510,13 +514,13 @@ /* =========== -SelectAlienLockSpawnPoint +G_SelectAlienLockSpawnPoint Try to find a spawn point for alien intermission otherwise use normal intermission spawn. ============ */ -gentity_t *SelectAlienLockSpawnPoint( vec3_t origin, vec3_t angles ) +gentity_t *G_SelectAlienLockSpawnPoint( vec3_t origin, vec3_t angles ) { gentity_t *spot; @@ -524,7 +528,7 @@ spot = G_Find( spot, FOFS( classname ), "info_alien_intermission" ); if( !spot ) - return SelectSpectatorSpawnPoint( origin, angles ); + return G_SelectSpectatorSpawnPoint( origin, angles ); VectorCopy( spot->s.origin, origin ); VectorCopy( spot->s.angles, angles ); @@ -535,13 +539,13 @@ /* =========== -SelectHumanLockSpawnPoint +G_SelectHumanLockSpawnPoint Try to find a spawn point for human intermission otherwise use normal intermission spawn. ============ */ -gentity_t *SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ) +gentity_t *G_SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ) { gentity_t *spot; @@ -549,7 +553,7 @@ spot = G_Find( spot, FOFS( classname ), "info_human_intermission" ); if( !spot ) - return SelectSpectatorSpawnPoint( origin, angles ); + return G_SelectSpectatorSpawnPoint( origin, angles ); VectorCopy( spot->s.origin, origin ); VectorCopy( spot->s.angles, angles ); @@ -731,11 +735,11 @@ /* ================== -SetClientViewAngle +G_SetClientViewAngle ================== */ -void SetClientViewAngle( gentity_t *ent, vec3_t angle ) +void G_SetClientViewAngle( gentity_t *ent, vec3_t angle ) { int i; @@ -1132,10 +1136,7 @@ strcpy( c1, Info_ValueForKey( userinfo, "color1" ) ); strcpy( c2, Info_ValueForKey( userinfo, "color2" ) ); - if( client->ps.pm_flags & PMF_FOLLOW ) - team = PTE_NONE; - else - team = client->ps.stats[ STAT_PTEAM ]; + team = client->pers.teamSelection; // send over a subset of the userinfo keys so other clients can // print scoreboards, display models, and play custom sounds @@ -1295,6 +1296,7 @@ client->pers.connected = CON_CONNECTED; client->pers.enterTime = level.time; client->pers.teamState.state = TEAM_BEGIN; + client->pers.classSelection = PCL_NONE; // save eflags around this, because changing teams will // cause this to happen with a valid entity, and we @@ -1382,11 +1384,11 @@ if( client->sess.sessionTeam == TEAM_SPECTATOR ) { if( teamLocal == PTE_NONE ) - spawnPoint = SelectSpectatorSpawnPoint( spawn_origin, spawn_angles ); + spawnPoint = G_SelectSpectatorSpawnPoint( spawn_origin, spawn_angles ); else if( teamLocal == PTE_ALIENS ) - spawnPoint = SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + spawnPoint = G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); else if( teamLocal == PTE_HUMANS ) - spawnPoint = SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + spawnPoint = G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); } else { @@ -1442,6 +1444,10 @@ client->ps.persistant[ PERS_SPAWN_COUNT ]++; client->ps.persistant[ PERS_TEAM ] = client->sess.sessionTeam; + // restore really persistant things + client->ps.persistant[ PERS_SCORE ] = client->pers.score; + client->ps.persistant[ PERS_CREDIT ] = client->pers.credit; + client->airOutTime = level.time + 12000; trap_GetUserinfo( index, userinfo, sizeof( userinfo ) ); @@ -1564,7 +1570,7 @@ client->ps.pm_flags |= PMF_RESPAWNED; trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd ); - SetClientViewAngle( ent, spawn_angles ); + G_SetClientViewAngle( ent, spawn_angles ); if( !( client->sess.sessionTeam == TEAM_SPECTATOR ) ) { Index: src/game/g_cmds.c =================================================================== --- src/game/g_cmds.c (revision 935) +++ src/game/g_cmds.c (working copy) @@ -263,10 +263,12 @@ if( cl->pers.connected == CON_CONNECTING ) ping = -1; + else if( cl->sess.spectatorState == SPECTATOR_FOLLOW ) + ping = cl->pers.ping < 999 ? cl->pers.ping : 999; else ping = cl->ps.ping < 999 ? cl->ps.ping : 999; - if( cl->ps.stats[ STAT_HEALTH ] > 0 ) + if( cl->sess.sessionTeam != TEAM_SPECTATOR ) { weapon = cl->ps.weapon; @@ -290,8 +292,8 @@ } Com_sprintf( entry, sizeof( entry ), - " %d %d %d %d %d %d", level.sortedClients[ i ], cl->ps.persistant[ PERS_SCORE ], - ping, ( level.time - cl->pers.enterTime ) / 60000, weapon, upgrade ); + " %d %d %d %d %d %d", level.sortedClients[ i ], cl->pers.score, ping, + ( level.time - cl->pers.enterTime ) / 60000, weapon, upgrade ); j = strlen( entry ); @@ -596,41 +598,20 @@ ( ( oldTeam == PTE_HUMANS || oldTeam == PTE_ALIENS ) && ( level.time - ent->client->pers.teamChangeTime ) > 60000 ) ) { - if( oldTeam == PTE_NONE ) - { - // ps.persistant[] from a spectator cannot be trusted - ent->client->ps.persistant[ PERS_SCORE ] = ent->client->pers.savedScore; - ent->client->ps.persistant[ PERS_CREDIT ] = ent->client->pers.savedCredit; - } - else if( oldTeam == PTE_ALIENS ) - { - // always save in human credtis - ent->client->ps.persistant[ PERS_CREDIT ] *= - (float)FREEKILL_HUMAN / FREEKILL_ALIEN; - } - - if( newTeam == PTE_NONE ) - { - // save values before the client enters the spectator team and their - // ps.persistant[] values become trashed - ent->client->pers.savedScore = ent->client->ps.persistant[ PERS_SCORE ]; - ent->client->pers.savedCredit = ent->client->ps.persistant[ PERS_CREDIT ]; - } + if( oldTeam == PTE_ALIENS ) + ent->client->pers.credit *= (float)FREEKILL_HUMAN / FREEKILL_ALIEN; else if( newTeam == PTE_ALIENS ) - { - // convert to alien currency - ent->client->ps.persistant[ PERS_CREDIT ] *= - (float)FREEKILL_ALIEN / FREEKILL_HUMAN; - } + ent->client->pers.credit *= (float)FREEKILL_ALIEN / FREEKILL_HUMAN; } else { - ent->client->ps.persistant[ PERS_CREDIT ] = 0; - ent->client->ps.persistant[ PERS_SCORE ] = 0; - ent->client->pers.savedScore = 0; - ent->client->pers.savedCredit = 0; + ent->client->pers.credit = 0; + ent->client->pers.score = 0; } + // stop any following clients + G_StopFromFollowing( ent ); + ent->client->pers.classSelection = PCL_NONE; ClientSpawn( ent, NULL, NULL, NULL ); @@ -748,7 +729,7 @@ return; //guard against build timer exploit - if( oldteam != PTE_NONE && + if( oldteam != PTE_NONE && ent->client->sess.sessionTeam != TEAM_SPECTATOR && ( ent->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0 || ent->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG || BG_InventoryContainsWeapon( WP_HBUILD, ent->client->ps.stats ) || @@ -1639,7 +1620,7 @@ vec3_t infestOrigin; int allowedClasses[ PCL_NUM_CLASSES ]; int numClasses = 0; - pClass_t currentClass = ent->client->ps.stats[ STAT_PCLASS ]; + pClass_t currentClass = ent->client->pers.classSelection; pClass_t newClass; int numLevels; int entityList[ MAX_GENTITIES ]; @@ -1648,8 +1629,12 @@ int num; gentity_t *other; - if( ent->client->ps.stats[ STAT_HEALTH ] <= 0 ) + if( ent->health <= 0 && !ent->client->sess.sessionTeam != TEAM_SPECTATOR ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You cannot use the class cmd when dead unless spec\n\"" ) ); return; + } clientNum = ent->client - level.clients; trap_Argv( 1, s, sizeof( s ) ); @@ -1709,7 +1694,8 @@ } //guard against selling the HBUILD weapons exploit - if( ( currentClass == PCL_ALIEN_BUILDER0 || + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR && + ( currentClass == PCL_ALIEN_BUILDER0 || currentClass == PCL_ALIEN_BUILDER0_UPG ) && ent->client->ps.stats[ STAT_MISC ] > 0 ) { @@ -2564,6 +2550,30 @@ /* ================= +G_StopFromFollowing + +stops any other clients from following this one +called when a player leaves a team or dies +================= +*/ +void G_StopFromFollowing( gentity_t *ent ) +{ + int i; + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].sess.sessionTeam == TEAM_SPECTATOR && + level.clients[ i ].sess.spectatorState == SPECTATOR_FOLLOW && + level.clients[ i ].sess.spectatorClient == ent->client->ps.clientNum ) + { + if( !G_FollowNewClient( &g_entities[ i ], 1 ) ) + G_StopFollowing( &g_entities[ i ] ); + } + } +} + +/* +================= G_StopFollowing If the client being followed leaves the game, or you just want to drop @@ -2574,10 +2584,27 @@ { ent->client->ps.persistant[ PERS_TEAM ] = TEAM_SPECTATOR; ent->client->sess.sessionTeam = TEAM_SPECTATOR; - ent->client->sess.spectatorState = SPECTATOR_FREE; + ent->client->ps.stats[ STAT_PTEAM ] = ent->client->pers.teamSelection; + + if( ent->client->pers.teamSelection == PTE_NONE ) + { + ent->client->sess.spectatorState = SPECTATOR_FREE; + } + else + { + vec3_t spawn_origin, spawn_angles; + + ent->client->sess.spectatorState = SPECTATOR_LOCKED; + if( ent->client->pers.teamSelection == PTE_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( ent->client->pers.teamSelection == PTE_HUMANS ) + G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, ent->client->ps.origin ); + G_SetClientViewAngle( ent, spawn_angles ); + } ent->client->sess.spectatorClient = -1; ent->client->ps.pm_flags &= ~PMF_FOLLOW; - ent->client->ps.stats[ STAT_PTEAM ] = PTE_NONE; ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBINGCEILING; @@ -2645,6 +2672,14 @@ if( level.clients[ clientnum ].sess.sessionTeam == TEAM_SPECTATOR ) continue; + // can only follow teammates when dead + if( ent->client->pers.teamSelection != PTE_NONE && + ( level.clients[ clientnum ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + { + continue; + } + // this is good, we can use it ent->client->sess.spectatorClient = clientnum; ent->client->sess.spectatorState = SPECTATOR_FOLLOW; @@ -2666,15 +2701,17 @@ int pids[ MAX_CLIENTS ]; char arg[ MAX_TOKEN_CHARS ]; + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) + return; + if( trap_Argc( ) != 2 || toggle ) { if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) G_StopFollowing( ent ); - else if( ent->client->sess.spectatorState == SPECTATOR_FREE ) + else G_FollowNewClient( ent, 1 ); } - else if( ent->client->sess.spectatorState == SPECTATOR_FREE || - ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + else { trap_Argv( 1, arg, sizeof( arg ) ); if( G_ClientNumbersFromString( arg, pids ) == 1 ) @@ -2701,6 +2738,14 @@ if( level.clients[ i ].pers.teamSelection == PTE_NONE ) return; + // can only follow teammates when dead and on a team + if( ent->client->pers.teamSelection != PTE_NONE && + ( level.clients[ i ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + { + return; + } + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; ent->client->sess.spectatorClient = i; } @@ -2714,7 +2759,7 @@ void Cmd_FollowCycle_f( gentity_t *ent, int dir ) { // won't work unless spectating - if( ent->client->pers.teamSelection != PTE_NONE ) + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) return; if( ent->client->sess.spectatorState == SPECTATOR_NOT ) return; @@ -2968,7 +3013,68 @@ // ignore all other commands when at intermission if( level.intermissiontime ) return; + + if( Q_stricmp( cmd, "team" ) == 0 ) + { + Cmd_Team_f( ent ); + return; + } + else if( Q_stricmp( cmd, "class" ) == 0 ) + { + Cmd_Class_f( ent ); + return; + } + else if( Q_stricmp( cmd, "callvote" ) == 0 ) + { + Cmd_CallVote_f( ent ); + return; + } + else if( Q_stricmp( cmd, "vote" ) == 0 ) + { + Cmd_Vote_f( ent ); + return; + } + else if( Q_stricmp( cmd, "callteamvote" ) == 0 ) + { + Cmd_CallTeamVote_f( ent ); + return; + } + else if( Q_stricmp( cmd, "teamvote" ) == 0 ) + { + Cmd_TeamVote_f( ent ); + return; + } + else if( Q_stricmp( cmd, "follow" ) == 0 ) + { + Cmd_Follow_f( ent, qfalse ); + return; + } + else if( Q_stricmp (cmd, "follownext") == 0) + { + Cmd_FollowCycle_f( ent, 1 ); + return; + } + else if( Q_stricmp( cmd, "followprev" ) == 0 ) + { + Cmd_FollowCycle_f( ent, -1 ); + return; + } + else if( Q_stricmp( cmd, "ptrcverify" ) == 0 ) + { + Cmd_PTRCVerify_f( ent ); + return; + } + else if( Q_stricmp( cmd, "ptrcrestore" ) == 0 ) + { + Cmd_PTRCRestore_f( ent ); + return; + } + // ignore all other commands when client is a spectator to prevent issues + // with the borrowed playerState_t when in SPECTATOR_FOLLOW + if( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) + return; + if( Q_stricmp( cmd, "give" ) == 0 ) Cmd_Give_f( ent ); else if( Q_stricmp( cmd, "god" ) == 0 ) @@ -2981,10 +3087,6 @@ Cmd_Kill_f( ent ); else if( Q_stricmp( cmd, "levelshot" ) == 0 ) Cmd_LevelShot_f( ent ); - else if( Q_stricmp( cmd, "team" ) == 0 ) - Cmd_Team_f( ent ); - else if( Q_stricmp( cmd, "class" ) == 0 ) - Cmd_Class_f( ent ); else if( Q_stricmp( cmd, "build" ) == 0 ) Cmd_Build_f( ent ); else if( Q_stricmp( cmd, "buy" ) == 0 ) @@ -3007,26 +3109,8 @@ Cmd_Boost_f( ent ); else if( Q_stricmp( cmd, "where" ) == 0 ) Cmd_Where_f( ent ); - else if( Q_stricmp( cmd, "callvote" ) == 0 ) - Cmd_CallVote_f( ent ); - else if( Q_stricmp( cmd, "vote" ) == 0 ) - Cmd_Vote_f( ent ); - else if( Q_stricmp( cmd, "callteamvote" ) == 0 ) - Cmd_CallTeamVote_f( ent ); - else if( Q_stricmp( cmd, "follow" ) == 0 ) - Cmd_Follow_f( ent, qfalse ); - else if( Q_stricmp (cmd, "follownext") == 0) - Cmd_FollowCycle_f( ent, 1 ); - else if( Q_stricmp( cmd, "followprev" ) == 0 ) - Cmd_FollowCycle_f( ent, -1 ); - else if( Q_stricmp( cmd, "teamvote" ) == 0 ) - Cmd_TeamVote_f( ent ); else if( Q_stricmp( cmd, "setviewpos" ) == 0 ) Cmd_SetViewpos_f( ent ); - else if( Q_stricmp( cmd, "ptrcverify" ) == 0 ) - Cmd_PTRCVerify_f( ent ); - else if( Q_stricmp( cmd, "ptrcrestore" ) == 0 ) - Cmd_PTRCRestore_f( ent ); else if( Q_stricmp( cmd, "test" ) == 0 ) Cmd_Test_f( ent ); else Index: src/cgame/cg_tutorial.c =================================================================== --- src/cgame/cg_tutorial.c (revision 935) +++ src/cgame/cg_tutorial.c (working copy) @@ -533,40 +533,34 @@ */ static void CG_SpectatorText( char *text, playerState_t *ps ) { - if( ps->pm_flags & PMF_FOLLOW ) + if( cgs.clientinfo[ cg.clientNum ].team != PTE_NONE ) { Q_strcat( text, MAX_TUTORIAL_TEXT, - va( "Press %s to return to free spectator mode\n", - CG_KeyNameForCommand( "+button2" ) ) ); - - if( CG_PlayerCount( ) > 1 ) - { - Q_strcat( text, MAX_TUTORIAL_TEXT, - va( "Press %s or ", - CG_KeyNameForCommand( "weapprev" ) ) ); - Q_strcat( text, MAX_TUTORIAL_TEXT, - va( "%s to change player\n", - CG_KeyNameForCommand( "weapnext" ) ) ); - } + va( "Press %s to spawn\n", CG_KeyNameForCommand( "+attack" ) ) ); } - else if( ps->pm_type == PM_SPECTATOR ) + else { Q_strcat( text, MAX_TUTORIAL_TEXT, - va( "Press %s to join a team\n", - CG_KeyNameForCommand( "+attack" ) ) ); + va( "Press %s to join a team\n", CG_KeyNameForCommand( "+attack" ) ) ); + } - if( CG_PlayerCount( ) > 0 ) - { - Q_strcat( text, MAX_TUTORIAL_TEXT, - va( "Press %s to enter spectator follow mode\n", - CG_KeyNameForCommand( "+button2" ) ) ); - } + if( ps->pm_flags & PMF_FOLLOW ) + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to stop following\n", + CG_KeyNameForCommand( "+button2" ) ) ); + + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s or ", CG_KeyNameForCommand( "weapprev" ) ) ); + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "%s to change player\n", CG_KeyNameForCommand( "weapnext" ) ) ); } else { Q_strcat( text, MAX_TUTORIAL_TEXT, - va( "Press %s to spawn\n", - CG_KeyNameForCommand( "+attack" ) ) ); + va( "Press %s to follow a %s\n", CG_KeyNameForCommand( "+button2" ), + ( cgs.clientinfo[ cg.clientNum ].team == PTE_NONE ) + ? "player" : "teammate" ) ); } }