]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/client.qc
Merge branch 'bones_was_here/q3compat' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / client.qc
index 6bebfeb3666d33591dfe7c5c865344dba112979b..804025ace1e5efab8a88d140ff2db5808a72b2eb 100644 (file)
@@ -6,6 +6,7 @@
 #include <common/effects/qc/globalsound.qh>
 #include <common/ent_cs.qh>
 #include <common/gamemodes/_mod.qh>
+#include <common/gamemodes/gamemode/lms/sv_lms.qh>
 #include <common/gamemodes/gamemode/nexball/sv_nexball.qh>
 #include <common/items/_mod.qh>
 #include <common/items/inventory.qh>
@@ -23,6 +24,7 @@
 #include <common/mutators/mutator/instagib/sv_instagib.qh>
 #include <common/mutators/mutator/nades/nades.qh>
 #include <common/mutators/mutator/overkill/oknex.qh>
+#include <common/mutators/mutator/status_effects/_mod.qh>
 #include <common/mutators/mutator/waypoints/all.qh>
 #include <common/net_linked.qh>
 #include <common/net_notice.qh>
@@ -199,19 +201,19 @@ string CheckPlayerModel(string plyermodel) {
                FallbackPlayerModel = strzone(cvar_defstring("_cl_playermodel"));
        }
        // only in right path
-       if( substring(plyermodel,0,14) != "models/player/")
+       if(substring(plyermodel, 0, 14) != "models/player/")
                return FallbackPlayerModel;
        // only good file extensions
-       if(substring(plyermodel,-4,4) != ".zym")
-       if(substring(plyermodel,-4,4) != ".dpm")
-       if(substring(plyermodel,-4,4) != ".iqm")
-       if(substring(plyermodel,-4,4) != ".md3")
-       if(substring(plyermodel,-4,4) != ".psk")
+       if(substring(plyermodel, -4, 4) != ".iqm"
+               && substring(plyermodel, -4, 4) != ".zym"
+               && substring(plyermodel, -4, 4) != ".dpm"
+               && substring(plyermodel, -4, 4) != ".md3"
+               && substring(plyermodel, -4, 4) != ".psk")
+       {
                return FallbackPlayerModel;
+       }
        // forbid the LOD models
-       if(substring(plyermodel, -9,5) == "_lod1")
-               return FallbackPlayerModel;
-       if(substring(plyermodel, -9,5) == "_lod2")
+       if(substring(plyermodel, -9, 5) == "_lod1" || substring(plyermodel, -9, 5) == "_lod2")
                return FallbackPlayerModel;
        if(plyermodel != strtolower(plyermodel))
                return FallbackPlayerModel;
@@ -253,6 +255,7 @@ void PutObserverInServer(entity this)
                        if (vote_called) { VoteCount(false); }
                        ReadyCount();
                }
+               entcs_update_players(this);
        }
 
        entity spot = SelectSpawnPoint(this, true);
@@ -334,9 +337,6 @@ void PutObserverInServer(entity this)
        this.scale = 0;
        this.fade_time = 0;
        this.pain_finished = 0;
-       STAT(STRENGTH_FINISHED, this) = 0;
-       STAT(INVINCIBLE_FINISHED, this) = 0;
-       STAT(SUPERWEAPONS_FINISHED, this) = 0;
        STAT(AIR_FINISHED, this) = 0;
        //this.dphitcontentsmask = 0;
        this.dphitcontentsmask = DPCONTENTS_SOLID;
@@ -372,7 +372,6 @@ void PutObserverInServer(entity this)
        this.punchangle = '0 0 0';
        this.punchvector = '0 0 0';
        this.oldvelocity = this.velocity;
-       this.fire_endtime = -1;
        this.event_damage = func_null;
        this.event_heal = func_null;
 
@@ -394,6 +393,9 @@ void PutObserverInServer(entity this)
                SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR);
                this.frags = FRAGS_SPECTATOR;
        }
+
+       bot_relinkplayerlist();
+
        if (CS(this).just_joined)
                CS(this).just_joined = false;
 }
@@ -589,11 +591,13 @@ void PutPlayerInServer(entity this)
 
        PS(this).dual_weapons = '0 0 0';
 
-       STAT(SUPERWEAPONS_FINISHED, this) = (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
+       if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
+               StatusEffects_apply(STATUSEFFECT_Superweapons, this, time + autocvar_g_balance_superweapons_time, 0);
 
        this.items = start_items;
 
-       this.spawnshieldtime = time + autocvar_g_spawnshieldtime;
+       float shieldtime = time + autocvar_g_spawnshieldtime;
+
        this.pauserotarmor_finished = time + autocvar_g_balance_pause_armor_rot_spawn;
        this.pauserothealth_finished = time + autocvar_g_balance_pause_health_rot_spawn;
        this.pauserotfuel_finished = time + autocvar_g_balance_pause_fuel_rot_spawn;
@@ -601,19 +605,20 @@ void PutPlayerInServer(entity this)
        if (!sv_ready_restart_after_countdown && time < game_starttime)
        {
                float f = game_starttime - time;
-               this.spawnshieldtime += f;
+               shieldtime += f;
                this.pauserotarmor_finished += f;
                this.pauserothealth_finished += f;
                this.pauseregen_finished += f;
        }
 
+       StatusEffects_apply(STATUSEFFECT_SpawnShield, this, shieldtime, 0);
+
        this.damageforcescale = autocvar_g_player_damageforcescale;
        this.death_time = 0;
        this.respawn_flags = 0;
        this.respawn_time = 0;
        STAT(RESPAWN_TIME, this) = 0;
-       bool q3dfcompat = autocvar_sv_q3defragcompat && autocvar_sv_q3defragcompat_changehitbox;
-       this.scale = ((q3dfcompat) ? 0.9 : autocvar_sv_player_scale);
+       this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) ? 0.9 : autocvar_sv_player_scale);
        this.fade_time = 0;
        this.pain_finished = 0;
        this.pushltime = 0;
@@ -637,16 +642,9 @@ void PutPlayerInServer(entity this)
        this.punchangle = '0 0 0';
        this.punchvector = '0 0 0';
 
-       STAT(STRENGTH_FINISHED, this) = 0;
-       STAT(INVINCIBLE_FINISHED, this) = 0;
-       this.fire_endtime = -1;
        STAT(REVIVE_PROGRESS, this) = 0;
        this.revival_time = 0;
 
-       // TODO: we can't set these in the PlayerSpawn hook since the target code is called before it!
-       STAT(BUFFS, this) = 0;
-       STAT(BUFF_TIME, this) = 0;
-
        STAT(AIR_FINISHED, this) = 0;
        this.waterlevel = WATERLEVEL_NONE;
        this.watertype = CONTENT_EMPTY;
@@ -749,6 +747,10 @@ void PutPlayerInServer(entity this)
                }
        });
 
+       Unfreeze(this, false);
+
+       MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
+
        {
                string s = spot.target;
                if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack
@@ -758,10 +760,6 @@ void PutPlayerInServer(entity this)
                        spot.target = s;
        }
 
-       Unfreeze(this, false);
-
-       MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
-
        if (autocvar_spawn_debug)
        {
                sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
@@ -771,14 +769,15 @@ void PutPlayerInServer(entity this)
        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
                .entity weaponentity = weaponentities[slot];
+               entity w_ent = this.(weaponentity);
                if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
-                       this.(weaponentity).m_switchweapon = w_getbestweapon(this, weaponentity);
+                       w_ent.m_switchweapon = w_getbestweapon(this, weaponentity);
                else
-                       this.(weaponentity).m_switchweapon = WEP_Null;
-               this.(weaponentity).m_weapon = WEP_Null;
-               this.(weaponentity).weaponname = "";
-               this.(weaponentity).m_switchingweapon = WEP_Null;
-               this.(weaponentity).cnt = -1;
+                       w_ent.m_switchweapon = WEP_Null;
+               w_ent.m_weapon = WEP_Null;
+               w_ent.weaponname = "";
+               w_ent.m_switchingweapon = WEP_Null;
+               w_ent.cnt = -1;
        }
 
        MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
@@ -824,6 +823,8 @@ void PutClientInServer(entity this)
        } else if (IS_PLAYER(this)) {
                PutPlayerInServer(this);
        }
+
+       bot_relinkplayerlist();
 }
 
 // TODO do we need all these fields, or should we stop autodetecting runtime
@@ -1042,14 +1043,10 @@ string getwelcomemessage(entity this)
                modifications = strcat(modifications, ", Weapons stay");
        if(autocvar_g_jetpack)
                modifications = strcat(modifications, ", Jet pack");
-       if(autocvar_g_powerups == 0)
-               modifications = strcat(modifications, ", No powerups");
-       if(autocvar_g_powerups > 0)
-               modifications = strcat(modifications, ", Powerups");
        modifications = substring(modifications, 2, strlen(modifications) - 2);
 
        string versionmessage = GetClientVersionMessage(this);
-       string s = strcat(versionmessage, "^8\n^8\nhost is ^9", autocvar_hostname, "^8\n");
+       string s = strcat(versionmessage, "^8\n^8\nserver is ^9", autocvar_hostname, "^8\n");
 
        s = strcat(s, "^8\nmatch type is ^1", gamemode_name, "^8\n");
 
@@ -1079,8 +1076,6 @@ string getwelcomemessage(entity this)
        return s;
 }
 
-bool autocvar_sv_qcphysics = true; // TODO this is for testing - remove when qcphysics work
-
 /**
 =============
 ClientConnect
@@ -1200,6 +1195,8 @@ 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);
@@ -1252,6 +1249,8 @@ void ClientDisconnect(entity this)
        ReadyCount();
        if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
 
+       player_powerups_remove_all(this); // stop powerup sound
+
        ONREMOVE(this);
 }
 
@@ -1444,6 +1443,18 @@ void play_countdown(entity this, float finished, Sound samp)
                                sound (this, CH_INFO, samp, VOL_BASE, ATTEN_NORM);
 }
 
+void player_powerups_remove_all(entity this)
+{
+       if (this.items & IT_SUPERWEAPON)
+       {
+               // don't play the poweroff sound when the game restarts or the player disconnects
+               if (time > game_starttime + 1 && IS_CLIENT(this))
+                       sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM);
+               stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound
+               this.items -= (this.items & IT_SUPERWEAPON);
+       }
+}
+
 void player_powerups(entity this)
 {
        if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped)
@@ -1451,19 +1462,10 @@ void player_powerups(entity this)
        else
                this.modelflags &= ~MF_ROCKET;
 
-       this.effects &= ~(EF_RED | EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT | EF_FLAME | EF_NODEPTHTEST);
+       this.effects &= ~EF_NODEPTHTEST;
 
        if (IS_DEAD(this))
-       {
-               if (this.items & (ITEM_Strength.m_itemid | ITEM_Shield.m_itemid | IT_SUPERWEAPON))
-               {
-                       sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM);
-                       stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound
-                       this.items &= ~ITEM_Strength.m_itemid;
-                       this.items &= ~ITEM_Shield.m_itemid;
-                       this.items -= (this.items & IT_SUPERWEAPON);
-               }
-       }
+               player_powerups_remove_all(this);
 
        if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed
                return;
@@ -1471,58 +1473,14 @@ void player_powerups(entity this)
        // add a way to see what the items were BEFORE all of these checks for the mutator hook
        int items_prev = this.items;
 
-       Fire_ApplyDamage(this);
-       Fire_ApplyEffect(this);
-
        if (!MUTATOR_IS_ENABLED(mutator_instagib))
        {
-               if (this.items & ITEM_Strength.m_itemid)
-               {
-                       play_countdown(this, STAT(STRENGTH_FINISHED, this), SND_POWEROFF);
-                       this.effects = this.effects | (EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT);
-                       if (time > STAT(STRENGTH_FINISHED, this))
-                       {
-                               this.items = this.items - (this.items & ITEM_Strength.m_itemid);
-                               //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERDOWN_STRENGTH, this.netname);
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERDOWN_STRENGTH);
-                       }
-               }
-               else
-               {
-                       if (time < STAT(STRENGTH_FINISHED, this))
-                       {
-                               this.items = this.items | ITEM_Strength.m_itemid;
-                               if(!g_cts)
-                                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERUP_STRENGTH, this.netname);
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERUP_STRENGTH);
-                       }
-               }
-               if (this.items & ITEM_Shield.m_itemid)
-               {
-                       play_countdown(this, STAT(INVINCIBLE_FINISHED, this), SND_POWEROFF);
-                       this.effects = this.effects | (EF_RED | EF_ADDITIVE | EF_FULLBRIGHT);
-                       if (time > STAT(INVINCIBLE_FINISHED, this))
-                       {
-                               this.items = this.items - (this.items & ITEM_Shield.m_itemid);
-                               //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERDOWN_SHIELD, this.netname);
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERDOWN_SHIELD);
-                       }
-               }
-               else
-               {
-                       if (time < STAT(INVINCIBLE_FINISHED, this))
-                       {
-                               this.items = this.items | ITEM_Shield.m_itemid;
-                               if(!g_cts)
-                                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERUP_SHIELD, this.netname);
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERUP_SHIELD);
-                       }
-               }
+               // NOTE: superweapons are a special case and as such are handled here instead of the status effects system
                if (this.items & IT_SUPERWEAPON)
                {
                        if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
                        {
-                               STAT(SUPERWEAPONS_FINISHED, this) = 0;
+                               StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL);
                                this.items = this.items - (this.items & IT_SUPERWEAPON);
                                //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname);
                                Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST);
@@ -1533,8 +1491,8 @@ void player_powerups(entity this)
                        }
                        else
                        {
-                               play_countdown(this, STAT(SUPERWEAPONS_FINISHED, this), SND_POWEROFF);
-                               if (time > STAT(SUPERWEAPONS_FINISHED, this))
+                               play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
+                               if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
                                {
                                        this.items = this.items - (this.items & IT_SUPERWEAPON);
                                        STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
@@ -1545,7 +1503,7 @@ void player_powerups(entity this)
                }
                else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
                {
-                       if (time < STAT(SUPERWEAPONS_FINISHED, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
+                       if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
                        {
                                this.items = this.items | IT_SUPERWEAPON;
                                if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
@@ -1557,13 +1515,14 @@ void player_powerups(entity this)
                        }
                        else
                        {
-                               STAT(SUPERWEAPONS_FINISHED, this) = 0;
+                               if(StatusEffects_active(STATUSEFFECT_Superweapons, this))
+                                       StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT);
                                STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
                        }
                }
-               else
+               else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame!
                {
-                       STAT(SUPERWEAPONS_FINISHED, this) = 0;
+                       StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR);
                }
        }
 
@@ -1573,10 +1532,6 @@ void player_powerups(entity this)
        if(autocvar_g_fullbrightplayers)
                this.effects = this.effects | EF_FULLBRIGHT;
 
-       if (time >= game_starttime)
-       if (time < this.spawnshieldtime)
-               this.effects = this.effects | (EF_ADDITIVE | EF_FULLBRIGHT);
-
        MUTATOR_CALLHOOK(PlayerPowerups, this, items_prev);
 }
 
@@ -1703,6 +1658,14 @@ void SetZoomState(entity this, float newzoom)
 void GetPressedKeys(entity this)
 {
        MUTATOR_CALLHOOK(GetPressedKeys, this);
+       if (game_stopped)
+       {
+               CS(this).pressedkeys = 0;
+               STAT(PRESSED_KEYS, this) = 0;
+               return;
+       }
+
+       // NOTE: GetPressedKeys and PM_dodging_GetPressedKeys use similar code
        int keys = STAT(PRESSED_KEYS, this);
        keys = BITSET(keys, KEY_FORWARD,        CS(this).movement.x > 0);
        keys = BITSET(keys, KEY_BACKWARD,       CS(this).movement.x < 0);
@@ -1745,9 +1708,6 @@ void SpectateCopy(entity this, entity spectatee)
        this.items = spectatee.items;
        STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
        STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
-       STAT(STRENGTH_FINISHED, this) = STAT(STRENGTH_FINISHED, spectatee);
-       STAT(INVINCIBLE_FINISHED, this) = STAT(INVINCIBLE_FINISHED, spectatee);
-       STAT(SUPERWEAPONS_FINISHED, this) = STAT(SUPERWEAPONS_FINISHED, spectatee);
        STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee);
        STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
        STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
@@ -2064,23 +2024,6 @@ int nJoinAllowed(entity this, entity ignore)
        return free_slots;
 }
 
-/**
- * Checks whether the client is an observer or spectator, if so, he will get kicked after
- * g_maxplayers_spectator_blocktime seconds
- */
-void checkSpectatorBlock(entity this)
-{
-       if(IS_SPEC(this) || IS_OBSERVER(this))
-       if(!this.caplayer)
-       if(IS_REAL_CLIENT(this))
-       {
-               if( time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime) ) {
-                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
-                       dropclient(this);
-               }
-       }
-}
-
 void PrintWelcomeMessage(entity this)
 {
        if(CS(this).motd_actived_time == 0)
@@ -2298,6 +2241,7 @@ bool PlayerThink(entity this)
 }
 
 .bool would_spectate;
+// merged SpectatorThink and ObserverThink (old names are here so you can grep for them)
 void ObserverOrSpectatorThink(entity this)
 {
        bool is_spec = IS_SPEC(this);
@@ -2328,6 +2272,8 @@ void ObserverOrSpectatorThink(entity this)
                                TRANSMUTE(Observer, this);
                                PutClientInServer(this);
                        }
+                       else
+                               this.would_spectate = false; // unable to spectate anyone
                        if (is_spec)
                                CS(this).impulse = 0;
                } else if (is_spec) {
@@ -2351,10 +2297,13 @@ void ObserverOrSpectatorThink(entity this)
                        }
                }
                else {
-                       int preferred_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? CS_CVAR(this).cvar_cl_clippedspectating : !CS_CVAR(this).cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
+                       bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating;
+                       if (PHYS_INPUT_BUTTON_USE(this))
+                               wouldclip = !wouldclip;
+                       int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
                        set_movetype(this, preferred_movetype);
                }
-       } else {
+       } else { // jump pressed
                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;
@@ -2438,12 +2387,16 @@ void PlayerPreThink (entity this)
        if (frametime) {
                // physics frames: update anticheat stuff
                anticheat_prethink(this);
-       }
 
-       if (blockSpectators && frametime) {
                // WORKAROUND: only use dropclient in server frames (frametime set).
                // Never use it in cl_movement frames (frametime zero).
-               checkSpectatorBlock(this);
+               if (blockSpectators && IS_REAL_CLIENT(this)
+                       && (IS_SPEC(this) || IS_OBSERVER(this)) && !this.caplayer
+                       && 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;
@@ -2497,7 +2450,7 @@ void PlayerPreThink (entity this)
                this.max_armorvalue = 0;
        }
 
-       if (frametime && IS_PLAYER(this))
+       if (frametime && IS_PLAYER(this) && time >= game_starttime)
        {
                if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
                {
@@ -2620,15 +2573,6 @@ void PlayerPreThink (entity this)
        }
 
        target_voicescript_next(this);
-
-       // WEAPONTODO: Move into weaponsystem somehow
-       // if a player goes unarmed after holding a loaded weapon, empty his clip size and remove the crosshair ammo ring
-       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
-       {
-               .entity weaponentity = weaponentities[slot];
-               if(this.(weaponentity).m_weapon == WEP_Null)
-                       this.(weaponentity).clip_load = this.(weaponentity).clip_size = 0;
-       }
 }
 
 void DrownPlayer(entity this)
@@ -2689,22 +2633,39 @@ void PlayerPostThink (entity this)
 {
        Player_Physics(this);
 
-       if (autocvar_sv_maxidle > 0)
+       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_REAL_CLIENT(this))
-       if (IS_PLAYER(this) || autocvar_sv_maxidle_spectatorsareidle)
+       if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
+       if (!intermission_running) // NextLevel() kills all centerprints after setting this true
        {
                int totalClients = 0;
-               if(autocvar_sv_maxidle_slots > 0)
+               if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0)
                {
-                       FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
+                       // maxidle disabled in local matches by not counting clients (totalClients 0)
+                       if (server_is_dedicated)
+                       {
+                               FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
+                               {
+                                       ++totalClients;
+                               });
+                               if (maxclients - totalClients > autocvar_sv_maxidle_slots)
+                                       totalClients = 0;
+                       }
+               }
+               else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+               {
+                       FOREACH_CLIENT(IS_REAL_CLIENT(it),
                        {
                                ++totalClients;
                        });
                }
 
-               if (autocvar_sv_maxidle_slots > 0 && (maxclients - totalClients) > autocvar_sv_maxidle_slots)
-               { /* do nothing */ }
+               if (totalClients < autocvar_sv_maxidle_minplayers)
+               {
+                       // idle kick disabled
+                       CS(this).parm_idlesince = time;
+               }
                else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
                {
                        if (CS(this).idlekick_lasttimeleft)
@@ -2715,19 +2676,37 @@ void PlayerPostThink (entity this)
                }
                else
                {
-                       float timeleft = ceil(autocvar_sv_maxidle - (time - CS(this).parm_idlesince));
-                       if (timeleft == min(10, autocvar_sv_maxidle - 1)) { // - 1 to support sv_maxidle <= 10
-                               if (!CS(this).idlekick_lasttimeleft)
+                       float maxidle_time = autocvar_sv_maxidle;
+                       if (IS_PLAYER(this) && 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);
+                               else
                                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
                        }
                        if (timeleft <= 0) {
-                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname);
-                               dropclient(this);
+                               if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+                               {
+                                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
+                                       if (this.caplayer)
+                                               this.caplayer = 0;
+                                       this.lms_spectate_warning = 2; // TODO: mutator hook for players forcibly moved to spectator?
+                                       PutObserverInServer(this);
+                               }
+                               else
+                               {
+                                       if (dropclient_schedule(this))
+                                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time);
+                               }
                                return;
                        }
-                       else if (timeleft <= 10) {
+                       else if (timeleft <= countdown_time) {
                                if (timeleft != CS(this).idlekick_lasttimeleft)
-                                       Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_IDLE, timeleft));
+                                       play2(this, SND(TALK2));
                                CS(this).idlekick_lasttimeleft = timeleft;
                        }
                }
@@ -2756,12 +2735,12 @@ void PlayerPostThink (entity this)
                DrownPlayer(this);
                UpdateChatBubble(this);
                if (CS(this).impulse) ImpulseCommands(this);
+               GetPressedKeys(this);
                if (game_stopped)
                {
                        CSQCMODEL_AUTOUPDATE(this);
                        return;
                }
-               GetPressedKeys(this);
        }
        else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
        {