]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/client.qc
Merge branch 'master' into Lyberta/WaypointIcons
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / client.qc
index 909e89768b866bad6e8516c50ba7e66946301064..0a9a38fafe3c6b68666345b36b46a000c08a5246 100644 (file)
@@ -1,6 +1,7 @@
 #include "client.qh"
 
-#include <server/defs.qh>
+#include <common/weapons/_all.qh>
+#include <common/stats.qh>
 #include <server/miscfunctions.qh>
 #include <common/effects/all.qh>
 #include "anticheat.qh"
 #include "teamplay.qh"
 #include "spawnpoints.qh"
 #include "resources.qh"
-#include "g_damage.qh"
+#include "damage.qh"
 #include "handicap.qh"
-#include "g_hook.qh"
+#include "hook.qh"
 #include "command/common.qh"
 #include "command/vote.qh"
+#include "clientkill.qh"
 #include "cheats.qh"
-#include "g_world.qh"
+#include "world.qh"
+#include <server/gamelog.qh>
 #include "race.qh"
+#include <server/main.qh>
 #include "antilag.qh"
 #include "campaign.qh"
 #include "command/common.qh"
 #include "scores_rules.qh"
+#include "weapons/common.qh"
 
 #include "bot/api.qh"
 
 #include "../common/wepent.qh"
 #include <common/state.qh>
 
+#include "compat/quake3.qh"
+
 #include <common/effects/qc/globalsound.qh>
 
 #include "../common/mapobjects/func/conveyor.qh"
+#include <common/mapobjects/func/ladder.qh>
 #include "../common/mapobjects/teleporters.qh"
 #include "../common/mapobjects/target/spawnpoint.qh"
+#include <common/mapobjects/trigger/counter.qh>
+#include <common/mapobjects/trigger/swamp.qh>
 
 #include "../common/vehicles/all.qh"
 
 #include "weapons/hitplot.qh"
+#include "weapons/selection.qh"
 #include "weapons/weaponsystem.qh"
 
 #include "../common/net_notice.qh"
@@ -50,6 +61,8 @@
 
 #include "../common/items/_mod.qh"
 
+#include <common/gamemodes/gamemode/nexball/sv_nexball.qh>
+
 #include "../common/mutators/mutator/waypoints/all.qh"
 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
 #include <common/gamemodes/_mod.qh>
@@ -68,6 +81,8 @@
 
 #include <common/mutators/mutator/overkill/oknex.qh>
 
+#include <common/weapons/weapon/vortex.qh>
+
 STATIC_METHOD(Client, Add, void(Client this, int _team))
 {
     ClientConnect(this);
@@ -106,9 +121,13 @@ void WriteSpectators(entity player, entity to)
 {
        if(!player) { return; } // not sure how, but best to be safe
 
+       int spec_count = 0;
        FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
        {
+               if(spec_count >= MAX_SPECTATORS)
+                       break;
                WriteByte(MSG_ENTITY, num_for_edict(it));
+               ++spec_count;
        });
 }
 
@@ -228,7 +247,7 @@ void PutObserverInServer(entity this)
 
        if (IS_PLAYER(this))
        {
-               if(GetResourceAmount(this, RESOURCE_HEALTH) >= 1)
+               if(GetResource(this, RES_HEALTH) >= 1)
                {
                        // despawn effect
                        Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
@@ -270,7 +289,7 @@ void PutObserverInServer(entity this)
 
     RemoveGrapplingHooks(this);
        Portal_ClearAll(this);
-       Unfreeze(this);
+       Unfreeze(this, false);
        SetSpectatee(this, NULL);
 
        if (this.alivetime)
@@ -284,25 +303,11 @@ void PutObserverInServer(entity this)
 
        WaypointSprite_PlayerDead(this);
 
-       if (mutator_returnvalue) {
-           // mutator prevents resetting teams+score
-       } else {
-               Player_SetTeamIndex(this, -1);
-               this.frags = FRAGS_SPECTATOR;
-        PlayerScore_Clear(this);  // clear scores when needed
-    }
-
        if (CS(this).killcount != FRAGS_SPECTATOR)
        {
-               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE, this.netname);
                if(!game_stopped)
                if(autocvar_g_chat_nospectators == 1 || (!warmup_stage && autocvar_g_chat_nospectators == 2))
                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS);
-
-               if(!CS(this).just_joined)
-                       LogTeamchange(this.playerid, -1, TEAM_CHANGE_SPECTATOR);
-               else
-                       CS(this).just_joined = false;
        }
 
        accuracy_resend(this);
@@ -321,14 +326,14 @@ void PutObserverInServer(entity this)
        if(this.damagedbycontents)
                IL_REMOVE(g_damagedbycontents, this);
        this.damagedbycontents = false;
-       SetResourceAmountExplicit(this, RESOURCE_HEALTH, FRAGS_SPECTATOR);
+       SetResourceExplicit(this, RES_HEALTH, FRAGS_SPECTATOR);
        SetSpectatee_status(this, etof(this));
        this.takedamage = DAMAGE_NO;
        this.solid = SOLID_NOT;
        set_movetype(this, MOVETYPE_FLY_WORLDONLY); // user preference is controlled by playerprethink
        this.flags = FL_CLIENT | FL_NOTARGET;
        this.effects = 0;
-       SetResourceAmountExplicit(this, RESOURCE_ARMOR, autocvar_g_balance_armor_start); // was 666?!
+       SetResourceExplicit(this, RES_ARMOR, autocvar_g_balance_armor_start); // was 666?!
        this.pauserotarmor_finished = 0;
        this.pauserothealth_finished = 0;
        this.pauseregen_finished = 0;
@@ -340,20 +345,24 @@ void PutObserverInServer(entity this)
        this.alpha = 0;
        this.scale = 0;
        this.fade_time = 0;
-       this.pain_frame = 0;
        this.pain_finished = 0;
-       this.strength_finished = 0;
-       this.invincible_finished = 0;
-       this.superweapons_finished = 0;
-       this.dphitcontentsmask = 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;
+       if (autocvar_g_playerclip_collisions)
+               this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP;
        this.pushltime = 0;
        this.istypefrag = 0;
        setthink(this, func_null);
        this.nextthink = 0;
        this.deadflag = DEAD_NO;
-       this.crouch = false;
+       UNSET_DUCKED(this);
        STAT(REVIVE_PROGRESS, this) = 0;
        this.revival_time = 0;
+       this.draggable = drag_undraggable;
 
        this.items = 0;
        STAT(WEAPONS, this) = '0 0 0';
@@ -387,6 +396,18 @@ void PutObserverInServer(entity this)
                if(axh.owner == this && axh != NULL && !wasfreed(axh))
                        delete(axh);
        }
+
+       if (mutator_returnvalue)
+       {
+               // mutator prevents resetting teams+score
+       }
+       else
+       {
+               SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR);
+               this.frags = FRAGS_SPECTATOR;
+       }
+       if (CS(this).just_joined)
+               CS(this).just_joined = false;
 }
 
 int player_getspecies(entity this)
@@ -551,24 +572,24 @@ void PutPlayerInServer(entity this)
        this.effects = EF_TELEPORT_BIT | EF_RESTARTANIM_BIT;
 
        if (warmup_stage) {
-               SetResourceAmount(this, RESOURCE_SHELLS, warmup_start_ammo_shells);
-               SetResourceAmount(this, RESOURCE_BULLETS, warmup_start_ammo_nails);
-               SetResourceAmount(this, RESOURCE_ROCKETS, warmup_start_ammo_rockets);
-               SetResourceAmount(this, RESOURCE_CELLS, warmup_start_ammo_cells);
-               SetResourceAmount(this, RESOURCE_PLASMA, warmup_start_ammo_plasma);
-               SetResourceAmount(this, RESOURCE_FUEL, warmup_start_ammo_fuel);
-               SetResourceAmount(this, RESOURCE_HEALTH, warmup_start_health);
-               SetResourceAmount(this, RESOURCE_ARMOR, warmup_start_armorvalue);
+               SetResource(this, RES_SHELLS, warmup_start_ammo_shells);
+               SetResource(this, RES_BULLETS, warmup_start_ammo_nails);
+               SetResource(this, RES_ROCKETS, warmup_start_ammo_rockets);
+               SetResource(this, RES_CELLS, warmup_start_ammo_cells);
+               SetResource(this, RES_PLASMA, warmup_start_ammo_plasma);
+               SetResource(this, RES_FUEL, warmup_start_ammo_fuel);
+               SetResource(this, RES_HEALTH, warmup_start_health);
+               SetResource(this, RES_ARMOR, warmup_start_armorvalue);
                STAT(WEAPONS, this) = WARMUP_START_WEAPONS;
        } else {
-               SetResourceAmount(this, RESOURCE_SHELLS, start_ammo_shells);
-               SetResourceAmount(this, RESOURCE_BULLETS, start_ammo_nails);
-               SetResourceAmount(this, RESOURCE_ROCKETS, start_ammo_rockets);
-               SetResourceAmount(this, RESOURCE_CELLS, start_ammo_cells);
-               SetResourceAmount(this, RESOURCE_PLASMA, start_ammo_plasma);
-               SetResourceAmount(this, RESOURCE_FUEL, start_ammo_fuel);
-               SetResourceAmount(this, RESOURCE_HEALTH, start_health);
-               SetResourceAmount(this, RESOURCE_ARMOR, start_armorvalue);
+               SetResource(this, RES_SHELLS, start_ammo_shells);
+               SetResource(this, RES_BULLETS, start_ammo_nails);
+               SetResource(this, RES_ROCKETS, start_ammo_rockets);
+               SetResource(this, RES_CELLS, start_ammo_cells);
+               SetResource(this, RES_PLASMA, start_ammo_plasma);
+               SetResource(this, RES_FUEL, start_ammo_fuel);
+               SetResource(this, RES_HEALTH, start_health);
+               SetResource(this, RES_ARMOR, start_armorvalue);
                STAT(WEAPONS, this) = start_weapons;
                if (MUTATOR_CALLHOOK(ForbidRandomStartWeapons, this) == false)
                {
@@ -580,7 +601,7 @@ void PutPlayerInServer(entity this)
 
        PS(this).dual_weapons = '0 0 0';
 
-       this.superweapons_finished = (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
+       STAT(SUPERWEAPONS_FINISHED, this) = (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
 
        this.items = start_items;
 
@@ -598,14 +619,14 @@ void PutPlayerInServer(entity this)
                this.pauseregen_finished += f;
        }
 
-       this.damageforcescale = 2;
+       this.damageforcescale = autocvar_g_player_damageforcescale;
        this.death_time = 0;
        this.respawn_flags = 0;
        this.respawn_time = 0;
        STAT(RESPAWN_TIME, this) = 0;
-       this.scale = autocvar_sv_player_scale;
+       bool q3dfcompat = autocvar_sv_q3defragcompat && autocvar_sv_q3defragcompat_changehitbox;
+       this.scale = ((q3dfcompat) ? 0.9 : autocvar_sv_player_scale);
        this.fade_time = 0;
-       this.pain_frame = 0;
        this.pain_finished = 0;
        this.pushltime = 0;
        setthink(this, func_null); // players have no think function
@@ -618,20 +639,27 @@ void PutPlayerInServer(entity this)
        this.angles = spot.angles;
        this.angles_z = 0; // never spawn tilted even if the spot says to
        if (IS_BOT_CLIENT(this))
+       {
                this.v_angle = this.angles;
+               bot_aim_reset(this);
+       }
        this.fixangle = true; // turn this way immediately
        this.oldvelocity = this.velocity = '0 0 0';
        this.avelocity = '0 0 0';
        this.punchangle = '0 0 0';
        this.punchvector = '0 0 0';
 
-       this.strength_finished = 0;
-       this.invincible_finished = 0;
+       STAT(STRENGTH_FINISHED, this) = 0;
+       STAT(INVINCIBLE_FINISHED, this) = 0;
        this.fire_endtime = -1;
        STAT(REVIVE_PROGRESS, this) = 0;
        this.revival_time = 0;
 
-       this.air_finished = time + 12;
+       // 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;
 
@@ -659,7 +687,7 @@ void PutPlayerInServer(entity this)
 
        this.spawnpoint_targ = NULL;
 
-       this.crouch = false;
+       UNSET_DUCKED(this);
        this.view_ofs = STAT(PL_VIEW_OFS, this);
        setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
        this.spawnorigin = spot.origin;
@@ -669,11 +697,24 @@ void PutPlayerInServer(entity this)
        if(this.conveyor)
                IL_REMOVE(g_conveyed, this);
        this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player
+       if(this.swampslug)
+               IL_REMOVE(g_swamped, this);
+       this.swampslug = NULL;
+       this.swamp_interval = 0;
+       if(this.ladder_entity)
+               IL_REMOVE(g_ladderents, this);
+       this.ladder_entity = NULL;
+       IL_EACH(g_counters, it.realowner == this,
+       {
+               delete(it);
+       });
        STAT(HUD, this) = HUD_NORMAL;
 
        this.event_damage = PlayerDamage;
        this.event_heal = PlayerHeal;
 
+       this.draggable = func_null;
+
        if(!this.bot_attack)
                IL_PUSH(g_bot_targets, this);
        this.bot_attack = true;
@@ -694,10 +735,7 @@ void PutPlayerInServer(entity this)
        for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
                .entity weaponentity = weaponentities[slot];
-               entity oldwep = this.(weaponentity);
                CL_SpawnWeaponentity(this, weaponentity);
-               if(oldwep && oldwep.owner == this)
-                       this.(weaponentity).m_gunalign = oldwep.m_gunalign;
        }
        this.alpha = default_player_alpha;
        this.colormod = '1 1 1' * autocvar_g_player_brightness;
@@ -705,6 +743,9 @@ void PutPlayerInServer(entity this)
 
        this.speedrunning = false;
 
+       this.counter_cnt = 0;
+       this.fragsfilter_cnt = 0;
+
        target_voicescript_clear(this);
 
        // reset fields the weapons may use
@@ -722,12 +763,14 @@ void PutPlayerInServer(entity this)
 
        {
                string s = spot.target;
-               spot.target = string_null;
+               if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack
+                       spot.target = string_null;
                SUB_UseTargets(spot, this, NULL);
-               spot.target = s;
+               if(g_assault || g_race)
+                       spot.target = s;
        }
 
-       Unfreeze(this);
+       Unfreeze(this, false);
 
        MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
 
@@ -754,6 +797,7 @@ void PutPlayerInServer(entity this)
 
        if (CS(this).impulse) ImpulseCommands(this);
 
+       W_ResetGunAlign(this, CS(this).cvar_cl_gunalign);
        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
                .entity weaponentity = weaponentities[slot];
@@ -900,204 +944,6 @@ void DecodeLevelParms(entity this)
        MUTATOR_CALLHOOK(DecodeLevelParms);
 }
 
-/*
-=============
-ClientKill
-
-Called when a client types 'kill' in the console
-=============
-*/
-
-.float clientkill_nexttime;
-void ClientKill_Now_TeamChange(entity this)
-{
-       if(this.killindicator_teamchange == -1)
-       {
-               TeamBalance_JoinBestTeam(this);
-       }
-       else if(this.killindicator_teamchange == -2)
-       {
-               if(blockSpectators)
-                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
-               PutObserverInServer(this);
-       }
-       else
-               SV_ChangeTeam(this, this.killindicator_teamchange - 1);
-       this.killindicator_teamchange = 0;
-}
-
-void ClientKill_Now(entity this)
-{
-       if(this.vehicle)
-       {
-           vehicles_exit(this.vehicle, VHEF_RELEASE);
-           if(!this.killindicator_teamchange)
-           {
-            this.vehicle_health = -1;
-            Damage(this, this, this, 1 , DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
-           }
-       }
-
-       if(this.killindicator && !wasfreed(this.killindicator))
-               delete(this.killindicator);
-
-       this.killindicator = NULL;
-
-       if(this.killindicator_teamchange)
-               ClientKill_Now_TeamChange(this);
-
-       if (!IS_SPEC(this) && !IS_OBSERVER(this) && MUTATOR_CALLHOOK(ClientKill_Now, this) == false)
-       {
-               Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
-       }
-
-       // now I am sure the player IS dead
-}
-void KillIndicator_Think(entity this)
-{
-       if (game_stopped)
-       {
-               this.owner.killindicator = NULL;
-               delete(this);
-               return;
-       }
-
-       if (this.owner.alpha < 0 && !this.owner.vehicle)
-       {
-               this.owner.killindicator = NULL;
-               delete(this);
-               return;
-       }
-
-       if(this.cnt <= 0)
-       {
-               ClientKill_Now(this.owner);
-               return;
-       }
-    else if(this.count == 1) // count == 1 means that it's silent
-    {
-        this.nextthink = time + 1;
-        this.cnt -= 1;
-    }
-       else
-       {
-               if(this.cnt <= 10)
-                       setmodel(this, MDL_NUM(this.cnt));
-               if(IS_REAL_CLIENT(this.owner))
-               {
-                       if(this.cnt <= 10)
-                               { Send_Notification(NOTIF_ONE, this.owner, MSG_ANNCE, Announcer_PickNumber(CNT_KILL, this.cnt)); }
-               }
-               this.nextthink = time + 1;
-               this.cnt -= 1;
-       }
-}
-
-float clientkilltime;
-void ClientKill_TeamChange (entity this, float targetteam) // 0 = don't change, -1 = auto, -2 = spec
-{
-       float killtime;
-       float starttime;
-
-       if (game_stopped)
-               return;
-
-       killtime = autocvar_g_balance_kill_delay;
-
-    if(MUTATOR_CALLHOOK(ClientKill, this, killtime))
-       return;
-    killtime = M_ARGV(1, float);
-
-       this.killindicator_teamchange = targetteam;
-
-    if(!this.killindicator)
-       {
-               if(!IS_DEAD(this))
-               {
-                       killtime = max(killtime, this.clientkill_nexttime - time);
-                       this.clientkill_nexttime = time + killtime + autocvar_g_balance_kill_antispam;
-               }
-
-               if(killtime <= 0 || !IS_PLAYER(this) || IS_DEAD(this))
-               {
-                       ClientKill_Now(this);
-               }
-               else
-               {
-                       starttime = max(time, clientkilltime);
-
-                       this.killindicator = spawn();
-                       this.killindicator.owner = this;
-                       this.killindicator.scale = 0.5;
-                       setattachment(this.killindicator, this, "");
-                       setorigin(this.killindicator, '0 0 52');
-                       setthink(this.killindicator, KillIndicator_Think);
-                       this.killindicator.nextthink = starttime + (this.lip) * 0.05;
-                       clientkilltime = max(clientkilltime, this.killindicator.nextthink + 0.05);
-                       this.killindicator.cnt = ceil(killtime);
-                       this.killindicator.count = bound(0, ceil(killtime), 10);
-                       //sprint(this, strcat("^1You'll be dead in ", ftos(this.killindicator.cnt), " seconds\n"));
-
-                       IL_EACH(g_clones, it.enemy == this && !(it.effects & CSQCMODEL_EF_RESPAWNGHOST),
-                       {
-                               it.killindicator = spawn();
-                               it.killindicator.owner = it;
-                               it.killindicator.scale = 0.5;
-                               setattachment(it.killindicator, it, "");
-                               setorigin(it.killindicator, '0 0 52');
-                               setthink(it.killindicator, KillIndicator_Think);
-                               it.killindicator.nextthink = starttime + (it.lip) * 0.05;
-                               //clientkilltime = max(clientkilltime, it.killindicator.nextthink + 0.05);
-                               it.killindicator.cnt = ceil(killtime);
-                       });
-                       this.lip = 0;
-               }
-       }
-       if(this.killindicator)
-       {
-               if(targetteam == 0) // just die
-               {
-                       this.killindicator.colormod = '0 0 0';
-                       if(IS_REAL_CLIENT(this))
-                       if(this.killindicator.cnt > 0)
-                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_TEAMCHANGE_SUICIDE, this.killindicator.cnt);
-               }
-               else if(targetteam == -1) // auto
-               {
-                       this.killindicator.colormod = '0 1 0';
-                       if(IS_REAL_CLIENT(this))
-                       if(this.killindicator.cnt > 0)
-                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_TEAMCHANGE_AUTO, this.killindicator.cnt);
-               }
-               else if(targetteam == -2) // spectate
-               {
-                       this.killindicator.colormod = '0.5 0.5 0.5';
-                       if(IS_REAL_CLIENT(this))
-                       if(this.killindicator.cnt > 0)
-                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_TEAMCHANGE_SPECTATE, this.killindicator.cnt);
-               }
-               else
-               {
-                       this.killindicator.colormod = Team_ColorRGB(targetteam);
-                       if(IS_REAL_CLIENT(this))
-                       if(this.killindicator.cnt > 0)
-                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(targetteam, CENTER_TEAMCHANGE), this.killindicator.cnt);
-               }
-       }
-
-}
-
-void ClientKill (entity this)
-{
-       // TODO: once .health is removed, will need to check it here for the "already dead" message!
-
-       if(game_stopped) return;
-       if(this.player_blocked) return;
-       if(STAT(FROZEN, this)) return;
-
-       ClientKill_TeamChange(this, 0);
-}
-
 void FixClientCvars(entity e)
 {
        // send prediction settings to the client
@@ -1215,7 +1061,9 @@ string getwelcomemessage(entity this)
        modifications = substring(modifications, 2, strlen(modifications) - 2);
 
        string versionmessage = GetClientVersionMessage(this);
-       string s = strcat(versionmessage, "^8\n^8\nmatch type is ^1", gamemode_name, "^8\n");
+       string s = strcat(versionmessage, "^8\n^8\nhost is ^9", autocvar_hostname, "^8\n");
+
+       s = strcat(s, "^8\nmatch type is ^1", gamemode_name, "^8\n");
 
        if(modifications != "")
                s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
@@ -1243,6 +1091,8 @@ string getwelcomemessage(entity this)
        return s;
 }
 
+bool autocvar_sv_qcphysics = true; // TODO this is for testing - remove when qcphysics work
+
 /**
 =============
 ClientConnect
@@ -1267,38 +1117,7 @@ void ClientConnect(entity this)
 
        bot_clientconnect(this);
 
-       // identify the right forced team
-       if (autocvar_g_campaign)
-       {
-               if (IS_REAL_CLIENT(this)) // only players, not bots
-               {
-                       switch (autocvar_g_campaign_forceteam)
-                       {
-                               case 1: this.team_forced = NUM_TEAM_1; break;
-                               case 2: this.team_forced = NUM_TEAM_2; break;
-                               case 3: this.team_forced = NUM_TEAM_3; break;
-                               case 4: this.team_forced = NUM_TEAM_4; break;
-                               default: this.team_forced = 0;
-                       }
-               }
-       }
-       else if (PlayerInList(this, autocvar_g_forced_team_red))    this.team_forced = NUM_TEAM_1;
-       else if (PlayerInList(this, autocvar_g_forced_team_blue))   this.team_forced = NUM_TEAM_2;
-       else if (PlayerInList(this, autocvar_g_forced_team_yellow)) this.team_forced = NUM_TEAM_3;
-       else if (PlayerInList(this, autocvar_g_forced_team_pink))   this.team_forced = NUM_TEAM_4;
-       else switch (autocvar_g_forced_team_otherwise)
-       {
-               default: this.team_forced = 0; break;
-               case "red": this.team_forced = NUM_TEAM_1; break;
-               case "blue": this.team_forced = NUM_TEAM_2; break;
-               case "yellow": this.team_forced = NUM_TEAM_3; break;
-               case "pink": this.team_forced = NUM_TEAM_4; break;
-               case "spectate":
-               case "spectator":
-                       this.team_forced = -1;
-                       break;
-       }
-       if (!teamplay && this.team_forced > 0) this.team_forced = 0;
+       Player_DetermineForcedTeam(this);
 
        TRANSMUTE(Observer, this);
 
@@ -1311,7 +1130,7 @@ void ClientConnect(entity this)
                CS(this).allowed_timeouts = autocvar_sv_timeout_number;
 
        if (autocvar_sv_eventlog)
-               GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? this.netaddress : "bot"), ":", playername(this, false)));
+               GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this, false)));
 
        CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
 
@@ -1366,6 +1185,8 @@ void ClientConnect(entity this)
        if (IS_REAL_CLIENT(this))
                sv_notice_join(this);
 
+       this.move_qcphysics = autocvar_sv_qcphysics;
+
        // update physics stats (players can spawn before physics runs)
        Physics_UpdateStats(this);
 
@@ -1419,7 +1240,7 @@ void ClientDisconnect(entity this)
 
        Portal_ClearAll(this);
 
-       Unfreeze(this);
+       Unfreeze(this, false);
 
        RemoveGrapplingHooks(this);
 
@@ -1430,6 +1251,11 @@ void ClientDisconnect(entity this)
        if (this.chatbubbleentity) delete(this.chatbubbleentity);
        if (this.killindicator) delete(this.killindicator);
 
+       IL_EACH(g_counters, it.realowner == this,
+       {
+               delete(it);
+       });
+
        WaypointSprite_PlayerGone(this);
 
        bot_relinkplayerlist();
@@ -1459,7 +1285,7 @@ void ChatBubbleThink(entity this)
 
        if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
        {
-               if ( CS(this.owner).active_minigame )
+               if ( CS(this.owner).active_minigame && PHYS_INPUT_BUTTON_MINIGAME(this.owner) )
                        this.mdl = "models/sprites/minigame_busy.iqm";
                else if (PHYS_INPUT_BUTTON_CHAT(this.owner))
                        this.mdl = "models/misc/chatbubble.spr";
@@ -1492,6 +1318,91 @@ void UpdateChatBubble(entity this)
        }
 }
 
+void calculate_player_respawn_time(entity this)
+{
+       if(MUTATOR_CALLHOOK(CalculateRespawnTime, this))
+               return;
+
+       float gametype_setting_tmp;
+       float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
+       float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
+       float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
+       float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
+       float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
+       float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
+
+       float pcount = 1;  // Include myself whether or not team is already set right and I'm a "player".
+       if (teamplay)
+       {
+               FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
+                       if(it.team == this.team)
+                               ++pcount;
+               });
+               if (sdelay_small_count == 0)
+                       sdelay_small_count = 1;
+               if (sdelay_large_count == 0)
+                       sdelay_large_count = 1;
+       }
+       else
+       {
+               FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
+                       ++pcount;
+               });
+               if (sdelay_small_count == 0)
+               {
+                       if (IS_INDEPENDENT_PLAYER(this))
+                       {
+                               // Players play independently. No point in requiring enemies.
+                               sdelay_small_count = 1;
+                       }
+                       else
+                       {
+                               // Players play AGAINST each other. Enemies required.
+                               sdelay_small_count = 2;
+                       }
+               }
+               if (sdelay_large_count == 0)
+               {
+                       if (IS_INDEPENDENT_PLAYER(this))
+                       {
+                               // Players play independently. No point in requiring enemies.
+                               sdelay_large_count = 1;
+                       }
+                       else
+                       {
+                               // Players play AGAINST each other. Enemies required.
+                               sdelay_large_count = 2;
+                       }
+               }
+       }
+
+       float sdelay;
+
+       if (pcount <= sdelay_small_count)
+               sdelay = sdelay_small;
+       else if (pcount >= sdelay_large_count)
+               sdelay = sdelay_large;
+       else  // NOTE: this case implies sdelay_large_count > sdelay_small_count.
+               sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
+
+       if(waves)
+               this.respawn_time = ceil((time + sdelay) / waves) * waves;
+       else
+               this.respawn_time = time + sdelay;
+
+       if(sdelay < sdelay_max)
+               this.respawn_time_max = time + sdelay_max;
+       else
+               this.respawn_time_max = this.respawn_time;
+
+       if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
+               this.respawn_countdown = 10; // first number to count down from is 10
+       else
+               this.respawn_countdown = -1; // do not count down
+
+       if(autocvar_g_forced_respawn)
+               this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
+}
 
 // LordHavoc: this hack will be removed when proper _pants/_shirt layers are
 // added to the model skins
@@ -1511,53 +1422,67 @@ void UpdateChatBubble(entity this)
 
 void respawn(entity this)
 {
-       if(this.alpha >= 0 && autocvar_g_respawn_ghosts)
+       bool damagedbycontents_prev = this.damagedbycontents;
+       if(this.alpha >= 0)
        {
-               this.solid = SOLID_NOT;
-               this.takedamage = DAMAGE_NO;
-               set_movetype(this, MOVETYPE_FLY);
-               this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
-               this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
-               this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
-               Send_Effect(EFFECT_RESPAWN_GHOST, this.origin, '0 0 0', 1);
-               if(autocvar_g_respawn_ghosts_maxtime)
-                       SUB_SetFade (this, time + autocvar_g_respawn_ghosts_maxtime / 2 + random () * (autocvar_g_respawn_ghosts_maxtime - autocvar_g_respawn_ghosts_maxtime / 2), 1.5);
+               if(autocvar_g_respawn_ghosts)
+               {
+                       this.solid = SOLID_NOT;
+                       this.takedamage = DAMAGE_NO;
+                       this.damagedbycontents = false;
+                       set_movetype(this, MOVETYPE_FLY);
+                       this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
+                       this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
+                       this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
+                       this.alpha = min(this.alpha, autocvar_g_respawn_ghosts_alpha);
+                       Send_Effect(EFFECT_RESPAWN_GHOST, this.origin, '0 0 0', 1);
+                       if(autocvar_g_respawn_ghosts_time > 0)
+                               SUB_SetFade(this, time + autocvar_g_respawn_ghosts_time, autocvar_g_respawn_ghosts_fadetime);
+               }
+               else
+                       SUB_SetFade (this, time, 1); // fade out the corpse immediately
        }
 
        CopyBody(this, 1);
+       this.damagedbycontents = damagedbycontents_prev;
 
        this.effects |= EF_NODRAW; // prevent another CopyBody
        PutClientInServer(this);
 }
 
+ERASEABLE
 void PrintToChat(entity client, string text)
 {
        text = strcat("\{1}^7", text, "\n");
        sprint(client, text);
 }
 
+ERASEABLE
 void DebugPrintToChat(entity client, string text)
 {
-       if (autocvar_developer)
+       if (autocvar_developer > 0)
        {
                PrintToChat(client, text);
        }
 }
 
+ERASEABLE
 void PrintToChatAll(string text)
 {
        text = strcat("\{1}^7", text, "\n");
        bprint(text);
 }
 
+ERASEABLE
 void DebugPrintToChatAll(string text)
 {
-       if (autocvar_developer)
+       if (autocvar_developer > 0)
        {
                PrintToChatAll(text);
        }
 }
 
+ERASEABLE
 void PrintToChatTeam(int team_num, string text)
 {
        text = strcat("\{1}^7", text, "\n");
@@ -1570,9 +1495,10 @@ void PrintToChatTeam(int team_num, string text)
        });
 }
 
+ERASEABLE
 void DebugPrintToChatTeam(int team_num, string text)
 {
-       if (autocvar_developer)
+       if (autocvar_developer > 0)
        {
                PrintToChatTeam(team_num, text);
        }
@@ -1589,9 +1515,6 @@ void play_countdown(entity this, float finished, Sound samp)
 
 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;
-
        if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped)
                this.modelflags |= MF_ROCKET;
        else
@@ -1599,9 +1522,24 @@ void player_powerups(entity this)
 
        this.effects &= ~(EF_RED | EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT | EF_FLAME | 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);
+               }
+       }
+
        if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed
                return;
 
+       // 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);
 
@@ -1609,9 +1547,9 @@ void player_powerups(entity this)
        {
                if (this.items & ITEM_Strength.m_itemid)
                {
-                       play_countdown(this, this.strength_finished, SND_POWEROFF);
+                       play_countdown(this, STAT(STRENGTH_FINISHED, this), SND_POWEROFF);
                        this.effects = this.effects | (EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT);
-                       if (time > this.strength_finished)
+                       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);
@@ -1620,7 +1558,7 @@ void player_powerups(entity this)
                }
                else
                {
-                       if (time < this.strength_finished)
+                       if (time < STAT(STRENGTH_FINISHED, this))
                        {
                                this.items = this.items | ITEM_Strength.m_itemid;
                                if(!g_cts)
@@ -1630,9 +1568,9 @@ void player_powerups(entity this)
                }
                if (this.items & ITEM_Shield.m_itemid)
                {
-                       play_countdown(this, this.invincible_finished, SND_POWEROFF);
+                       play_countdown(this, STAT(INVINCIBLE_FINISHED, this), SND_POWEROFF);
                        this.effects = this.effects | (EF_RED | EF_ADDITIVE | EF_FULLBRIGHT);
-                       if (time > this.invincible_finished)
+                       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);
@@ -1641,7 +1579,7 @@ void player_powerups(entity this)
                }
                else
                {
-                       if (time < this.invincible_finished)
+                       if (time < STAT(INVINCIBLE_FINISHED, this))
                        {
                                this.items = this.items | ITEM_Shield.m_itemid;
                                if(!g_cts)
@@ -1653,7 +1591,7 @@ void player_powerups(entity this)
                {
                        if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
                        {
-                               this.superweapons_finished = 0;
+                               STAT(SUPERWEAPONS_FINISHED, this) = 0;
                                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);
@@ -1664,8 +1602,8 @@ void player_powerups(entity this)
                        }
                        else
                        {
-                               play_countdown(this, this.superweapons_finished, SND_POWEROFF);
-                               if (time > this.superweapons_finished)
+                               play_countdown(this, STAT(SUPERWEAPONS_FINISHED, this), SND_POWEROFF);
+                               if (time > STAT(SUPERWEAPONS_FINISHED, this))
                                {
                                        this.items = this.items - (this.items & IT_SUPERWEAPON);
                                        STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
@@ -1676,22 +1614,25 @@ void player_powerups(entity this)
                }
                else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
                {
-                       if (time < this.superweapons_finished || (this.items & IT_UNLIMITED_SUPERWEAPONS))
+                       if (time < STAT(SUPERWEAPONS_FINISHED, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
                        {
                                this.items = this.items | IT_SUPERWEAPON;
-                               if(!g_cts)
-                                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_PICKUP, this.netname);
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_PICKUP);
+                               if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
+                               {
+                                       if(!g_cts)
+                                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_PICKUP, this.netname);
+                                       Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_PICKUP);
+                               }
                        }
                        else
                        {
-                               this.superweapons_finished = 0;
+                               STAT(SUPERWEAPONS_FINISHED, this) = 0;
                                STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
                        }
                }
                else
                {
-                       this.superweapons_finished = 0;
+                       STAT(SUPERWEAPONS_FINISHED, this) = 0;
                }
        }
 
@@ -1728,8 +1669,10 @@ float CalcRot(float current, float stable, float rotfactor, float rotframetime)
                return max(stable, current + (stable - current) * rotfactor * rotframetime);
 }
 
-float CalcRotRegen(float current, float regenstable, float regenfactor, float regenlinear, float regenframetime, float rotstable, float rotfactor, float rotlinear, float rotframetime, float limit)
+void RotRegen(entity this, int res, float regenstable, float regenfactor, float regenlinear, float regenframetime, float rotstable, float rotfactor, float rotlinear, float rotframetime, float limit_mod)
 {
+       float old = GetResource(this, res);
+       float current = old;
        if(current > rotstable)
        {
                if(rotframetime > 0)
@@ -1747,10 +1690,12 @@ float CalcRotRegen(float current, float regenstable, float regenfactor, float re
                }
        }
 
+       float limit = GetResourceLimit(this, res) * limit_mod;
        if(current > limit)
                current = limit;
 
-       return current;
+       if (current != old)
+               SetResource(this, res, current);
 }
 
 void player_regen(entity this)
@@ -1780,28 +1725,21 @@ void player_regen(entity this)
        if(!mutator_returnvalue)
        if(!STAT(FROZEN, this))
        {
-               float mina, maxa, limith, limita;
-               maxa = autocvar_g_balance_armor_rotstable;
-               mina = autocvar_g_balance_armor_regenstable;
-               limith = GetResourceLimit(this, RESOURCE_HEALTH);
-               limita = GetResourceLimit(this, RESOURCE_ARMOR);
+               float maxa = autocvar_g_balance_armor_rotstable;
+               float mina = autocvar_g_balance_armor_regenstable;
 
-               regen_health_rotstable = regen_health_rotstable * max_mod;
-               regen_health_stable = regen_health_stable * max_mod;
-               limith = limith * limit_mod;
-               limita = limita * limit_mod;
+               RotRegen(this, RES_ARMOR, mina, autocvar_g_balance_armor_regen, autocvar_g_balance_armor_regenlinear,
+                       regen_mod * frametime * (time > this.pauseregen_finished), maxa, autocvar_g_balance_armor_rot, autocvar_g_balance_armor_rotlinear,
+                       rot_mod * frametime * (time > this.pauserotarmor_finished), limit_mod);
 
-               SetResourceAmount(this, RESOURCE_ARMOR, CalcRotRegen(GetResourceAmount(this, RESOURCE_ARMOR), mina, autocvar_g_balance_armor_regen, autocvar_g_balance_armor_regenlinear, 
-                                                                       regen_mod * frametime * (time > this.pauseregen_finished), maxa, autocvar_g_balance_armor_rot, autocvar_g_balance_armor_rotlinear,
-                                                                       rot_mod * frametime * (time > this.pauserotarmor_finished), limita));
-               SetResourceAmount(this, RESOURCE_HEALTH, CalcRotRegen(GetResourceAmount(this, RESOURCE_HEALTH), regen_health_stable, regen_health, regen_health_linear,
-                                                                       regen_mod * frametime * (time > this.pauseregen_finished), regen_health_rotstable, regen_health_rot, regen_health_rotlinear,
-                                                                       rot_mod * frametime * (time > this.pauserothealth_finished), limith));
+               RotRegen(this, RES_HEALTH, regen_health_stable * max_mod, regen_health, regen_health_linear,
+                       regen_mod * frametime * (time > this.pauseregen_finished), regen_health_rotstable * max_mod, regen_health_rot, regen_health_rotlinear,
+                       rot_mod * frametime * (time > this.pauserothealth_finished), limit_mod);
        }
 
        // if player rotted to death...  die!
        // check this outside above checks, as player may still be able to rot to death
-       if(GetResourceAmount(this, RESOURCE_HEALTH) < 1)
+       if(GetResource(this, RES_HEALTH) < 1)
        {
                if(this.vehicle)
                        vehicles_exit(this.vehicle, VHEF_RELEASE);
@@ -1809,17 +1747,14 @@ void player_regen(entity this)
                        this.event_damage(this, this, this, 1, DEATH_ROT.m_id, DMG_NOWEP, this.origin, '0 0 0');
        }
 
-       if (!(this.items & IT_UNLIMITED_WEAPON_AMMO))
+       if (!(this.items & IT_UNLIMITED_AMMO))
        {
-               float minf, maxf, limitf;
-
-               maxf = autocvar_g_balance_fuel_rotstable;
-               minf = autocvar_g_balance_fuel_regenstable;
-               limitf = GetResourceLimit(this, RESOURCE_FUEL);
+               float maxf = autocvar_g_balance_fuel_rotstable;
+               float minf = autocvar_g_balance_fuel_regenstable;
 
-               SetResourceAmount(this, RESOURCE_FUEL, CalcRotRegen(GetResourceAmount(this, RESOURCE_FUEL), minf, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear, 
-                                                                               frametime * (time > this.pauseregen_finished) * ((this.items & ITEM_JetpackRegen.m_itemid) != 0),
-                                                                               maxf, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, frametime * (time > this.pauserotfuel_finished), limitf));
+               RotRegen(this, RES_FUEL, minf, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear,
+                       frametime * (time > this.pauseregen_finished) * ((this.items & ITEM_JetpackRegen.m_itemid) != 0),
+                       maxf, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, frametime * (time > this.pauserotfuel_finished), 1);
        }
 }
 
@@ -1844,7 +1779,7 @@ void GetPressedKeys(entity this)
        keys = BITSET(keys, KEY_LEFT,           CS(this).movement.y < 0);
 
        keys = BITSET(keys, KEY_JUMP,           PHYS_INPUT_BUTTON_JUMP(this));
-       keys = BITSET(keys, KEY_CROUCH,         PHYS_INPUT_BUTTON_CROUCH(this));
+       keys = BITSET(keys, KEY_CROUCH,         IS_DUCKED(this)); // workaround: player can't un-crouch until their path is clear, so we keep the button held here
        keys = BITSET(keys, KEY_ATCK,           PHYS_INPUT_BUTTON_ATCK(this));
        keys = BITSET(keys, KEY_ATCK2,          PHYS_INPUT_BUTTON_ATCK2(this));
        CS(this).pressedkeys = keys; // store for other users
@@ -1865,22 +1800,24 @@ void SpectateCopy(entity this, entity spectatee)
        MUTATOR_CALLHOOK(SpectateCopy, spectatee, this);
        PS(this) = PS(spectatee);
        this.armortype = spectatee.armortype;
-       SetResourceAmountExplicit(this, RESOURCE_ARMOR, GetResourceAmount(spectatee, RESOURCE_ARMOR));
-       SetResourceAmountExplicit(this, RESOURCE_CELLS, GetResourceAmount(spectatee, RESOURCE_CELLS));
-       SetResourceAmountExplicit(this, RESOURCE_PLASMA, GetResourceAmount(spectatee, RESOURCE_PLASMA));
-       SetResourceAmountExplicit(this, RESOURCE_SHELLS, GetResourceAmount(spectatee, RESOURCE_SHELLS));
-       SetResourceAmountExplicit(this, RESOURCE_BULLETS, GetResourceAmount(spectatee, RESOURCE_BULLETS));
-       SetResourceAmountExplicit(this, RESOURCE_ROCKETS, GetResourceAmount(spectatee, RESOURCE_ROCKETS));
-       SetResourceAmountExplicit(this, RESOURCE_FUEL, GetResourceAmount(spectatee, RESOURCE_FUEL));
+       SetResourceExplicit(this, RES_ARMOR, GetResource(spectatee, RES_ARMOR));
+       SetResourceExplicit(this, RES_CELLS, GetResource(spectatee, RES_CELLS));
+       SetResourceExplicit(this, RES_PLASMA, GetResource(spectatee, RES_PLASMA));
+       SetResourceExplicit(this, RES_SHELLS, GetResource(spectatee, RES_SHELLS));
+       SetResourceExplicit(this, RES_BULLETS, GetResource(spectatee, RES_BULLETS));
+       SetResourceExplicit(this, RES_ROCKETS, GetResource(spectatee, RES_ROCKETS));
+       SetResourceExplicit(this, RES_FUEL, GetResource(spectatee, RES_FUEL));
        this.effects = spectatee.effects & EFMASK_CHEAP; // eat performance
-       SetResourceAmountExplicit(this, RESOURCE_HEALTH, GetResourceAmount(spectatee, RESOURCE_HEALTH));
+       SetResourceExplicit(this, RES_HEALTH, GetResource(spectatee, RES_HEALTH));
        CS(this).impulse = 0;
+       this.disableclientprediction = 1; // no need to run prediction on a spectator
        this.items = spectatee.items;
        STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
        STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
-       this.strength_finished = spectatee.strength_finished;
-       this.invincible_finished = spectatee.invincible_finished;
-       this.superweapons_finished = spectatee.superweapons_finished;
+       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);
        this.punchangle = spectatee.punchangle;
@@ -1972,6 +1909,11 @@ void SetSpectatee_status(entity this, int spectatee_num)
 
        if (CS(this).spectatee_status != oldspectatee_status)
        {
+               if (STAT(PRESSED_KEYS, this))
+               {
+                       CS(this).pressedkeys = 0;
+                       STAT(PRESSED_KEYS, this) = 0;
+               }
                ClientData_Touch(this);
                if (g_race || g_cts) race_InitSpectator();
        }
@@ -2102,7 +2044,7 @@ 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) || this.team_forced > 0)
+       if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
                return false;
        stuffcmd(this, "menu_showteamselect\n");
        return true;
@@ -2125,13 +2067,22 @@ void Join(entity this)
        if(IS_PLAYER(this))
        if(teamplay && this.team != -1)
        {
-               //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
        }
        else
                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
        this.team_selected = false;
 }
 
+int GetPlayerLimit()
+{
+       if(g_duel)
+               return 2; // TODO: this workaround is needed since the mutator hook from duel can't be activated before the gametype is loaded (e.g. switching modes via gametype vote screen)
+       int player_limit = autocvar_g_maxplayers;
+       MUTATOR_CALLHOOK(GetPlayerLimit, player_limit);
+       player_limit = M_ARGV(0, int);
+       return player_limit;
+}
+
 /**
  * Determines whether the player is allowed to join. This depends on cvar
  * g_maxplayers, if it isn't used this function always return true, otherwise
@@ -2150,7 +2101,7 @@ int nJoinAllowed(entity this, entity ignore)
                        return 0;
        }
 
-       if(this && this.team_forced < 0)
+       if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
                return 0; // forced spectators can never join
 
        // TODO simplify this
@@ -2164,17 +2115,19 @@ int nJoinAllowed(entity this, entity ignore)
                        ++currentlyPlaying;
        });
 
-       float free_slots = 0;
-       if (!autocvar_g_maxplayers)
+       int player_limit = GetPlayerLimit();
+
+       int free_slots = 0;
+       if (!player_limit)
                free_slots = maxclients - totalClients;
-       else if(currentlyPlaying < autocvar_g_maxplayers)
-               free_slots = min(maxclients - totalClients, autocvar_g_maxplayers - currentlyPlaying);
+       else if(player_limit > 0 && currentlyPlaying < player_limit)
+               free_slots = min(maxclients - totalClients, player_limit - currentlyPlaying);
 
-       static float join_prevent_msg_time = 0;
-       if(this && ignore && !free_slots && time > join_prevent_msg_time)
+       static float msg_time = 0;
+       if(this && !this.caplayer && ignore && !free_slots && time > msg_time)
        {
                Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT);
-               join_prevent_msg_time = time + 3;
+               msg_time = time + 0.5;
        }
 
        return free_slots;
@@ -2204,7 +2157,7 @@ void PrintWelcomeMessage(entity this)
                if (autocvar_g_campaign) {
                        if ((IS_PLAYER(this) && PHYS_INPUT_BUTTON_INFO(this)) || (!IS_PLAYER(this))) {
                                CS(this).motd_actived_time = time;
-                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOTD, campaign_message);
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_CAMPAIGN_MESSAGE, Campaign_GetMessage(), Campaign_GetLevelNum());
                        }
                } else {
                        if (PHYS_INPUT_BUTTON_INFO(this)) {
@@ -2220,7 +2173,7 @@ void PrintWelcomeMessage(entity this)
                                CS(this).motd_actived_time = time;
                        else if ((time - CS(this).motd_actived_time > 2) && IS_PLAYER(this)) { // hide it some seconds after BUTTON_INFO has been released
                                CS(this).motd_actived_time = 0;
-                               Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_MOTD);
+                               Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_CAMPAIGN_MESSAGE);
                        }
                } else {
                        if (PHYS_INPUT_BUTTON_INFO(this))
@@ -2235,16 +2188,27 @@ void PrintWelcomeMessage(entity this)
        {
                if(PHYS_INPUT_BUTTON_INFO(this)) // BUTTON_INFO hides initial MOTD
                        CS(this).motd_actived_time = -2; // wait until BUTTON_INFO gets released
-               else if(CS(this).motd_actived_time == -2 || IS_PLAYER(this) || IS_SPEC(this))
+               else if (CS(this).motd_actived_time == -2)
                {
-                       // instanctly hide MOTD
+                       // instantly hide MOTD
                        CS(this).motd_actived_time = 0;
-                       Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_MOTD);
+                       if (autocvar_g_campaign)
+                               Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_CAMPAIGN_MESSAGE);
+                       else
+                               Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_MOTD);
+               }
+               else if (IS_PLAYER(this) || IS_SPEC(this))
+               {
+                       // FIXME occasionally for some reason MOTD never goes away
+                       // delay MOTD removal a little bit in the hope it fixes this bug
+                       if (CS(this).motd_actived_time == -1) // MOTD marked to fade away as soon as client becomes player or spectator
+                               CS(this).motd_actived_time = -(5 + floor(random() * 10)); // add small delay
+                       else //if (CS(this).motd_actived_time < -2)
+                               CS(this).motd_actived_time++;
                }
        }
 }
 
-const int MIN_SPEC_TIME = 1;
 bool joinAllowed(entity this)
 {
        if (CS(this).version_mismatch) return false;
@@ -2256,8 +2220,8 @@ bool joinAllowed(entity this)
        return true;
 }
 
-.int items_added;
 .string shootfromfixedorigin;
+.bool dualwielding_prev;
 bool PlayerThink(entity this)
 {
        if (game_stopped || intermission_running) {
@@ -2360,91 +2324,59 @@ bool PlayerThink(entity this)
                stuffcmd(this, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
        }
 
+       // reset gun alignment when dual wielding status changes
+       // to ensure guns are always aligned right and left
+       bool dualwielding = W_DualWielding(this);
+       if(this.dualwielding_prev != dualwielding)
+       {
+               W_ResetGunAlign(this, CS(this).cvar_cl_gunalign);
+               this.dualwielding_prev = dualwielding;
+       }
+
        // LordHavoc: allow firing on move frames (sub-ticrate), this gives better timing on slow servers
        //if(frametime)
        {
-               this.items &= ~this.items_added;
-
                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                {
                        .entity weaponentity = weaponentities[slot];
+                       if(WEP_CVAR(vortex, charge_always))
+                               W_Vortex_Charge(this, weaponentity, frametime);
                        W_WeaponFrame(this, weaponentity);
                }
-
-               this.items_added = 0;
-               if ((this.items & ITEM_Jetpack.m_itemid) && ((this.items & ITEM_JetpackRegen.m_itemid) || GetResourceAmount(this, RESOURCE_FUEL) >= 0.01))
-            this.items_added |= IT_FUEL;
-
-               this.items |= this.items_added;
        }
 
-       player_regen(this);
-
-       // WEAPONTODO: Add a weapon request for this
-       // rot vortex charge to the charge limit
-       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+       if (frametime)
        {
-               .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 (frametime) player_anim(this);
+               // WEAPONTODO: Add a weapon request for this
+               // rot vortex charge to the charge limit
+               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);
+               }
 
-       // secret status
-       secrets_setstatus(this);
+               player_regen(this);
+               player_anim(this);
+               this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
+       }
 
-       // monsters status
        monsters_setstatus(this);
 
-       this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
-
        return true;
 }
 
 .bool would_spectate;
-void ObserverThink(entity this)
+void ObserverOrSpectatorThink(entity this)
 {
+       bool is_spec = IS_SPEC(this);
        if ( CS(this).impulse )
        {
-               MinigameImpulse(this, CS(this).impulse);
-               CS(this).impulse = 0;
-       }
-
-       if (this.flags & FL_JUMPRELEASED) {
-               if (PHYS_INPUT_BUTTON_JUMP(this) && joinAllowed(this)) {
-                       this.flags &= ~FL_JUMPRELEASED;
-                       this.flags |= FL_SPAWNING;
-               } else if(PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch || this.would_spectate) {
-                       this.flags &= ~FL_JUMPRELEASED;
-                       if(SpectateNext(this)) {
-                               TRANSMUTE(Spectator, this);
-                       }
-               } else {
-                       int preferred_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? CS(this).cvar_cl_clippedspectating : !CS(this).cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
-                       set_movetype(this, preferred_movetype);
-               }
-       } else {
-               if (!(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this))) {
-                       this.flags |= FL_JUMPRELEASED;
-                       if(this.flags & FL_SPAWNING)
-                       {
-                               this.flags &= ~FL_SPAWNING;
-                               Join(this);
-                               return;
-                       }
-               }
-       }
-}
-
-void SpectatorThink(entity this)
-{
-       if ( CS(this).impulse )
-       {
-               if(MinigameImpulse(this, CS(this).impulse))
+               int r = MinigameImpulse(this, CS(this).impulse);
+               if (!is_spec || r)
                        CS(this).impulse = 0;
 
-               if (CS(this).impulse == IMP_weapon_drop.impulse)
+               if (is_spec && CS(this).impulse == IMP_weapon_drop.impulse)
                {
                        STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
                        CS(this).impulse = 0;
@@ -2453,57 +2385,63 @@ void SpectatorThink(entity this)
        }
 
        if (this.flags & FL_JUMPRELEASED) {
-               if (PHYS_INPUT_BUTTON_JUMP(this) && joinAllowed(this)) {
+               if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
                        this.flags &= ~FL_JUMPRELEASED;
                        this.flags |= FL_SPAWNING;
-               } else if(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)) {
+               } 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)))
+                       || (!is_spec && ((PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) || this.would_spectate))) {
                        this.flags &= ~FL_JUMPRELEASED;
                        if(SpectateNext(this)) {
                                TRANSMUTE(Spectator, this);
-                       } else {
+                       } else if (is_spec) {
                                TRANSMUTE(Observer, this);
                                PutClientInServer(this);
                        }
-                       CS(this).impulse = 0;
-               } else if(CS(this).impulse == 12 || CS(this).impulse == 16  || CS(this).impulse == 19 || (CS(this).impulse >= 220 && CS(this).impulse <= 229)) {
-                       this.flags &= ~FL_JUMPRELEASED;
-                       if(SpectatePrev(this)) {
-                               TRANSMUTE(Spectator, this);
-                       } else {
+                       if (is_spec)
+                               CS(this).impulse = 0;
+               } else if (is_spec) {
+                       if(CS(this).impulse == 12 || CS(this).impulse == 16  || CS(this).impulse == 19 || (CS(this).impulse >= 220 && CS(this).impulse <= 229)) {
+                               this.flags &= ~FL_JUMPRELEASED;
+                               if(SpectatePrev(this)) {
+                                       TRANSMUTE(Spectator, this);
+                               } else {
+                                       TRANSMUTE(Observer, this);
+                                       PutClientInServer(this);
+                               }
+                               CS(this).impulse = 0;
+                       } else if(PHYS_INPUT_BUTTON_ATCK2(this)) {
+                               this.would_spectate = false;
+                               this.flags &= ~FL_JUMPRELEASED;
                                TRANSMUTE(Observer, this);
                                PutClientInServer(this);
-                       }
-                       CS(this).impulse = 0;
-               } else if (PHYS_INPUT_BUTTON_ATCK2(this)) {
-                       this.would_spectate = false;
-                       this.flags &= ~FL_JUMPRELEASED;
-                       TRANSMUTE(Observer, this);
-                       PutClientInServer(this);
-               } else {
-                       if(!SpectateUpdate(this))
-                       {
-                               if(!SpectateNext(this))
-                               {
-                                       PutObserverInServer(this);
-                                       this.would_spectate = true;
-                               }
+                       } else if(!SpectateUpdate(this) && !SpectateNext(this)) {
+                               PutObserverInServer(this);
+                               this.would_spectate = true;
                        }
                }
+               else {
+                       int preferred_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? CS(this).cvar_cl_clippedspectating : !CS(this).cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
+                       set_movetype(this, preferred_movetype);
+               }
        } else {
-               if (!(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(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;
                        if(this.flags & FL_SPAWNING)
                        {
                                this.flags &= ~FL_SPAWNING;
-                               Join(this);
+                               if(joinAllowed(this))
+                                       Join(this);
+                               else if(time < CS(this).jointime + MIN_SPEC_TIME)
+                                       CS(this).autojoin_checked = -1;
                                return;
                        }
                }
-               if(!SpectateUpdate(this))
+               if(is_spec && !SpectateUpdate(this))
                        PutObserverInServer(this);
        }
-
-       this.flags |= FL_CLIENT | FL_NOTARGET;
+       if (is_spec)
+               this.flags |= FL_CLIENT | FL_NOTARGET;
 }
 
 void PlayerUseKey(entity this)
@@ -2521,19 +2459,15 @@ void PlayerUseKey(entity this)
        }
        else if(autocvar_g_vehicles_enter)
        {
-               if(!STAT(FROZEN, this))
-               if(!IS_DEAD(this))
-               if(!game_stopped)
+               if(!game_stopped && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
                {
                        entity head, closest_target = NULL;
                        head = WarpZone_FindRadius(this.origin, autocvar_g_vehicles_enter_radius, true);
 
                        while(head) // find the closest acceptable target to enter
                        {
-                               if(IS_VEHICLE(head))
-                               if(!IS_DEAD(head))
+                               if(IS_VEHICLE(head) && !IS_DEAD(head) && head.takedamage != DAMAGE_NO)
                                if(!head.owner || ((head.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(head.owner, this)))
-                               if(head.takedamage != DAMAGE_NO)
                                {
                                        if(closest_target)
                                        {
@@ -2587,9 +2521,18 @@ void PlayerPreThink (entity this)
        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?
                }
@@ -2623,23 +2566,24 @@ void PlayerPreThink (entity this)
                this.max_armorvalue = 0;
        }
 
-       if(IS_PLAYER(this))
+       if (frametime && IS_PLAYER(this))
        {
-               if (STAT(FROZEN, this) == 2)
+               if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
                {
                        STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
-                       SetResourceAmountExplicit(this, RESOURCE_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
-                       this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 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);
+                               Unfreeze(this, false);
                }
-               else if (STAT(FROZEN, this) == 3)
+               else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
                {
                        STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
-                       SetResourceAmountExplicit(this, RESOURCE_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
+                       SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
 
-                       if (GetResourceAmount(this, RESOURCE_HEALTH) < 1)
+                       if (GetResource(this, RES_HEALTH) < 1)
                        {
                                if (this.vehicle)
                                        vehicles_exit(this.vehicle, VHEF_RELEASE);
@@ -2647,29 +2591,28 @@ void PlayerPreThink (entity this)
                                        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);
+                               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))
+       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),
+               FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
                {
-                       if(!IS_DEAD(it) && it.takedamage != DAMAGE_NO)
-                       if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
-                       {
-                               Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
-                       }
-                       else if(!it.owner)
+                       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;
@@ -2696,25 +2639,24 @@ void PlayerPreThink (entity this)
                        IntermissionThink(this);
                return;
        }
-       else if (IS_REAL_CLIENT(this) && !CS(this).autojoin_checked && time >= CS(this).jointime + MIN_SPEC_TIME)
+       else if (IS_REAL_CLIENT(this) && CS(this).autojoin_checked <= 0 && time >= CS(this).jointime + MIN_SPEC_TIME)
        {
-               CS(this).autojoin_checked = true;
+               bool early_join_requested = (CS(this).autojoin_checked < 0);
+               CS(this).autojoin_checked = 1;
                // don't do this in ClientConnect
                // many things can go wrong if a client is spawned as player on connection
-               if (MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
-                       || (!(autocvar_sv_spectate || autocvar_g_campaign || this.team_forced < 0)
+               if (early_join_requested || MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
+                       || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
                                && (!teamplay || autocvar_g_balance_teams)))
                {
                        campaign_bots_may_start = true;
-                       Join(this);
+                       if(joinAllowed(this))
+                               Join(this);
                        return;
                }
        }
-       else if (IS_OBSERVER(this)) {
-               ObserverThink(this);
-       }
-       else if (IS_SPEC(this)) {
-               SpectatorThink(this);
+       else if (IS_OBSERVER(this) || IS_SPEC(this)) {
+               ObserverOrSpectatorThink(this);
        }
 
        // WEAPONTODO: Add weapon request for this
@@ -2728,7 +2670,7 @@ void PlayerPreThink (entity this)
                                wep_zoomed += thiswep.wr_zoom(thiswep, this);
                }
                SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
-    }
+       }
 
        if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
        {
@@ -2760,21 +2702,30 @@ void PlayerPreThink (entity this)
 
 void DrownPlayer(entity this)
 {
-       if(IS_DEAD(this) || game_stopped || time < game_starttime)
+       if(IS_DEAD(this) || game_stopped || time < game_starttime || this.vehicle
+               || STAT(FROZEN, this) || this.watertype != CONTENT_WATER)
+       {
+               STAT(AIR_FINISHED, this) = 0;
                return;
+       }
 
-       if (this.waterlevel != WATERLEVEL_SUBMERGED || this.vehicle)
+       if (this.waterlevel != WATERLEVEL_SUBMERGED)
        {
-               if(this.air_finished < time)
+               if(STAT(AIR_FINISHED, this) && STAT(AIR_FINISHED, this) < time)
                        PlayerSound(this, playersound_gasp, CH_PLAYER, VOL_BASE, VOICETYPE_PLAYERSOUND);
-               this.air_finished = time + autocvar_g_balance_contents_drowndelay;
+               STAT(AIR_FINISHED, this) = 0;
        }
-       else if (this.air_finished < time)
-       {       // drown!
-               if (this.pain_finished < time)
-               {
-                       Damage (this, NULL, NULL, autocvar_g_balance_contents_playerdamage_drowning * autocvar_g_balance_contents_damagerate, DEATH_DROWN.m_id, DMG_NOWEP, this.origin, '0 0 0');
-                       this.pain_finished = time + 0.5;
+       else
+       {
+               if (!STAT(AIR_FINISHED, this))
+                       STAT(AIR_FINISHED, this) = time + autocvar_g_balance_contents_drowndelay;
+               if (STAT(AIR_FINISHED, this) < time)
+               {       // drown!
+                       if (this.pain_finished < time)
+                       {
+                               Damage (this, NULL, NULL, autocvar_g_balance_contents_playerdamage_drowning * autocvar_g_balance_contents_damagerate, DEATH_DROWN.m_id, DMG_NOWEP, this.origin, '0 0 0');
+                               this.pain_finished = time + 0.5;
+                       }
                }
        }
 }
@@ -2783,7 +2734,7 @@ void DrownPlayer(entity this)
 
 void Player_Physics(entity this)
 {
-       set_movetype(this, this.move_movetype);
+       this.movetype = (this.move_qcphysics) ? MOVETYPE_QCPLAYER : this.move_movetype;
 
        if(!this.move_qcphysics)
                return;
@@ -2859,6 +2810,9 @@ void PlayerPostThink (entity this)
                this.solid = SOLID_NOT;
                this.takedamage = DAMAGE_NO;
                set_movetype(this, MOVETYPE_NONE);
+               CS(this).teamkill_complain = 0;
+               CS(this).teamkill_soundtime = 0;
+               CS(this).teamkill_soundsource = NULL;
        }
 
        if (IS_PLAYER(this)) {
@@ -2879,15 +2833,351 @@ void PlayerPostThink (entity this)
                }
                GetPressedKeys(this);
        }
+       else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
+       {
+               CS(this).pressedkeys = 0;
+               STAT(PRESSED_KEYS, this) = 0;
+       }
 
        if (this.waypointsprite_attachedforcarrier) {
-           vector v = healtharmor_maxdamage(GetResourceAmount(this, RESOURCE_HEALTH), GetResourceAmount(this, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id);
-               WaypointSprite_UpdateHealth(this.waypointsprite_attachedforcarrier, '1 0 0' * v);
-    }
+               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);
 }
 
+/**
+ * message "": do not say, just test flood control
+ * return value:
+ *   1 = accept
+ *   0 = reject
+ *  -1 = fake accept
+ */
+int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
+{
+       if (!teamsay && !privatesay && substring(msgin, 0, 1) == " ")
+               msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
+
+       if (source)
+               msgin = formatmessage(source, msgin);
+
+       string colorstr;
+       if (!(IS_PLAYER(source) || source.caplayer))
+               colorstr = "^0"; // black for spectators
+       else if(teamplay)
+               colorstr = Team_ColorCode(source.team);
+       else
+       {
+               colorstr = "";
+               teamsay = false;
+       }
+
+       if (!source) {
+               colorstr = "";
+               teamsay = false;
+       }
+
+       if(msgin != "")
+               msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
+
+       /*
+        * using bprint solves this... me stupid
+       // how can we prevent the message from appearing in a listen server?
+       // for now, just give "say" back and only handle say_team
+       if(!teamsay)
+       {
+               clientcommand(source, strcat("say ", msgin));
+               return;
+       }
+       */
+
+       string namestr = "";
+       if (source)
+               namestr = playername(source, autocvar_g_chat_teamcolors);
+
+       string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
+
+       string msgstr = "", cmsgstr = "";
+       string privatemsgprefix = string_null;
+       int privatemsgprefixlen = 0;
+       if (msgin != "")
+       {
+               bool found_me = false;
+               if(strstrofs(msgin, "/me", 0) >= 0)
+               {
+                       string newmsgin = "";
+                       string newnamestr = ((teamsay) ? strcat(colorstr, "(", colorprefix, namestr, colorstr, ")", "^7") : strcat(colorprefix, namestr, "^7"));
+                       FOREACH_WORD(msgin, true,
+                       {
+                               if(strdecolorize(it) == "/me")
+                               {
+                                       found_me = true;
+                                       newmsgin = cons(newmsgin, newnamestr);
+                               }
+                               else
+                                       newmsgin = cons(newmsgin, it);
+                       });
+                       msgin = newmsgin;
+               }
+
+               if(privatesay)
+               {
+                       msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7");
+                       privatemsgprefixlen = strlen(msgstr);
+                       msgstr = strcat(msgstr, msgin);
+                       cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin);
+                       privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay, autocvar_g_chat_teamcolors), ": ^7");
+               }
+               else if(teamsay)
+               {
+                       if(found_me)
+                       {
+                               //msgin = strreplace("/me", "", msgin);
+                               //msgin = substring(msgin, 3, strlen(msgin));
+                               //msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin);
+                               msgstr = strcat("\{1}\{13}^4* ", "^7", msgin);
+                       }
+                       else
+                               msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
+                       cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
+               }
+               else
+               {
+                       if(found_me)
+                       {
+                               //msgin = strreplace("/me", "", msgin);
+                               //msgin = substring(msgin, 3, strlen(msgin));
+                               //msgin = strreplace("/me", strcat(colorprefix, namestr), msgin);
+                               msgstr = strcat("\{1}^4* ^7", msgin);
+                       }
+                       else {
+                               msgstr = "\{1}";
+                               msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
+                               msgstr = strcat(msgstr, msgin);
+                       }
+                       cmsgstr = "";
+               }
+               msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
+       }
+
+       string fullmsgstr = msgstr;
+       string fullcmsgstr = cmsgstr;
+
+       // FLOOD CONTROL
+       int flood = 0;
+       var .float flood_field = floodcontrol_chat;
+       if(floodcontrol && source)
+       {
+               float flood_spl;
+               float flood_burst;
+               float flood_lmax;
+               float lines;
+               if(privatesay)
+               {
+                       flood_spl = autocvar_g_chat_flood_spl_tell;
+                       flood_burst = autocvar_g_chat_flood_burst_tell;
+                       flood_lmax = autocvar_g_chat_flood_lmax_tell;
+                       flood_field = floodcontrol_chattell;
+               }
+               else if(teamsay)
+               {
+                       flood_spl = autocvar_g_chat_flood_spl_team;
+                       flood_burst = autocvar_g_chat_flood_burst_team;
+                       flood_lmax = autocvar_g_chat_flood_lmax_team;
+                       flood_field = floodcontrol_chatteam;
+               }
+               else
+               {
+                       flood_spl = autocvar_g_chat_flood_spl;
+                       flood_burst = autocvar_g_chat_flood_burst;
+                       flood_lmax = autocvar_g_chat_flood_lmax;
+                       flood_field = floodcontrol_chat;
+               }
+               flood_burst = max(0, flood_burst - 1);
+               // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
+
+               // do flood control for the default line size
+               if(msgstr != "")
+               {
+                       getWrappedLine_remaining = msgstr;
+                       msgstr = "";
+                       lines = 0;
+                       while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
+                       {
+                               msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
+                               ++lines;
+                       }
+                       msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
+
+                       if(getWrappedLine_remaining != "")
+                       {
+                               msgstr = strcat(msgstr, "\n");
+                               flood = 2;
+                       }
+
+                       if (time >= source.(flood_field))
+                       {
+                               source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
+                       }
+                       else
+                       {
+                               flood = 1;
+                               msgstr = fullmsgstr;
+                       }
+               }
+               else
+               {
+                       if (time >= source.(flood_field))
+                               source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
+                       else
+                               flood = 1;
+               }
+
+               if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
+                       source.(flood_field) = flood = 0;
+       }
+
+       string sourcemsgstr, sourcecmsgstr;
+       if(flood == 2) // cannot happen for empty msgstr
+       {
+               if(autocvar_g_chat_flood_notify_flooder)
+               {
+                       sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
+                       sourcecmsgstr = "";
+               }
+               else
+               {
+                       sourcemsgstr = fullmsgstr;
+                       sourcecmsgstr = fullcmsgstr;
+               }
+               cmsgstr = "";
+       }
+       else
+       {
+               sourcemsgstr = msgstr;
+               sourcecmsgstr = cmsgstr;
+       }
+
+       if (!privatesay && source && !(IS_PLAYER(source) || source.caplayer))
+       {
+               if (!game_stopped)
+               if (teamsay || (autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !warmup_stage))
+                       teamsay = -1; // spectators
+       }
+
+       if(flood)
+               LOG_INFO("NOTE: ", playername(source, true), "^7 is flooding.");
+
+       // build sourcemsgstr by cutting off a prefix and replacing it by the other one
+       if(privatesay)
+               sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
+
+       int ret;
+       if(source && CS(source).muted)
+       {
+               // always fake the message
+               ret = -1;
+       }
+       else if(flood == 1)
+       {
+               if (autocvar_g_chat_flood_notify_flooder)
+               {
+                       sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
+                       ret = 0;
+               }
+               else
+                       ret = -1;
+       }
+       else
+       {
+               ret = 1;
+       }
+
+       if (privatesay && source && !(IS_PLAYER(source) || source.caplayer))
+       {
+               if (!game_stopped)
+               if ((privatesay && (IS_PLAYER(privatesay) || privatesay.caplayer)) && ((autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !warmup_stage)))
+                       ret = -1; // just hide the message completely
+       }
+
+       MUTATOR_CALLHOOK(ChatMessage, source, ret);
+       ret = M_ARGV(1, int);
+
+       string event_log_msg = "";
+
+       if(sourcemsgstr != "" && ret != 0)
+       {
+               if(ret < 0) // faked message, because the player is muted
+               {
+                       sprint(source, sourcemsgstr);
+                       if(sourcecmsgstr != "" && !privatesay)
+                               centerprint(source, sourcecmsgstr);
+               }
+               else if(privatesay) // private message, between 2 people only
+               {
+                       sprint(source, sourcemsgstr);
+                       if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
+                       if(!MUTATOR_CALLHOOK(ChatMessageTo, privatesay, source))
+                       {
+                               sprint(privatesay, msgstr);
+                               if(cmsgstr != "")
+                                       centerprint(privatesay, cmsgstr);
+                       }
+               }
+               else if ( teamsay && CS(source).active_minigame )
+               {
+                       sprint(source, sourcemsgstr);
+                       dedicated_print(msgstr); // send to server console too
+                       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && CS(it).active_minigame == CS(source).active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+                               sprint(it, msgstr);
+                       });
+                       event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
+
+               }
+               else if(teamsay > 0) // team message, only sent to team mates
+               {
+                       sprint(source, sourcemsgstr);
+                       dedicated_print(msgstr); // send to server console too
+                       if(sourcecmsgstr != "")
+                               centerprint(source, sourcecmsgstr);
+                       FOREACH_CLIENT((IS_PLAYER(it) || it.caplayer) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+                               sprint(it, msgstr);
+                               if(cmsgstr != "")
+                                       centerprint(it, cmsgstr);
+                       });
+                       event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
+               }
+               else if(teamsay < 0) // spectator message, only sent to spectators
+               {
+                       sprint(source, sourcemsgstr);
+                       dedicated_print(msgstr); // send to server console too
+                       FOREACH_CLIENT(!(IS_PLAYER(it) || it.caplayer) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+                               sprint(it, msgstr);
+                       });
+                       event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
+               }
+               else
+               {
+                       if (source) {
+                               sprint(source, sourcemsgstr);
+                               dedicated_print(msgstr); // send to server console too
+                               MX_Say(strcat(playername(source, true), "^7: ", msgin));
+                       }
+                       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+                               sprint(it, msgstr);
+                       });
+                       event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
+               }
+       }
+
+       if (autocvar_sv_eventlog && (event_log_msg != "")) {
+               GameLogEcho(event_log_msg);
+       }
+
+       return ret;
+}
+
 // hack to copy the button fields from the client entity to the Client State
 void PM_UpdateButtons(entity this, entity store)
 {
@@ -2895,7 +3185,7 @@ void PM_UpdateButtons(entity this, entity store)
                store.impulse = this.impulse;
        this.impulse = 0;
 
-       bool typing = this.buttonchat;
+       bool typing = this.buttonchat || this.button12;
 
        store.button0 = (typing) ? 0 : this.button0;
        //button1?!
@@ -2928,7 +3218,7 @@ void PM_UpdateButtons(entity this, entity store)
        store.ping_movementloss = this.ping_movementloss;
 
        store.v_angle = this.v_angle;
-       store.movement = (typing) ? '0 0 0' : this.movement;
+       store.movement = this.movement;
 }
 
 NET_HANDLE(fpsreport, bool)