]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/client.qc
Merge branch 'k9er/fix-2907' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / client.qc
index 4c19adcff1081e845e1d2314d8478843fcb7adad..92256c58f734dbd2e2efd4e2ff8e1cb049b619cd 100644 (file)
@@ -2,6 +2,7 @@
 
 #include <common/csqcmodel_settings.qh>
 #include <common/deathtypes/all.qh>
+#include <common/debug.qh>
 #include <common/effects/all.qh>
 #include <common/effects/qc/globalsound.qh>
 #include <common/ent_cs.qh>
@@ -51,7 +52,8 @@
 #include <server/chat.qh>
 #include <server/cheats.qh>
 #include <server/clientkill.qh>
-#include <server/command/common.qh>
+#include <server/command/banning.qh>
+#include <server/command/cmd.qh>
 #include <server/command/common.qh>
 #include <server/command/vote.qh>
 #include <server/compat/quake3.qh>
@@ -95,10 +97,6 @@ STATIC_METHOD(Client, Remove, void(Client this))
     ClientDisconnect(this);
 }
 
-void send_CSQC_teamnagger() {
-       WriteHeader(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
-}
-
 int CountSpectators(entity player, entity to)
 {
        if(!player) { return 0; } // not sure how, but best to be safe
@@ -142,6 +140,8 @@ bool ClientData_Send(entity this, entity to, int sf)
                                        sf |= BIT(3); // observing blocked
        if (autocvar_sv_showspectators == 1 || (autocvar_sv_showspectators && IS_SPEC(to)))
                                        sf |= BIT(4); // show spectators
+       if (autocvar_sv_teamnagger && teamplay && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
+                                                 sf |= (autocvar_sv_teamnagger & 0x03) << 5; // BIT(5) | BIT(6)
 
        WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
        WriteByte(MSG_ENTITY, sf);
@@ -238,6 +238,16 @@ void setplayermodel(entity e, string modelname)
                UpdatePlayerSounds(e);
 }
 
+entity SelectObservePoint(entity this)
+{
+       RandomSelection_Init();
+       IL_EACH(g_observepoints, true,
+       {
+               RandomSelection_AddEnt(it, 1, 1);
+       });
+       return RandomSelection_chosen_ent;
+}
+
 /** putting a client as observer in the server */
 void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
 {
@@ -258,18 +268,23 @@ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
                {
                        if (vote_called) { VoteCount(false); }
                        this.ready = false;
-                       if (warmup_stage || game_starttime > time) recount_ready = true;
+                       if (warmup_stage || game_starttime > time) /* warmup OR countdown */ recount_ready = true;
                }
                entcs_update_players(this);
        }
 
        if (use_spawnpoint)
        {
-               entity spot = SelectSpawnPoint(this, true);
+               // first try to find a random "nice" location to view from
+               entity spot = SelectObservePoint(this);
+               bool is_observepoint = (spot != NULL);
+               if(!spot) // otherwise just use the player spawn points
+                       spot = SelectSpawnPoint(this, true);
                if (!spot) LOG_FATAL("No spawnpoints for observers?!?");
+
                this.angles = vec2(spot.angles);
                // offset it so that the spectator spawns higher off the ground, looks better this way
-               setorigin(this, spot.origin + STAT(PL_VIEW_OFS, this));
+               setorigin(this, spot.origin + (is_observepoint ? '0 0 0' : autocvar_sv_player_viewoffset));
        }
        else // change origin to restore previous view origin
                setorigin(this, this.origin + STAT(PL_VIEW_OFS, this) - STAT(PL_CROUCH_VIEW_OFS, this));
@@ -309,8 +324,6 @@ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
 
        TRANSMUTE(Observer, this);
 
-       if(recount_ready) ReadyCount(); // FIXME: please add comment about why this is delayed
-
        WaypointSprite_PlayerDead(this);
        accuracy_resend(this);
 
@@ -412,6 +425,9 @@ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
 
        if (CS(this).just_joined)
                CS(this).just_joined = false;
+
+       if (recount_ready)
+               ReadyCount(); // must be called after SetPlayerTeam() and TRANSMUTE(Observer
 }
 
 int player_getspecies(entity this)
@@ -551,16 +567,23 @@ void GiveWarmupResources(entity this)
 
 void PutPlayerInServer(entity this)
 {
+       if (MUTATOR_CALLHOOK(ForbidSpawn, this))
+               return;
+
        if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
 
        PlayerState_attach(this);
        accuracy_resend(this);
 
-       if (teamplay && this.bot_forced_team)
-               SetPlayerTeam(this, this.bot_forced_team, TEAM_CHANGE_MANUAL);
-
-       if (this.team < 0)
-               TeamBalance_JoinBestTeam(this);
+       if (teamplay)
+       {
+               if (this.bot_forced_team)
+                       SetPlayerTeam(this, this.bot_forced_team, TEAM_CHANGE_MANUAL);
+               else if (this.wants_join > 0)
+                       SetPlayerTeam(this, this.wants_join, TEAM_CHANGE_MANUAL);
+               else if (this.team <= 0 || this.wants_join < 0 || autocvar_g_campaign)
+                       TeamBalance_JoinBestTeam(this);
+       }
 
        entity spot = SelectSpawnPoint(this, false);
        if (!spot) {
@@ -641,8 +664,9 @@ void PutPlayerInServer(entity this)
        this.respawn_flags = 0;
        this.respawn_time = 0;
        STAT(RESPAWN_TIME, this) = 0;
-       // DP model scaling uses 1/16 accuracy and 13/16 is closest to 56/69
-       this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) ? 0.8125 : autocvar_sv_player_scale);
+       this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) || !autocvar_sv_mapformat_is_quake3)
+               ? 0.8125 // DP model scaling uses 1/16 accuracy and 13/16 is closest to 56/69
+               : autocvar_sv_player_scale;
        this.fade_time = 0;
        this.pain_finished = 0;
        this.pushltime = 0;
@@ -819,7 +843,7 @@ void PutPlayerInServer(entity this)
 
        antilag_clear(this, CS(this));
 
-       if (warmup_stage < 0 || warmup_stage > 1)
+       if (warmup_stage)
                ReadyCount();
 }
 
@@ -980,7 +1004,7 @@ bool findinlist_abbrev(string tofind, string list)
                return false; // empty list or search, just return
 
        // this function allows abbreviated strings!
-       FOREACH_WORD(list, it == substring(tofind, 0, strlen(it)),
+       FOREACH_WORD(list, it != "" && it == substring(tofind, 0, strlen(it)),
        {
                return true;
        });
@@ -1008,6 +1032,8 @@ bool PlayerInIDList(entity p, string idlist)
 
 bool PlayerInList(entity player, string list)
 {
+       if (list == "")
+               return false;
        return boolean(PlayerInIDList(player, list) || PlayerInIPList(player, list));
 }
 
@@ -1037,18 +1063,31 @@ void ClientPreConnect(entity this)
 // also note that they aren't all registered mutators, e.g. jetpack, low gravity
 void SendWelcomeMessage(entity this, int msg_type)
 {
-       WriteByte(msg_type, boolean(autocvar_g_campaign));
        if (boolean(autocvar_g_campaign))
        {
-               WriteString(msg_type, Campaign_GetTitle());
+               WriteByte(msg_type, 1);
                WriteByte(msg_type, Campaign_GetLevelNum());
-               WriteString(msg_type, Campaign_GetMessage());
                return;
        }
+
+       int flags = 0;
+       if (CS(this).version_mismatch)
+               flags |= 2;
+       if (CS(this).version < autocvar_gameversion)
+               flags |= 4;
+       MapInfo_Get_ByName(mi_shortname, 0, NULL);
+       if (MapInfo_Map_author != "")
+               flags |= 8;
+       WriteByte(msg_type, flags);
+
        WriteString(msg_type, autocvar_hostname);
        WriteString(msg_type, autocvar_g_xonoticversion);
-       WriteByte(msg_type, CS(this).version_mismatch);
-       WriteByte(msg_type, (CS(this).version < autocvar_gameversion));
+
+       WriteString(msg_type, MapInfo_Map_titlestring);
+       if (flags & 8)
+               WriteString(msg_type, MapInfo_Map_author);
+       MapInfo_ClearTemps();
+
        WriteByte(msg_type, autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers);
        WriteByte(msg_type, GetPlayerLimit());
 
@@ -1085,6 +1124,7 @@ void SendWelcomeMessage(entity this, int msg_type)
 ClientConnect
 
 Called when a client connects to the server
+Do not send temp entity headers directly here, they may arrive before CSQC is ready.
 =============
 */
 void ClientConnect(entity this)
@@ -1117,6 +1157,7 @@ void ClientConnect(entity this)
                GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this.netname, this.team, false)));
 
        CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
+       this.wants_join = 0;
 
        stuffcmd(this, clientstuff, "\n");
        stuffcmd(this, "cl_particles_reloadeffects\n"); // TODO do we still need this?
@@ -1142,10 +1183,9 @@ void ClientConnect(entity this)
        bot_relinkplayerlist();
 
        CS(this).spectatortime = time;
-       if (blockSpectators)
-       {
+       if (!autocvar_sv_spectate)
+               // no centreprint here: player forced to join, or informed of why they can't via centreprint
                Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
-       }
 
        CS(this).jointime = time;
 
@@ -1164,10 +1204,6 @@ void ClientConnect(entity this)
        if (!autocvar_sv_foginterval && world.fog != "")
                stuffcmd(this, strcat("\nfog ", world.fog, "\nr_fog_exp2 0\nr_drawfog 1\n"));
 
-       if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
-               if(!MUTATOR_CALLHOOK(HideTeamNagger, this))
-                       send_CSQC_teamnagger();
-
        CSQCMODEL_AUTOINIT(this);
 
        CS(this).model_randomizer = random();
@@ -1186,6 +1222,13 @@ void ClientConnect(entity this)
 
        Handicap_Initialize(this);
 
+       // playban
+       if (PlayerInList(this, autocvar_g_playban_list))
+               TRANSMUTE(Observer, this);
+
+       if (PlayerInList(this, autocvar_g_chatban_list)) // chatban
+               CS(this).muted = true;
+
        MUTATOR_CALLHOOK(ClientConnect, this);
 
        if (player_count == 1)
@@ -1194,7 +1237,15 @@ void ClientConnect(entity this)
                        setpause(0);
                localcmd("\nsv_hook_firstjoin\n");
        }
+
+       if (get_nextmap() != "")
+               Send_NextMap_To_Player(this);
 }
+
+.string shootfromfixedorigin;
+.entity chatbubbleentity;
+void player_powerups_remove_all(entity this);
+
 /*
 =============
 ClientDisconnect
@@ -1202,13 +1253,24 @@ ClientDisconnect
 Called when a client disconnects from the server
 =============
 */
-.entity chatbubbleentity;
-void player_powerups_remove_all(entity this);
-
 void ClientDisconnect(entity this)
 {
        assert(IS_CLIENT(this), return);
 
+       /* from "ignore" command */
+       strfree(this.ignore_list);
+       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it.ignore_list,
+       {
+               if(it.crypto_idfp && it.crypto_idfp != "")
+                       continue;
+               string mylist = ignore_removefromlist(it, this);
+               if(it.ignore_list)
+                       strunzone(it.ignore_list);
+
+               it.ignore_list = strzone(mylist);
+       });
+       /* from "ignore" command */
+
        PlayerStats_GameReport_FinalizePlayer(this);
        if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
        if (CS(this).active_minigame) part_minigame(this);
@@ -1234,6 +1296,8 @@ void ClientDisconnect(entity this)
 
        RemoveGrapplingHooks(this);
 
+       strfree(this.shootfromfixedorigin);
+
        // Here, everything has been done that requires this player to be a client.
 
        this.flags &= ~FL_CLIENT;
@@ -1254,7 +1318,7 @@ void ClientDisconnect(entity this)
        if (this.personal) delete(this.personal);
 
        this.playerid = 0;
-       if (warmup_stage || game_starttime > time) ReadyCount();
+       if (warmup_stage || game_starttime > time) /* warmup OR countdown */ ReadyCount();
        if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
 
        player_powerups_remove_all(this); // stop powerup sound
@@ -1263,6 +1327,10 @@ void ClientDisconnect(entity this)
 
        if (player_count == 0)
                localcmd("\nsv_hook_lastleave\n");
+
+       if (!TeamBalance_QueuedPlayersTagIn(this))
+       if (autocvar_g_balance_teams_remove)
+               TeamBalance_RemoveExcessPlayers(NULL);
 }
 
 void ChatBubbleThink(entity this)
@@ -1278,7 +1346,7 @@ void ChatBubbleThink(entity this)
 
        this.mdl = "";
 
-       if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
+       if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) && !MUTATOR_CALLHOOK(ShowChatBubble, this.owner, this) )
        {
                if ( CS(this.owner).active_minigame && PHYS_INPUT_BUTTON_MINIGAME(this.owner) )
                        this.mdl = "models/sprites/minigame_busy.iqm";
@@ -1507,7 +1575,7 @@ void player_powerups(entity this)
                        else
                        {
                                play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
-                               if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
+                               if (time >= StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
                                {
                                        this.items = this.items - (this.items & IT_SUPERWEAPON);
                                        STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
@@ -1831,8 +1899,26 @@ void SetSpectatee_status(entity this, int spectatee_num)
                        CS(this).pressedkeys = 0;
                        STAT(PRESSED_KEYS, this) = 0;
                }
+
                ClientData_Touch(this);
-               if (g_race || g_cts) race_InitSpectator();
+
+               // init or clear race data
+               if ((g_race || g_cts) && g_race_qualifying && IS_REAL_CLIENT(this))
+               {
+                       msg_entity = this;
+
+                       if (this.enemy && this.enemy.race_laptime)
+                       {
+                               // init
+                               race_SendNextCheckpoint(this.enemy, 1);
+                       }
+                       else
+                       {
+                               // send reset to this spectator
+                               WriteHeader(MSG_ONE, TE_CSQC_RACE);
+                               WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR);
+                       }
+               }
        }
 }
 
@@ -1958,40 +2044,75 @@ void ShowRespawnCountdown(entity this)
        }
 }
 
-.bool team_selected;
 bool ShowTeamSelection(entity this)
 {
        if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
                return false;
+       if (QueuedPlayersReady(this, true))
+               return false;
        if (frametime) // once per frame is more than enough
                stuffcmd(this, "_scoreboard_team_selection 1\n");
        return true;
 }
-void Join(entity this)
+
+/// it's assumed this isn't called for bots (campaign_bots_may_start, centreprints)
+void Join(entity this, bool queued_join)
 {
+       entity player_with_dibs = NULL;
+
        if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
                ReadyRestart(true);
 
-       TRANSMUTE(Player, this);
+       if(queued_join
+       && TeamBalance_AreEqual(this, true)) // if a player couldn't tag in for balance, don't join them here as it would cause a stack
+       {
+               // First we must join player(s) queued for specific team(s) (they chose first)
+               // so TeamBalance_JoinBestTeam() (if necessary) won't select the same team(s).
+               // Relies on `this` skipping the queue (this.team already set, this.wants_join not set) or using autoselect.
+               FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join > 0,
+               {
+                       // detect any conflict between `this` and a queued player (queuePlayer() handles other conflicts)
+                       if (this.team < 0 && this.team_selected > 0 // `this` can't have their preference
+                       && it.wants_join == this.team_selected) // `it` is the player who already chose the team `this` wanted
+                               player_with_dibs = it;
 
-       if(!this.team_selected)
-       if(autocvar_g_campaign || autocvar_g_balance_teams)
-               TeamBalance_JoinBestTeam(this);
+                       Join(it, false);
+               });
+
+               // Second pass: queued players whose team will be autoselected
+               FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join < 0,
+               {
+                       Join(it, false);
+               });
+       }
 
        if(autocvar_g_campaign)
                campaign_bots_may_start = true;
 
        Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
 
+       TRANSMUTE(Player, this);
        PutClientInServer(this);
 
-       if(IS_PLAYER(this))
-       if(teamplay && this.team != -1)
+       if(IS_PLAYER(this)) // could be false due to PutClientInServer() mutator hook
        {
+               if (!teamplay)
+                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
+               else if (player_with_dibs)
+                       // limitation: notifications support only 1 translated team name
+                       // so the team `this` preferred can't be mentioned, only the team they got assigned to.
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_JOIN_PLAY_TEAM_QUEUECONFLICT), player_with_dibs.netname);
+               else if (this.wants_join)
+               {
+                       // Get queued player's attention
+                       if (game_starttime <= time) // No countdown in progress
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_ANNCE, ANNCE_BEGIN);
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_JOIN_PLAY_TEAM));
+               }
        }
-       else
-               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
-       this.team_selected = false;
+
+       this.team_selected = 0;
+       this.wants_join = 0;
 }
 
 int GetPlayerLimit()
@@ -2026,6 +2147,17 @@ int nJoinAllowed(entity this, entity ignore)
        if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
                return 0; // forced spectators can never join
 
+       static float msg_time = 0;
+       if(this && !INGAME(this) && ignore && PlayerInList(this, autocvar_g_playban_list))
+       {
+               if(time > msg_time)
+               {
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PLAYBAN);
+                       msg_time = time + 0.5;
+               }
+               return 0;
+       }
+
        // TODO simplify this
        int totalClients = 0;
        int currentlyPlaying = 0;
@@ -2044,7 +2176,6 @@ int nJoinAllowed(entity this, entity ignore)
        else if(player_limit > 0 && currentlyPlaying < player_limit)
                free_slots = min(maxclients - totalClients, player_limit - currentlyPlaying);
 
-       static float msg_time = 0;
        if(this && !INGAME(this) && ignore && !free_slots && time > msg_time)
        {
                Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT, player_limit);
@@ -2054,18 +2185,118 @@ int nJoinAllowed(entity this, entity ignore)
        return free_slots;
 }
 
-bool joinAllowed(entity this)
+// Callsites other than ClientCommand_selectteam() should pass this.wants_join as team_index
+// so the player won't accidentally reset a specific preference by pressing +jump
+// and will see the centreprint with their current preference each time they press +jump.
+bool queuePlayer(entity this, int team_index)
 {
-       if (CS(this).version_mismatch) return false;
+       if (IS_BOT_CLIENT(this) || !QueueNeeded(this))
+               return false;
+
+       // check if a queued player already chose the selected team
+       if (team_index > 0)
+       {
+               FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join == team_index,
+               {
+                       if (QueuedPlayersReady(this, false))
+                       {
+                               // Join() will handle the notification so it can mention the team `player` will actually get
+                               this.team = -1; // force autoselect in Join() (last player skips queue)
+                               this.team_selected = team_index; // tell it which team to check for to find the conflict
+                       }
+                       else // > 2 teams
+                       {
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(Team_IndexToTeam(team_index), CENTER_JOIN_PREVENT_QUEUE_TEAM_CONFLICT), it.netname);
+                               this.wants_join = -1; // force autoselect in Join()
+                               this.team_selected = -1; // prevents clobbering by CENTER_JOIN_PREVENT_QUEUE
+                       }
+                       return true;
+               });
+       }
+
+       if (QueuedPlayersReady(this, false))
+               return false;
+
+       if (team_index <= 0) // team auto select deferred until Join()
+       {
+               if (team_index != this.wants_join || !this.wants_join) // prevents chatcon spam
+                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_WANTS, this.netname);
+               if (this.team_selected >= 0) // prevents CENTER_JOIN_PREVENT_QUEUE_TEAM_CONFLICT getting clobbered
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT_QUEUE);
+               this.wants_join = -1;
+               this.team_selected = 0;
+       }
+       else
+       {
+               int team_num = Team_IndexToTeam(team_index);
+               if (team_index != this.wants_join) // prevents chatcon spam
+                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(team_num, INFO_JOIN_WANTS_TEAM), this.netname);
+               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(team_num, CENTER_JOIN_PREVENT_QUEUE_TEAM));
+               this.wants_join = team_index; // Player queued to join
+               this.team_selected = team_index;
+       }
+
+       return true;
+}
+
+bool joinAllowed(entity this, int team_index)
+{
+       if (CS(this).version_mismatch)
+       {
+               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT_VERSIONMISMATCH);
+               return false;
+       }
        if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
-       if (!nJoinAllowed(this, this)) return false;
-       if (teamplay && lockteams) return false;
-       if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
-       if (ShowTeamSelection(this)) return false;
+       if (teamplay && lockteams)
+       {
+               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_TEAMCHANGE_LOCKED);
+               return false;
+       }
+
+       if (QueueNeeded(this))
+       {
+               if (team_index == 0) // so ClientCommand_selectteam() can check joinAllowed() before calling SetPlayerTeam() without chicken/egg problem
+                       if (ShowTeamSelection(this)) return false; // only needed by callsites other than selectteam
+               // queuePlayer called here so that only conditions above block queuing (g_maxplayers shouldn't)
+               if (queuePlayer(this, team_index)) return false;
+               if (!nJoinAllowed(this, this)) return false;
+       }
+       else
+       {
+               if (!nJoinAllowed(this, this)) return false;
+               if (team_index == 0) // so ClientCommand_selectteam() can check joinAllowed() before calling SetPlayerTeam() without chicken/egg problem
+                       if (ShowTeamSelection(this)) return false; // only needed by callsites other than selectteam
+       }
+
        return true;
 }
 
-.string shootfromfixedorigin;
+void show_entnum(entity this)
+{
+       // waypoint editor implements a similar feature for waypoints
+       if (waypointeditor_enabled)
+               return;
+
+       if (wasfreed(this.wp_aimed))
+               this.wp_aimed = NULL;
+
+       WarpZone_crosshair_trace_plusvisibletriggers(this);
+       entity ent = NULL;
+       if (trace_ent)
+       {
+               ent = trace_ent;
+               if (ent != this.wp_aimed)
+               {
+                       string str = sprintf(
+                               "^7ent #%d\n^8 netname: ^3%s\n^8 classname: ^5%s\n^8 origin: ^2'%s'",
+                               etof(ent), ent.netname, ent.classname, vtos(ent.origin));
+                       debug_text_3d((ent.absmin + ent.absmax) * 0.5, str, 0, 7, '0 0 0');
+               }
+       }
+       if (this.wp_aimed != ent)
+               this.wp_aimed = ent;
+}
+
 .bool dualwielding_prev;
 bool PlayerThink(entity this)
 {
@@ -2086,6 +2317,8 @@ bool PlayerThink(entity this)
 
        if (frametime) player_powerups(this);
 
+       if (frametime && autocvar_sv_show_entnum) show_entnum(this);
+
        if (IS_DEAD(this)) {
                if (this.personal && g_race_qualifying) {
                        if (time > this.respawn_time) {
@@ -2165,7 +2398,7 @@ bool PlayerThink(entity this)
        FixPlayermodel(this);
 
        if (this.shootfromfixedorigin != autocvar_g_shootfromfixedorigin) {
-               this.shootfromfixedorigin = autocvar_g_shootfromfixedorigin;
+               strcpy(this.shootfromfixedorigin, autocvar_g_shootfromfixedorigin);
                stuffcmd(this, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
        }
 
@@ -2184,7 +2417,7 @@ bool PlayerThink(entity this)
                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                {
                        .entity weaponentity = weaponentities[slot];
-                       if(WEP_CVAR(vortex, charge_always))
+                       if(WEP_CVAR(WEP_VORTEX, charge_always))
                                W_Vortex_Charge(this, weaponentity, frametime);
                        W_WeaponFrame(this, weaponentity);
                }
@@ -2197,8 +2430,8 @@ bool PlayerThink(entity this)
                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                {
                        .entity weaponentity = weaponentities[slot];
-                       if (WEP_CVAR(vortex, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(vortex, charge_limit) && this.(weaponentity).vortex_charge_rottime < time)
-                               this.(weaponentity).vortex_charge = bound(WEP_CVAR(vortex, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(vortex, charge_rot_rate) * frametime / W_TICSPERFRAME, 1);
+                       if (WEP_CVAR(WEP_VORTEX, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(WEP_VORTEX, charge_limit) && this.(weaponentity).vortex_charge_rottime < time)
+                               this.(weaponentity).vortex_charge = bound(WEP_CVAR(WEP_VORTEX, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(WEP_VORTEX, charge_rot_rate) * frametime / W_TICSPERFRAME, 1);
                }
 
                player_regen(this);
@@ -2230,16 +2463,23 @@ void ObserverOrSpectatorThink(entity this)
                }
        }
 
+       if (frametime && autocvar_sv_show_entnum) show_entnum(this);
+
        if (IS_BOT_CLIENT(this) && !CS(this).autojoin_checked)
        {
                CS(this).autojoin_checked = true;
                TRANSMUTE(Player, this);
                PutClientInServer(this);
+
+               .entity weaponentity = weaponentities[0];
+               if(this.(weaponentity).m_weapon == WEP_Null)
+                       W_NextWeapon(this, 0, weaponentity);
+
                return;
        }
 
        if (this.flags & FL_JUMPRELEASED) {
-               if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
+               if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this, this.wants_join) || time < CS(this).jointime + MIN_SPEC_TIME)) {
                        this.flags &= ~FL_JUMPRELEASED;
                        this.flags |= FL_SPAWNING;
                } else if((is_spec && (PHYS_INPUT_BUTTON_ATCK(this) || CS(this).impulse == 10 || CS(this).impulse == 15 || CS(this).impulse == 18 || (CS(this).impulse >= 200 && CS(this).impulse <= 209)))
@@ -2288,11 +2528,12 @@ void ObserverOrSpectatorThink(entity this)
                if ((is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)))
                        || (!is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this)))) {
                        this.flags |= FL_JUMPRELEASED;
+                       // primary attack pressed
                        if(this.flags & FL_SPAWNING)
                        {
                                this.flags &= ~FL_SPAWNING;
-                               if(joinAllowed(this))
-                                       Join(this);
+                               if(joinAllowed(this, this.wants_join))
+                                       Join(this, teamplay);
                                else if(time < CS(this).jointime + MIN_SPEC_TIME)
                                        CS(this).autojoin_checked = -1;
                                return;
@@ -2354,135 +2595,23 @@ void PlayerUseKey(entity this)
 =============
 PlayerPreThink
 
-Called every frame for each client before the physics are run
+Called every frame for each real client by DP (and for each bot by StartFrame()),
+and when executing every asynchronous move, so only include things that MUST be done then.
+Use PlayerFrame() instead for code that only needs to run once per server frame.
+frametime == 0 in the asynchronous code path.
+
+TODO: move more stuff from here and PlayerThink() and ObserverOrSpectatorThink() to PlayerFrame() (frametime is always set there)
 =============
 */
 .float last_vehiclecheck;
 void PlayerPreThink (entity this)
 {
-       STAT(GUNALIGN, this) = CS_CVAR(this).cvar_cl_gunalign; // TODO
-       STAT(MOVEVARS_CL_TRACK_CANJUMP, this) = CS_CVAR(this).cvar_cl_movement_track_canjump;
-
        WarpZone_PlayerPhysics_FixVAngle(this);
 
-       if (frametime) {
-               // physics frames: update anticheat stuff
-               anticheat_prethink(this);
-
-               // WORKAROUND: only use dropclient in server frames (frametime set).
-               // Never use it in cl_movement frames (frametime zero).
-               if (blockSpectators && IS_REAL_CLIENT(this)
-                       && (IS_SPEC(this) || IS_OBSERVER(this)) && !INGAME(this)
-                       && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime))
-               {
-                       if (dropclient_schedule(this))
-                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
-               }
-       }
-
        zoomstate_set = false;
 
-       // Check for nameless players
-       if (this.netname == "" || this.netname != CS(this).netname_previous)
-       {
-               bool assume_unchanged = (CS(this).netname_previous == "");
-               if (autocvar_sv_name_maxlength > 0 && strlennocol(this.netname) > autocvar_sv_name_maxlength)
-               {
-                       int new_length = textLengthUpToLength(this.netname, autocvar_sv_name_maxlength, strlennocol);
-                       this.netname = strzone(strcat(substring(this.netname, 0, new_length), "^7"));
-                       sprint(this, sprintf("Warning: your name is longer than %d characters, it has been truncated.\n", autocvar_sv_name_maxlength));
-                       assume_unchanged = false;
-                       // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
-               }
-               if (isInvisibleString(this.netname))
-               {
-                       this.netname = strzone(sprintf("Player#%d", this.playerid));
-                       sprint(this, "Warning: invisible names are not allowed.\n");
-                       assume_unchanged = false;
-                       // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
-               }
-               if (!assume_unchanged && autocvar_sv_eventlog)
-                       GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this.netname, this.team, false)));
-               strcpy(CS(this).netname_previous, this.netname);
-       }
-
-       // version nagging
-       if (CS(this).version_nagtime && CS_CVAR(this).cvar_g_xonoticversion && time > CS(this).version_nagtime) {
-        CS(this).version_nagtime = 0;
-        if (strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "autobuild", 0) >= 0) {
-            // git client
-        } else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0) {
-            // git server
-            Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
-        } else {
-            int r = vercmp(CS_CVAR(this).cvar_g_xonoticversion, autocvar_g_xonoticversion);
-            if (r < 0) { // old client
-                Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
-            } else if (r > 0) { // old server
-                Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
-            }
-        }
-    }
-
-       // GOD MODE info
-       if (!(this.flags & FL_GODMODE) && this.max_armorvalue)
-       {
-               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue);
-               this.max_armorvalue = 0;
-       }
-
-       if (frametime && IS_PLAYER(this) && time >= game_starttime)
-       {
-               if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
-               {
-                       STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
-                       SetResourceExplicit(this, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
-                       if (this.iceblock)
-                               this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
-
-                       if (STAT(REVIVE_PROGRESS, this) >= 1)
-                               Unfreeze(this, false);
-               }
-               else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
-               {
-                       STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
-                       SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
-
-                       if (GetResource(this, RES_HEALTH) < 1)
-                       {
-                               if (this.vehicle)
-                                       vehicles_exit(this.vehicle, VHEF_RELEASE);
-                               if(this.event_damage)
-                                       this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
-                       }
-                       else if (STAT(REVIVE_PROGRESS, this) <= 0)
-                               Unfreeze(this, false);
-               }
-       }
-
        MUTATOR_CALLHOOK(PlayerPreThink, this);
 
-       if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle)
-       if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
-       {
-               FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
-               {
-                       if(!it.owner)
-                       {
-                               if(!it.team || SAME_TEAM(this, it))
-                                       Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER);
-                               else if(autocvar_g_vehicles_steal)
-                                       Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL);
-                       }
-                       else if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
-                       {
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
-                       }
-               });
-
-               this.last_vehiclecheck = time + 1;
-       }
-
        if(PHYS_INPUT_BUTTON_USE(this) && !CS(this).usekeypressed)
                PlayerUseKey(this);
        CS(this).usekeypressed = PHYS_INPUT_BUTTON_USE(this);
@@ -2508,8 +2637,8 @@ void PlayerPreThink (entity this)
                        || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
                                && (!teamplay || autocvar_g_balance_teams)))
                {
-                       if(joinAllowed(this))
-                               Join(this);
+                       if(joinAllowed(this, this.wants_join))
+                               Join(this, teamplay);
                        return;
                }
        }
@@ -2530,6 +2659,7 @@ void PlayerPreThink (entity this)
                SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
        }
 
+       // Voice sound effects
        if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
        {
                CS(this).teamkill_soundtime = 0;
@@ -2600,17 +2730,203 @@ void Player_Physics(entity this)
 =============
 PlayerPostThink
 
-Called every frame for each client after the physics are run
+Called every frame for each real client by DP (and for each bot by StartFrame()),
+and when executing every asynchronous move, so only include things that MUST be done then.
+Use PlayerFrame() instead for code that only needs to run once per server frame.
+frametime == 0 in the asynchronous code path.
 =============
 */
 void PlayerPostThink (entity this)
 {
        Player_Physics(this);
 
-       if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0))
-       if (frametime) // WORKAROUND: only use dropclient in server frames (frametime set). Never use it in cl_movement frames (frametime zero).
+       if (IS_PLAYER(this)) {
+               if(this.death_time == time && IS_DEAD(this))
+               {
+                       // player's bbox gets resized now, instead of in the damage event that killed the player,
+                       // once all the damage events of this frame have been processed with normal size
+                       float h = ceil((this.mins.z + this.maxs.z) * PL_CORPSE_SCALE * 10) / 10;
+                       this.maxs.z = max(h, this.mins.z + 1);
+                       setsize(this, this.mins, this.maxs);
+               }
+               DrownPlayer(this);
+               UpdateChatBubble(this);
+               if (CS(this).impulse) ImpulseCommands(this);
+               GetPressedKeys(this);
+               if (game_stopped)
+               {
+                       CSQCMODEL_AUTOUPDATE(this);
+                       return;
+               }
+       }
+       else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
+       {
+               CS(this).pressedkeys = 0;
+               STAT(PRESSED_KEYS, this) = 0;
+       }
+
+       CSQCMODEL_AUTOUPDATE(this);
+}
+
+/*
+=============
+PlayerFrame
+
+Called every frame for each client by StartFrame().
+Use this for code that only needs to run once per server frame.
+frametime is always set here.
+=============
+*/
+void PlayerFrame (entity this)
+{
+// formerly PreThink code
+
+       if (this.score_frame_dmg)
+       {
+               GameRules_scoring_add(this, DMG, this.score_frame_dmg);
+               this.score_frame_dmg = 0;
+       }
+       if (this.score_frame_dmgtaken)
+       {
+               GameRules_scoring_add(this, DMGTAKEN, this.score_frame_dmgtaken);
+               this.score_frame_dmgtaken = 0;
+       }
+
+       STAT(GUNALIGN, this) = CS_CVAR(this).cvar_cl_gunalign; // TODO
+       STAT(MOVEVARS_CL_TRACK_CANJUMP, this) = CS_CVAR(this).cvar_cl_movement_track_canjump;
+
+       // physics frames: update anticheat stuff
+       anticheat_prethink(this);
+
+       // Check if spectating is allowed
+       if (!autocvar_sv_spectate && IS_REAL_CLIENT(this)
+       && (IS_SPEC(this) || IS_OBSERVER(this)) && !INGAME(this))
+       {
+               float cutoff = CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime;
+               if (time > cutoff + MIN_SPEC_TIME * 0.5)
+               {
+                       // sv_spectate was disabled recently (or the server was stalled far too long)
+                       CS(this).spectatortime = time; // reset the grace period
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
+               }
+               else if (time > cutoff)
+                       if (dropclient_schedule(this))
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
+       }
+
+       // Check for nameless players
+       if (this.netname == "" || this.netname != CS(this).netname_previous)
+       {
+               bool assume_unchanged = (CS(this).netname_previous == "");
+               if (autocvar_sv_name_maxlength > 0 && strlennocol(this.netname) > autocvar_sv_name_maxlength)
+               {
+                       int new_length = textLengthUpToLength(this.netname, autocvar_sv_name_maxlength, strlennocol);
+                       this.netname = strzone(strcat(substring(this.netname, 0, new_length), "^7"));
+                       sprint(this, sprintf("Warning: your name is longer than %d characters, it has been truncated.\n", autocvar_sv_name_maxlength));
+                       assume_unchanged = false;
+                       // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
+               }
+               if (isInvisibleString(this.netname))
+               {
+                       this.netname = strzone(sprintf("Player#%d", this.playerid));
+                       sprint(this, "Warning: invisible names are not allowed.\n");
+                       assume_unchanged = false;
+                       // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
+               }
+               if (!assume_unchanged && autocvar_sv_eventlog)
+                       GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this.netname, this.team, false)));
+               strcpy(CS(this).netname_previous, this.netname);
+       }
+
+       // version nagging
+       if (CS(this).version_nagtime && CS_CVAR(this).cvar_g_xonoticversion && time > CS(this).version_nagtime)
+       {
+               CS(this).version_nagtime = 0;
+               if (strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "autobuild", 0) >= 0)
+               {
+                       // git client
+               }
+               else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0)
+               {
+                       // git server
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
+               }
+               else
+               {
+                       int r = vercmp(CS_CVAR(this).cvar_g_xonoticversion, autocvar_g_xonoticversion);
+                       if (r < 0) // old client
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
+                       else if (r > 0) // old server
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
+               }
+       }
+
+       // GOD MODE info
+       if (!(this.flags & FL_GODMODE) && this.max_armorvalue)
+       {
+               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue);
+               this.max_armorvalue = 0;
+       }
+
+       // FreezeTag
+       if (IS_PLAYER(this) && time >= game_starttime)
+       {
+               if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
+               {
+                       STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
+                       SetResourceExplicit(this, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
+                       if (this.iceblock)
+                               this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
+
+                       if (STAT(REVIVE_PROGRESS, this) >= 1)
+                               Unfreeze(this, false);
+               }
+               else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
+               {
+                       STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
+                       SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
+
+                       if (GetResource(this, RES_HEALTH) < 1)
+                       {
+                               if (this.vehicle)
+                                       vehicles_exit(this.vehicle, VHEF_RELEASE);
+                               if(this.event_damage)
+                                       this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
+                       }
+                       else if (STAT(REVIVE_PROGRESS, this) <= 0)
+                               Unfreeze(this, false);
+               }
+       }
+
+       // Vehicles
+       if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle)
+       if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
+       {
+               FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
+               {
+                       if(!it.owner)
+                       {
+                               if(!it.team || SAME_TEAM(this, it))
+                                       Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER);
+                               else if(autocvar_g_vehicles_steal)
+                                       Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL);
+                       }
+                       else if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
+                       {
+                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
+                       }
+               });
+
+               this.last_vehiclecheck = time + 1;
+       }
+
+
+
+// formerly PostThink code
+       if (autocvar_sv_maxidle > 0 || ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0))
        if (IS_REAL_CLIENT(this))
-       if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
+       if (IS_PLAYER(this) || this.wants_join || autocvar_sv_maxidle_alsokickspectators)
        if (!intermission_running) // NextLevel() kills all centerprints after setting this true
        {
                int totalClients = 0;
@@ -2627,7 +2943,7 @@ void PlayerPostThink (entity this)
                                        totalClients = 0;
                        }
                }
-               else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+               else if ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0)
                {
                        FOREACH_CLIENT(IS_REAL_CLIENT(it),
                        {
@@ -2651,22 +2967,39 @@ void PlayerPostThink (entity this)
                else
                {
                        float maxidle_time = autocvar_sv_maxidle;
-                       if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+                       if ((IS_PLAYER(this) || this.wants_join)
+                       && autocvar_sv_maxidle_playertospectator > 0)
                                maxidle_time = autocvar_sv_maxidle_playertospectator;
                        float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
                        float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
                        if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
                        {
-                               if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
-                                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
+                               if ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0)
+                               {
+                                       if (!this.wants_join) // no countdown centreprint when getting kicked off the join queue
+                                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
+                               }
                                else
                                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
                        }
-                       if (timeleft <= 0) {
-                               if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+                       if (timeleft <= 0)
+                       {
+                               if ((IS_PLAYER(this) || this.wants_join)
+                               && autocvar_sv_maxidle_playertospectator > 0)
                                {
-                                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
+                                       if (this.wants_join)
+                                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING_QUEUE, this.netname, maxidle_time);
+                                       else
+                                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
                                        PutObserverInServer(this, true, true);
+                                       // Can't do this in PutObserverInServer() or SetPlayerTeam() cos it causes
+                                       // mouse2 (change spectate mode) to kick the player off the join queue.
+                                       this.wants_join = 0;
+                                       this.team_selected = 0;
+                                       // when the player is kicked off the server, these are called in ClientDisconnect()
+                                       if (!TeamBalance_QueuedPlayersTagIn(this))
+                                       if (autocvar_g_balance_teams_remove)
+                                               TeamBalance_RemoveExcessPlayers(this);
                                }
                                else
                                {
@@ -2675,7 +3008,9 @@ void PlayerPostThink (entity this)
                                }
                                return;
                        }
-                       else if (timeleft <= countdown_time) {
+                       else if (timeleft <= countdown_time
+                       && !this.wants_join) // no countdown bangs when getting kicked off the join queue
+                       {
                                if (timeleft != CS(this).idlekick_lasttimeleft)
                                        play2(this, SND(TALK2));
                                CS(this).idlekick_lasttimeleft = timeleft;
@@ -2695,36 +3030,10 @@ void PlayerPostThink (entity this)
                CS(this).teamkill_soundsource = NULL;
        }
 
-       if (IS_PLAYER(this)) {
-               if(this.death_time == time && IS_DEAD(this))
-               {
-                       // player's bbox gets resized now, instead of in the damage event that killed the player,
-                       // once all the damage events of this frame have been processed with normal size
-                       this.maxs.z = 5;
-                       setsize(this, this.mins, this.maxs);
-               }
-               DrownPlayer(this);
-               UpdateChatBubble(this);
-               if (CS(this).impulse) ImpulseCommands(this);
-               GetPressedKeys(this);
-               if (game_stopped)
-               {
-                       CSQCMODEL_AUTOUPDATE(this);
-                       return;
-               }
-       }
-       else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
-       {
-               CS(this).pressedkeys = 0;
-               STAT(PRESSED_KEYS, this) = 0;
-       }
-
        if (this.waypointsprite_attachedforcarrier) {
                float hp = healtharmor_maxdamage(GetResource(this, RES_HEALTH), GetResource(this, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x;
                WaypointSprite_UpdateHealth(this.waypointsprite_attachedforcarrier, hp);
        }
-
-       CSQCMODEL_AUTOUPDATE(this);
 }
 
 // hack to copy the button fields from the client entity to the Client State