Merged master
authorLyberta <lyberta@lyberta.net>
Sun, 30 Jul 2017 07:57:45 +0000 (10:57 +0300)
committerLyberta <lyberta@lyberta.net>
Sun, 30 Jul 2017 07:57:45 +0000 (10:57 +0300)
1  2 
qcsrc/server/autocvars.qh
qcsrc/server/client.qc
qcsrc/server/command/cmd.qc
qcsrc/server/command/sv_cmd.qc
qcsrc/server/mutators/events.qh
qcsrc/server/player.qc
qcsrc/server/player.qh
qcsrc/server/teamplay.qc

index 66cb5172a187e2b865c53e3e16bd98237973a785,f585a9a2c33db3eafb7b72168b2b3d8a50de8afd..61b9df462e98a7c60d59063b57c24ba1f2cff763
@@@ -13,7 -13,7 +13,7 @@@ int autocvar_captureleadlimit_override
  float autocvar_ekg;
  #define autocvar_fraglimit cvar("fraglimit")
  #define autocvar_fraglimit_override cvar("fraglimit_override")
- bool autocvar_g_allow_oldvortexbeam;
//bool autocvar_g_allow_oldvortexbeam;
  int autocvar_g_antilag;
  float autocvar_g_antilag_nudge;
  float autocvar_g_balance_armor_blockpercent;
@@@ -88,7 -88,7 +88,7 @@@ float autocvar_g_balance_superweapons_t
  float autocvar_g_balance_selfdamagepercent;
  bool autocvar_g_balance_teams;
  bool autocvar_g_balance_teams_prevent_imbalance;
 -float autocvar_g_balance_teams_scorefactor;
 +//float autocvar_g_balance_teams_scorefactor;
  float autocvar_g_ballistics_density_corpse;
  float autocvar_g_ballistics_density_player;
  float autocvar_g_ballistics_mindistance;
@@@ -258,7 -258,7 +258,7 @@@ float autocvar_gameversion_min
  float autocvar_gameversion_max;
  string autocvar_hostname;
  bool autocvar_lastlevel;
- int autocvar_leadlimit;
//int autocvar_leadlimit;
  int autocvar_leadlimit_and_fraglimit;
  int autocvar_leadlimit_override;
  int autocvar_loddebug;
diff --combined qcsrc/server/client.qc
index 3f9273b3ca52bfb11eda2f66eb4591b0e64fccaa,804868fa0031ac041b3e24cf65ae737fb0bed114..513a79678075668f3753857df65fd0716ee1a961
@@@ -110,9 -110,9 +110,9 @@@ bool ClientData_Send(entity this, entit
        if (IS_SPEC(e)) e = e.enemy;
  
        sf = 0;
-       if (e.race_completed)       sf |= 1; // forced scoreboard
-       if (to.spectatee_status)    sf |= 2; // spectator ent number follows
-       if (e.zoomstate)            sf |= 4; // zoomed
+       if (CS(e).race_completed)       sf |= 1; // forced scoreboard
+       if (CS(to).spectatee_status)    sf |= 2; // spectator ent number follows
+       if (CS(e).zoomstate)            sf |= 4; // zoomed
        if (autocvar_sv_showspectators) sf |= 16; // show spectators
  
        WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
  
        if (sf & 2)
        {
-               WriteByte(MSG_ENTITY, to.spectatee_status);
+               WriteByte(MSG_ENTITY, CS(to).spectatee_status);
        }
  
        if(sf & 16)
  
  void ClientData_Attach(entity this)
  {
-       Net_LinkEntity(this.clientdata = new_pure(clientdata), false, 0, ClientData_Send);
-       this.clientdata.drawonlytoclient = this;
-       this.clientdata.owner = this;
+       Net_LinkEntity(CS(this).clientdata = new_pure(clientdata), false, 0, ClientData_Send);
+       CS(this).clientdata.drawonlytoclient = this;
+       CS(this).clientdata.owner = this;
  }
  
  void ClientData_Detach(entity this)
  {
-       delete(this.clientdata);
-       this.clientdata = NULL;
+       delete(CS(this).clientdata);
+       CS(this).clientdata = NULL;
  }
  
  void ClientData_Touch(entity e)
  {
-       e.clientdata.SendFlags = 1;
+       CS(e).clientdata.SendFlags = 1;
  
        // make it spectatable
-       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != e && IS_SPEC(it) && it.enemy == e, LAMBDA(it.clientdata.SendFlags = 1));
+       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != e && IS_SPEC(it) && it.enemy == e, LAMBDA(CS(it).clientdata.SendFlags = 1));
  }
  
- .string netname_previous;
  void SetSpectatee(entity this, entity spectatee);
  void SetSpectatee_status(entity this, int spectatee_num);
  
@@@ -227,12 -225,10 +225,10 @@@ void PutObserverInServer(entity this
      {
          entity spot = SelectSpawnPoint(this, true);
          if (!spot) LOG_FATAL("No spawnpoints for observers?!?");
-         this.angles = spot.angles;
-         this.angles_z = 0;
+         this.angles = vec2(spot.angles);
          this.fixangle = true;
          // offset it so that the spectator spawns higher off the ground, looks better this way
          setorigin(this, spot.origin + STAT(PL_VIEW_OFS, this));
-         this.prevorigin = this.origin;
          if (IS_REAL_CLIENT(this))
          {
              msg_entity = this;
        if (mutator_returnvalue) {
            // mutator prevents resetting teams+score
        } else {
 +              int oldteam = this.team;
                this.team = -1;  // move this as it is needed to log the player spectating in eventlog
 +              MUTATOR_CALLHOOK(Player_ChangedTeam, this, oldteam, this.team);
          this.frags = FRAGS_SPECTATOR;
          PlayerScore_Clear(this);  // clear scores when needed
      }
  
-       if (this.killcount != FRAGS_SPECTATOR)
+       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(this.just_joined == false) {
+               if(!CS(this).just_joined)
                        LogTeamchange(this.playerid, -1, 4);
-               else
-                       this.just_joined = false;
+               else
+                       CS(this).just_joined = false;
        }
  
        accuracy_resend(this);
  
-       this.spectatortime = time;
+       CS(this).spectatortime = time;
        if(this.bot_attack)
                IL_REMOVE(g_bot_targets, this);
        this.bot_attack = false;
  
        this.items = 0;
        this.weapons = '0 0 0';
-       this.dual_weapons = '0 0 0';
        this.drawonlytoclient = this;
  
        this.weaponmodel = "";
                this.weaponentities[slot] = NULL;
        }
        this.exteriorweaponentity = NULL;
-       this.killcount = FRAGS_SPECTATOR;
+       CS(this).killcount = FRAGS_SPECTATOR;
        this.velocity = '0 0 0';
        this.avelocity = '0 0 0';
        this.punchangle = '0 0 0';
@@@ -404,7 -397,7 +399,7 @@@ void FixPlayermodel(entity player
                int n = tokenize_console(defaultmodel);
                if(n > 0)
                {
-                       defaultmodel = argv(floor(n * player.model_randomizer));
+                       defaultmodel = argv(floor(n * CS(player).model_randomizer));
                        // However, do NOT randomize if the player-selected model is in the list.
                        for (int i = 0; i < n; ++i)
                                if ((argv(i) == player.playermodel && defaultskin == stof(player.playerskin)) || argv(i) == strcat(player.playermodel, ":", player.playerskin))
                                setcolor(player, stof(autocvar_sv_defaultplayercolors));
  }
  
- /** Called when a client spawns in the server */
- void PutClientInServer(entity this)
+ void PutPlayerInServer(entity this)
  {
-       if (IS_BOT_CLIENT(this)) {
-               TRANSMUTE(Player, this);
-       } else if (IS_REAL_CLIENT(this)) {
-               msg_entity = this;
-               WriteByte(MSG_ONE, SVC_SETVIEW);
-               WriteEntity(MSG_ONE, this);
+       if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
+       PlayerState_attach(this);
+       accuracy_resend(this);
+       if (this.team < 0)
+               JoinBestTeam(this, false, true);
+       entity spot = SelectSpawnPoint(this, false);
+       if (!spot) {
+               Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_NOSPAWNS);
+               return; // spawn failed
        }
-       if (game_stopped)
-               TRANSMUTE(Observer, this);
  
-       SetSpectatee(this, NULL);
+       TRANSMUTE(Player, this);
  
-       // reset player keys
-       this.itemkeys = 0;
+       CS(this).wasplayer = true;
+       this.iscreature = true;
+       this.teleportable = TELEPORT_NORMAL;
+       if(!this.damagedbycontents)
+               IL_PUSH(g_damagedbycontents, this);
+       this.damagedbycontents = true;
+       set_movetype(this, MOVETYPE_WALK);
+       this.solid = SOLID_SLIDEBOX;
+       this.dphitcontentsmask = DPCONTENTS_BODY | DPCONTENTS_SOLID;
+       if (autocvar_g_playerclip_collisions)
+               this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP;
+       if (IS_BOT_CLIENT(this) && autocvar_g_botclip_collisions)
+               this.dphitcontentsmask |= DPCONTENTS_BOTCLIP;
+       this.frags = FRAGS_PLAYER;
+       if (INDEPENDENT_PLAYERS) MAKE_INDEPENDENT_PLAYER(this);
+       this.flags = FL_CLIENT | FL_PICKUPITEMS;
+       if (autocvar__notarget)
+               this.flags |= FL_NOTARGET;
+       this.takedamage = DAMAGE_AIM;
+       this.effects = EF_TELEPORT_BIT | EF_RESTARTANIM_BIT;
+       this.dmg = 2; // WTF
+       if (warmup_stage) {
+               this.ammo_shells = warmup_start_ammo_shells;
+               this.ammo_nails = warmup_start_ammo_nails;
+               this.ammo_rockets = warmup_start_ammo_rockets;
+               this.ammo_cells = warmup_start_ammo_cells;
+               this.ammo_plasma = warmup_start_ammo_plasma;
+               this.ammo_fuel = warmup_start_ammo_fuel;
+               this.health = warmup_start_health;
+               this.armorvalue = warmup_start_armorvalue;
+               this.weapons = WARMUP_START_WEAPONS;
+       } else {
+               this.ammo_shells = start_ammo_shells;
+               this.ammo_nails = start_ammo_nails;
+               this.ammo_rockets = start_ammo_rockets;
+               this.ammo_cells = start_ammo_cells;
+               this.ammo_plasma = start_ammo_plasma;
+               this.ammo_fuel = start_ammo_fuel;
+               this.health = start_health;
+               this.armorvalue = start_armorvalue;
+               this.weapons = start_weapons;
+       }
+       SetSpectatee_status(this, 0);
+       PS(this).dual_weapons = '0 0 0';
+       this.superweapons_finished = (this.weapons & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
+       this.items = start_items;
+       this.spawnshieldtime = time + autocvar_g_spawnshieldtime;
+       this.pauserotarmor_finished = time + autocvar_g_balance_pause_armor_rot_spawn;
+       this.pauserothealth_finished = time + autocvar_g_balance_pause_health_rot_spawn;
+       this.pauserotfuel_finished = time + autocvar_g_balance_pause_fuel_rot_spawn;
+       this.pauseregen_finished = time + autocvar_g_balance_pause_health_regen_spawn;
+       // extend the pause of rotting if client was reset at the beginning of the countdown
+       if (!autocvar_sv_ready_restart_after_countdown && time < game_starttime) { // TODO why is this cvar NOTted?
+               float f = game_starttime - time;
+               this.spawnshieldtime += f;
+               this.pauserotarmor_finished += f;
+               this.pauserothealth_finished += f;
+               this.pauseregen_finished += f;
+       }
+       this.damageforcescale = 2;
+       this.death_time = 0;
+       this.respawn_flags = 0;
+       this.respawn_time = 0;
+       this.stat_respawn_time = 0;
+       this.scale = 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
+       this.nextthink = 0;
+       this.dmg_team = 0;
+       PS(this).ballistics_density = autocvar_g_ballistics_density_player;
  
-       MUTATOR_CALLHOOK(PutClientInServer, this);
+       this.deadflag = DEAD_NO;
  
-       if (IS_OBSERVER(this)) {
-               PutObserverInServer(this);
-       } else if (IS_PLAYER(this)) {
-               if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
+       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;
+       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';
  
-               PlayerState_attach(this);
-               accuracy_resend(this);
+       this.strength_finished = 0;
+       this.invincible_finished = 0;
+       this.fire_endtime = -1;
+       this.revive_progress = 0;
+       this.revival_time = 0;
+       this.air_finished = time + 12;
  
-               if (this.team < 0)
-                       JoinBestTeam(this, false, true);
+       entity spawnevent = new_pure(spawnevent);
+       spawnevent.owner = this;
+       Net_LinkEntity(spawnevent, false, 0.5, SpawnEvent_Send);
  
-               entity spot = SelectSpawnPoint(this, false);
-               if (!spot) {
-                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_NOSPAWNS);
-                       return; // spawn failed
-               }
+       // Cut off any still running player sounds.
+       stopsound(this, CH_PLAYER_SINGLE);
  
-               TRANSMUTE(Player, this);
+       this.model = "";
+       FixPlayermodel(this);
+       this.drawonlytoclient = NULL;
  
-               this.wasplayer = true;
-               this.iscreature = true;
-               this.teleportable = TELEPORT_NORMAL;
-               if(!this.damagedbycontents)
-                       IL_PUSH(g_damagedbycontents, this);
-               this.damagedbycontents = true;
-               set_movetype(this, MOVETYPE_WALK);
-               this.solid = SOLID_SLIDEBOX;
-               this.dphitcontentsmask = DPCONTENTS_BODY | DPCONTENTS_SOLID;
-               if (autocvar_g_playerclip_collisions)
-                       this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP;
-               if (IS_BOT_CLIENT(this) && autocvar_g_botclip_collisions)
-                       this.dphitcontentsmask |= DPCONTENTS_BOTCLIP;
-               this.frags = FRAGS_PLAYER;
-               if (INDEPENDENT_PLAYERS) MAKE_INDEPENDENT_PLAYER(this);
-               this.flags = FL_CLIENT | FL_PICKUPITEMS;
-               if (autocvar__notarget)
-                       this.flags |= FL_NOTARGET;
-               this.takedamage = DAMAGE_AIM;
-               this.effects = EF_TELEPORT_BIT | EF_RESTARTANIM_BIT;
-               this.dmg = 2; // WTF
-               if (warmup_stage) {
-                       this.ammo_shells = warmup_start_ammo_shells;
-                       this.ammo_nails = warmup_start_ammo_nails;
-                       this.ammo_rockets = warmup_start_ammo_rockets;
-                       this.ammo_cells = warmup_start_ammo_cells;
-                       this.ammo_plasma = warmup_start_ammo_plasma;
-                       this.ammo_fuel = warmup_start_ammo_fuel;
-                       this.health = warmup_start_health;
-                       this.armorvalue = warmup_start_armorvalue;
-                       this.weapons = WARMUP_START_WEAPONS;
-               } else {
-                       this.ammo_shells = start_ammo_shells;
-                       this.ammo_nails = start_ammo_nails;
-                       this.ammo_rockets = start_ammo_rockets;
-                       this.ammo_cells = start_ammo_cells;
-                       this.ammo_plasma = start_ammo_plasma;
-                       this.ammo_fuel = start_ammo_fuel;
-                       this.health = start_health;
-                       this.armorvalue = start_armorvalue;
-                       this.weapons = start_weapons;
-               }
-               SetSpectatee_status(this, 0);
-               this.dual_weapons = '0 0 0';
-               this.superweapons_finished = (this.weapons & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
-               this.items = start_items;
-               this.spawnshieldtime = time + autocvar_g_spawnshieldtime;
-               this.pauserotarmor_finished = time + autocvar_g_balance_pause_armor_rot_spawn;
-               this.pauserothealth_finished = time + autocvar_g_balance_pause_health_rot_spawn;
-               this.pauserotfuel_finished = time + autocvar_g_balance_pause_fuel_rot_spawn;
-               this.pauseregen_finished = time + autocvar_g_balance_pause_health_regen_spawn;
-               // extend the pause of rotting if client was reset at the beginning of the countdown
-               if (!autocvar_sv_ready_restart_after_countdown && time < game_starttime) { // TODO why is this cvar NOTted?
-                       float f = game_starttime - time;
-                       this.spawnshieldtime += f;
-                       this.pauserotarmor_finished += f;
-                       this.pauserothealth_finished += f;
-                       this.pauseregen_finished += f;
-               }
-               this.damageforcescale = 2;
-               this.death_time = 0;
-               this.respawn_flags = 0;
-               this.respawn_time = 0;
-               this.stat_respawn_time = 0;
-               this.scale = 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
-               this.nextthink = 0;
-               this.dmg_team = 0;
-               this.ballistics_density = autocvar_g_ballistics_density_player;
-               this.deadflag = DEAD_NO;
-               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;
-               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;
-               this.fire_endtime = -1;
-               this.revive_progress = 0;
-               this.revival_time = 0;
-               this.air_finished = time + 12;
-               entity spawnevent = new_pure(spawnevent);
-               spawnevent.owner = this;
-               Net_LinkEntity(spawnevent, false, 0.5, SpawnEvent_Send);
-               // Cut off any still running player sounds.
-               stopsound(this, CH_PLAYER_SINGLE);
-               this.model = "";
-               FixPlayermodel(this);
-               this.drawonlytoclient = NULL;
-               this.viewloc = NULL;
-               this.crouch = false;
-               this.view_ofs = STAT(PL_VIEW_OFS, this);
-               setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
-               this.spawnorigin = spot.origin;
-               setorigin(this, spot.origin + '0 0 1' * (1 - this.mins.z - 24));
-               // don't reset back to last position, even if new position is stuck in solid
-               this.oldorigin = this.origin;
-               this.prevorigin = this.origin;
-               this.lastteleporttime = time; // prevent insane speeds due to changing origin
-               if(this.conveyor)
-                       IL_REMOVE(g_conveyed, this);
-               this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player
-               this.hud = HUD_NORMAL;
-               this.event_damage = PlayerDamage;
-               if(!this.bot_attack)
-                       IL_PUSH(g_bot_targets, this);
-               this.bot_attack = true;
-               if(!this.monster_attack)
-                       IL_PUSH(g_monster_targets, this);
-               this.monster_attack = true;
-               navigation_dynamicgoal_init(this, false);
-               PHYS_INPUT_BUTTON_ATCK(this) = PHYS_INPUT_BUTTON_JUMP(this) = PHYS_INPUT_BUTTON_ATCK2(this) = false;
-               if (this.killcount == FRAGS_SPECTATOR) {
-                       PlayerScore_Clear(this);
-                       this.killcount = 0;
-               }
+       this.viewloc = NULL;
  
-               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;
-               this.exteriorweaponentity.alpha = default_weapon_alpha;
+       this.crouch = false;
+       this.view_ofs = STAT(PL_VIEW_OFS, this);
+       setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
+       this.spawnorigin = spot.origin;
+       setorigin(this, spot.origin + '0 0 1' * (1 - this.mins.z - 24));
+       // don't reset back to last position, even if new position is stuck in solid
+       this.oldorigin = this.origin;
+       this.lastteleporttime = time; // prevent insane speeds due to changing origin
+       if(this.conveyor)
+               IL_REMOVE(g_conveyed, this);
+       this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player
+       this.hud = HUD_NORMAL;
+       this.event_damage = PlayerDamage;
+       if(!this.bot_attack)
+               IL_PUSH(g_bot_targets, this);
+       this.bot_attack = true;
+       if(!this.monster_attack)
+               IL_PUSH(g_monster_targets, this);
+       this.monster_attack = true;
+       navigation_dynamicgoal_init(this, false);
+       PHYS_INPUT_BUTTON_ATCK(this) = PHYS_INPUT_BUTTON_JUMP(this) = PHYS_INPUT_BUTTON_ATCK2(this) = false;
+       if (CS(this).killcount == FRAGS_SPECTATOR) {
+               PlayerScore_Clear(this);
+               CS(this).killcount = 0;
+       }
+       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;
+       this.exteriorweaponentity.alpha = default_weapon_alpha;
  
-               this.speedrunning = false;
+       this.speedrunning = false;
  
-               target_voicescript_clear(this);
+       target_voicescript_clear(this);
  
-               // reset fields the weapons may use
-               FOREACH(Weapons, true, LAMBDA(
-                       it.wr_resetplayer(it, this);
+       // reset fields the weapons may use
+       FOREACH(Weapons, true, LAMBDA(
+               it.wr_resetplayer(it, this);
                        // reload all reloadable weapons
-                       if (it.spawnflags & WEP_FLAG_RELOADABLE) {
-                               for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
-                               {
-                                       .entity weaponentity = weaponentities[slot];
-                                       this.(weaponentity).weapon_load[it.m_id] = it.reloading_ammo;
-                               }
+               if (it.spawnflags & WEP_FLAG_RELOADABLE) {
+                       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+                       {
+                               .entity weaponentity = weaponentities[slot];
+                               this.(weaponentity).weapon_load[it.m_id] = it.reloading_ammo;
                        }
-               ));
-               {
-                       string s = spot.target;
-                       spot.target = string_null;
-                       SUB_UseTargets(spot, this, NULL);
-                       spot.target = s;
                }
+       ));
  
-               Unfreeze(this);
+       {
+               string s = spot.target;
+               spot.target = string_null;
+               SUB_UseTargets(spot, this, NULL);
+               spot.target = s;
+       }
  
-               MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
+       Unfreeze(this);
  
-               if (autocvar_spawn_debug)
-               {
-                       sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
-                       delete(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
-               }
+       MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
  
-               for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
-               {
-                       .entity weaponentity = weaponentities[slot];
-                       if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
-                               this.(weaponentity).m_switchweapon = w_getbestweapon(this, weaponentity);
-                       else
-                               this.(weaponentity).m_switchweapon = WEP_Null;
-                       this.(weaponentity).m_weapon = WEP_Null;
-                       this.(weaponentity).weaponname = "";
-                       this.(weaponentity).m_switchingweapon = WEP_Null;
-                       this.(weaponentity).cnt = -1;
-               }
+       if (autocvar_spawn_debug)
+       {
+               sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
+               delete(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
+       }
+       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+       {
+               .entity weaponentity = weaponentities[slot];
+               if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
+                       this.(weaponentity).m_switchweapon = w_getbestweapon(this, weaponentity);
+               else
+                       this.(weaponentity).m_switchweapon = WEP_Null;
+               this.(weaponentity).m_weapon = WEP_Null;
+               this.(weaponentity).weaponname = "";
+               this.(weaponentity).m_switchingweapon = WEP_Null;
+               this.(weaponentity).cnt = -1;
+       }
+       MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
+       if (!warmup_stage && !this.alivetime)
+               this.alivetime = time;
+       antilag_clear(this, CS(this));
+ }
+ /** Called when a client spawns in the server */
+ void PutClientInServer(entity this)
+ {
+       if (IS_BOT_CLIENT(this)) {
+               TRANSMUTE(Player, this);
+       } else if (IS_REAL_CLIENT(this)) {
+               msg_entity = this;
+               WriteByte(MSG_ONE, SVC_SETVIEW);
+               WriteEntity(MSG_ONE, this);
+       }
+       if (game_stopped)
+               TRANSMUTE(Observer, this);
  
-               MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
+       SetSpectatee(this, NULL);
  
-               if (!warmup_stage && !this.alivetime)
-                       this.alivetime = time;
+       // reset player keys
+       if(PS(this))
+               PS(this).itemkeys = 0;
  
-               antilag_clear(this, CS(this));
+       MUTATOR_CALLHOOK(PutClientInServer, this);
+       if (IS_OBSERVER(this)) {
+               PutObserverInServer(this);
+       } else if (IS_PLAYER(this)) {
+               PutPlayerInServer(this);
        }
  }
  
@@@ -775,6 -772,7 +774,7 @@@ void ClientInit_misc(entity this
        else
                WriteString(channel, "");
        WriteByte(channel, this.count * 255.0); // g_balance_armor_blockpercent
+       WriteByte(channel, this.cnt * 255.0); // g_balance_damagepush_speedfactor
        WriteByte(channel, serverflags);
        WriteCoord(channel, autocvar_g_trueaim_minrange);
  }
@@@ -787,6 -785,11 +787,11 @@@ void ClientInit_CheckUpdate(entity this
                this.count = autocvar_g_balance_armor_blockpercent;
                this.SendFlags |= 1;
        }
+       if(this.cnt != autocvar_g_balance_damagepush_speedfactor)
+       {
+               this.cnt = autocvar_g_balance_damagepush_speedfactor;
+               this.SendFlags |= 1;
+       }
  }
  
  void ClientInit_Spawn()
@@@ -819,7 -822,7 +824,7 @@@ SetChangeParm
  void SetChangeParms (entity this)
  {
        // save parms for level change
-       parm1 = this.parm_idlesince - time;
+       parm1 = CS(this).parm_idlesince - time;
  
        MUTATOR_CALLHOOK(SetChangeParms);
  }
@@@ -832,12 -835,12 +837,12 @@@ DecodeLevelParm
  void DecodeLevelParms(entity this)
  {
        // load parms
-       this.parm_idlesince = parm1;
-       if (this.parm_idlesince == -(86400 * 366))
-               this.parm_idlesince = time;
+       CS(this).parm_idlesince = parm1;
+       if (CS(this).parm_idlesince == -(86400 * 366))
+               CS(this).parm_idlesince = time;
  
        // whatever happens, allow 60 seconds of idling directly after connect for map loading
-       this.parm_idlesince = max(this.parm_idlesince, time - sv_maxidle + 60);
+       CS(this).parm_idlesince = max(CS(this).parm_idlesince, time - sv_maxidle + 60);
  
        MUTATOR_CALLHOOK(DecodeLevelParms);
  }
@@@ -853,19 -856,19 +858,19 @@@ Called when a client types 'kill' in th
  .float clientkill_nexttime;
  void ClientKill_Now_TeamChange(entity this)
  {
-       if(this.killindicator_teamchange == -1)
+       if(CS(this).killindicator_teamchange == -1)
        {
                JoinBestTeam( this, false, true );
        }
-       else if(this.killindicator_teamchange == -2)
+       else if(CS(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;
+               SV_ChangeTeam(this, CS(this).killindicator_teamchange - 1);
+       CS(this).killindicator_teamchange = 0;
  }
  
  void ClientKill_Now(entity this)
        if(this.vehicle)
        {
            vehicles_exit(this.vehicle, VHEF_RELEASE);
-           if(!this.killindicator_teamchange)
+           if(!CS(this).killindicator_teamchange)
            {
              this.vehicle_health = -1;
              Damage(this, this, this, 1 , DEATH_KILL.m_id, this.origin, '0 0 0');
  
        this.killindicator = NULL;
  
-       if(this.killindicator_teamchange)
+       if(CS(this).killindicator_teamchange)
                ClientKill_Now_TeamChange(this);
  
 -      if(!IS_SPEC(this) && !IS_OBSERVER(this))
 +      if (!IS_SPEC(this) && !IS_OBSERVER(this) && MUTATOR_CALLHOOK(ClientKill_Now, this) == false)
 +      {
                Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
 +      }
  
        // now I am sure the player IS dead
  }
@@@ -950,7 -951,7 +955,7 @@@ void ClientKill_TeamChange (entity this
        return;
      killtime = M_ARGV(1, float);
  
-       this.killindicator_teamchange = targetteam;
+       CS(this).killindicator_teamchange = targetteam;
  
      if(!this.killindicator)
        {
@@@ -1053,25 -1054,41 +1058,41 @@@ void FixClientCvars(entity e
        MUTATOR_CALLHOOK(FixClientCvars, e);
  }
  
- float PlayerInIDList(entity p, string idlist)
+ bool findinlist_abbrev(string tofind, string list)
+ {
+       if(list == "" || tofind == "")
+               return false; // empty list or search, just return
+       // this function allows abbreviated strings!
+       FOREACH_WORD(list, it == substring(tofind, 0, strlen(it)),
+       {
+               return true;
+       });
+       return false;
+ }
+ bool PlayerInIPList(entity p, string iplist)
  {
-       float n, i;
-       string s;
+       // some safety checks (never allow local?)
+       if(p.netaddress == "local" || p.netaddress == "" || !IS_REAL_CLIENT(p))
+               return false;
  
+       return findinlist_abbrev(p.netaddress, iplist);
+ }
+ bool PlayerInIDList(entity p, string idlist)
+ {
        // NOTE: we do NOT check crypto_idfp_signed here, an unsigned ID is fine too for this
-       if (!p.crypto_idfp)
-               return 0;
+       if(!p.crypto_idfp)
+               return false;
  
-       // this function allows abbreviated player IDs too!
-       n = tokenize_console(idlist);
-       for(i = 0; i < n; ++i)
-       {
-               s = argv(i);
-               if(s == substring(p.crypto_idfp, 0, strlen(s)))
-                       return 1;
-       }
+       return findinlist_abbrev(p.crypto_idfp, idlist);
+ }
  
-       return 0;
+ bool PlayerInList(entity player, string list)
+ {
+       return boolean(PlayerInIDList(player, list) || PlayerInIPList(player, list));
  }
  
  #ifdef DP_EXT_PRECONNECT
@@@ -1112,8 -1129,8 +1133,8 @@@ void ClientConnect(entity this
  #ifdef WATERMARK
        Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_WATERMARK, WATERMARK);
  #endif
-       this.version_nagtime = time + 10 + random() * 10;
        TRANSMUTE(Client, this);
+       CS(this).version_nagtime = time + 10 + random() * 10;
  
        // identify the right forced team
        if (autocvar_g_campaign)
                        }
                }
        }
-       else if (PlayerInIDList(this, autocvar_g_forced_team_red))    this.team_forced = NUM_TEAM_1;
-       else if (PlayerInIDList(this, autocvar_g_forced_team_blue))   this.team_forced = NUM_TEAM_2;
-       else if (PlayerInIDList(this, autocvar_g_forced_team_yellow)) this.team_forced = NUM_TEAM_3;
-       else if (PlayerInIDList(this, autocvar_g_forced_team_pink))   this.team_forced = NUM_TEAM_4;
+       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;
  
        LogTeamchange(this.playerid, this.team, 1);
  
-       this.just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
+       CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
  
-       this.netname_previous = strzone(this.netname);
+       CS(this).netname_previous = strzone(this.netname);
  
        if(teamplay && IS_PLAYER(this))
                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_CONNECT_TEAM), this.netname);
  
        bot_relinkplayerlist();
  
-       this.spectatortime = time;
+       CS(this).spectatortime = time;
        if (blockSpectators)
        {
                Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
        }
  
-       this.jointime = time;
-       this.allowed_timeouts = autocvar_sv_timeout_number;
+       CS(this).jointime = time;
+       CS(this).allowed_timeouts = autocvar_sv_timeout_number;
  
        if (IS_REAL_CLIENT(this))
        {
  
        CSQCMODEL_AUTOINIT(this);
  
-       this.model_randomizer = random();
+       CS(this).model_randomizer = random();
  
        if (IS_REAL_CLIENT(this))
                sv_notice_join(this);
  
        // update physics stats (players can spawn before physics runs)
-       STAT(MOVEVARS_HIGHSPEED, this) = autocvar_g_movement_highspeed;
-       MUTATOR_CALLHOOK(PlayerPhysics_UpdateStats, this); // do it BEFORE the function so we can modify highspeed!
-       Physics_UpdateStats(this, PHYS_HIGHSPEED(this));
+       Physics_UpdateStats(this);
  
        IL_EACH(g_initforplayer, it.init_for_player, {
                it.init_for_player(it, this);
        {
                if (!autocvar_g_campaign && !IS_PLAYER(this))
                {
-                       this.motd_actived_time = -1;
+                       CS(this).motd_actived_time = -1;
                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOTD, getwelcomemessage(this));
                }
        }
@@@ -1275,7 -1290,7 +1294,7 @@@ void ClientDisconnect(entity this
  
        PlayerStats_GameReport_FinalizePlayer(this);
        if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
-       if (this.active_minigame) part_minigame(this);
+       if (CS(this).active_minigame) part_minigame(this);
        if (IS_PLAYER(this)) Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
  
        if (autocvar_sv_eventlog)
  
      MUTATOR_CALLHOOK(ClientDisconnect, this);
  
+       if (CS(this).netname_previous) strunzone(CS(this).netname_previous); // needs to be before the CS entity is removed!
        ClientState_detach(this);
  
        Portal_ClearAll(this);
  
        bot_relinkplayerlist();
  
-       if (this.netname_previous) strunzone(this.netname_previous);
        if (this.clientstatus) strunzone(this.clientstatus);
        if (this.weaponorder_byimpulse) strunzone(this.weaponorder_byimpulse);
        if (this.personal) delete(this.personal);
@@@ -1334,7 -1349,7 +1353,7 @@@ void ChatBubbleThink(entity this
  
        if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
        {
-               if ( this.owner.active_minigame )
+               if ( CS(this.owner).active_minigame )
                        this.mdl = "models/sprites/minigame_busy.iqm";
                else if (PHYS_INPUT_BUTTON_CHAT(this.owner))
                        this.mdl = "models/misc/chatbubble.spr";
@@@ -1648,9 -1663,9 +1667,9 @@@ void player_regen(entity this
  bool zoomstate_set;
  void SetZoomState(entity this, float newzoom)
  {
-       if(newzoom != this.zoomstate)
+       if(newzoom != CS(this).zoomstate)
        {
-               this.zoomstate = newzoom;
+               CS(this).zoomstate = newzoom;
                ClientData_Touch(this);
        }
        zoomstate_set = true;
@@@ -1660,16 -1675,16 +1679,16 @@@ void GetPressedKeys(entity this
  {
        MUTATOR_CALLHOOK(GetPressedKeys, this);
        int keys = STAT(PRESSED_KEYS, this);
-       keys = BITSET(keys, KEY_FORWARD,        this.movement.x > 0);
-       keys = BITSET(keys, KEY_BACKWARD,       this.movement.x < 0);
-       keys = BITSET(keys, KEY_RIGHT,          this.movement.y > 0);
-       keys = BITSET(keys, KEY_LEFT,           this.movement.y < 0);
+       keys = BITSET(keys, KEY_FORWARD,        CS(this).movement.x > 0);
+       keys = BITSET(keys, KEY_BACKWARD,       CS(this).movement.x < 0);
+       keys = BITSET(keys, KEY_RIGHT,          CS(this).movement.y > 0);
+       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_ATCK,           PHYS_INPUT_BUTTON_ATCK(this));
        keys = BITSET(keys, KEY_ATCK2,          PHYS_INPUT_BUTTON_ATCK2(this));
-       this.pressedkeys = keys; // store for other users
+       CS(this).pressedkeys = keys; // store for other users
  
        STAT(PRESSED_KEYS, this) = keys;
  }
@@@ -1698,7 -1713,7 +1717,7 @@@ void SpectateCopy(entity this, entity s
        this.clip_size = spectatee.clip_size;
        this.effects = spectatee.effects & EFMASK_CHEAP; // eat performance
        this.health = spectatee.health;
-       this.impulse = 0;
+       CS(this).impulse = 0;
        this.items = spectatee.items;
        this.last_pickup = spectatee.last_pickup;
        this.hit_time = spectatee.hit_time;
        this.superweapons_finished = spectatee.superweapons_finished;
        STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
        this.weapons = spectatee.weapons;
-       this.dual_weapons = spectatee.dual_weapons;
        this.vortex_charge = spectatee.vortex_charge;
        this.vortex_chargepool_ammo = spectatee.vortex_chargepool_ammo;
        this.hagar_load = spectatee.hagar_load;
                this.fixangle = true;
        setorigin(this, spectatee.origin);
        setsize(this, spectatee.mins, spectatee.maxs);
-       SetZoomState(this, spectatee.zoomstate);
+       SetZoomState(this, CS(spectatee).zoomstate);
  
        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
@@@ -1808,10 -1822,10 +1826,10 @@@ bool SpectateSet(entity this
  
  void SetSpectatee_status(entity this, int spectatee_num)
  {
-       int oldspectatee_status = this.spectatee_status;
-       this.spectatee_status = spectatee_num;
+       int oldspectatee_status = CS(this).spectatee_status;
+       CS(this).spectatee_status = spectatee_num;
  
-       if (this.spectatee_status != oldspectatee_status)
+       if (CS(this).spectatee_status != oldspectatee_status)
        {
                ClientData_Touch(this);
                if (g_race || g_cts) race_InitSpectator();
@@@ -1943,7 -1957,7 +1961,7 @@@ void ShowRespawnCountdown(entity this
  .bool team_selected;
  bool ShowTeamSelection(entity this)
  {
-       if(!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (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) || this.team_forced > 0)
                return false;
        stuffcmd(this, "menu_showteamselect\n");
        return true;
@@@ -2029,7 -2043,7 +2047,7 @@@ void checkSpectatorBlock(entity this
        if(!this.caplayer)
        if(IS_REAL_CLIENT(this))
        {
-               if( time > (this.spectatortime + autocvar_g_maxplayers_spectator_blocktime) ) {
+               if( time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime) ) {
                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
                        dropclient(this);
                }
  
  void PrintWelcomeMessage(entity this)
  {
-       if(this.motd_actived_time == 0)
+       if(CS(this).motd_actived_time == 0)
        {
                if (autocvar_g_campaign) {
                        if ((IS_PLAYER(this) && PHYS_INPUT_BUTTON_INFO(this)) || (!IS_PLAYER(this))) {
-                               this.motd_actived_time = time;
+                               CS(this).motd_actived_time = time;
                                Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOTD, campaign_message);
                        }
                } else {
                        if (PHYS_INPUT_BUTTON_INFO(this)) {
-                               this.motd_actived_time = time;
+                               CS(this).motd_actived_time = time;
                                Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOTD, getwelcomemessage(this));
                        }
                }
        }
-       else if(this.motd_actived_time > 0) // showing MOTD or campaign message
+       else if(CS(this).motd_actived_time > 0) // showing MOTD or campaign message
        {
                if (autocvar_g_campaign) {
                        if (PHYS_INPUT_BUTTON_INFO(this))
-                               this.motd_actived_time = time;
-                       else if ((time - this.motd_actived_time > 2) && IS_PLAYER(this)) { // hide it some seconds after BUTTON_INFO has been released
-                               this.motd_actived_time = 0;
+                               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);
                        }
                } else {
                        if (PHYS_INPUT_BUTTON_INFO(this))
-                               this.motd_actived_time = time;
-                       else if (time - this.motd_actived_time > 2) { // hide it some seconds after BUTTON_INFO has been released
-                               this.motd_actived_time = 0;
+                               CS(this).motd_actived_time = time;
+                       else if (time - CS(this).motd_actived_time > 2) { // 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);
                        }
                }
        }
-       else //if(this.motd_actived_time < 0) // just connected, motd is active
+       else //if(CS(this).motd_actived_time < 0) // just connected, motd is active
        {
                if(PHYS_INPUT_BUTTON_INFO(this)) // BUTTON_INFO hides initial MOTD
-                       this.motd_actived_time = -2; // wait until BUTTON_INFO gets released
-               else if(this.motd_actived_time == -2 || IS_PLAYER(this) || IS_SPEC(this))
+                       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))
                {
                        // instanctly hide MOTD
-                       this.motd_actived_time = 0;
+                       CS(this).motd_actived_time = 0;
                        Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_MOTD);
                }
        }
  
  bool joinAllowed(entity this)
  {
-       if (this.version_mismatch) return false;
+       if (CS(this).version_mismatch) return false;
        if (!nJoinAllowed(this, this)) return false;
        if (teamplay && lockteams) return false;
        if (ShowTeamSelection(this)) return false;
        return true;
  }
  
+ .int items_added;
+ bool PlayerThink(entity this)
+ {
+       if (game_stopped || intermission_running) {
+               this.modelflags &= ~MF_ROCKET;
+               if(intermission_running)
+                       IntermissionThink(this);
+               return false;
+       }
+       if (timeout_status == TIMEOUT_ACTIVE) {
+         // don't allow the player to turn around while game is paused
+               // FIXME turn this into CSQC stuff
+               this.v_angle = this.lastV_angle;
+               this.angles = this.lastV_angle;
+               this.fixangle = true;
+       }
+       if (frametime) player_powerups(this);
+       if (IS_DEAD(this)) {
+               if (this.personal && g_race_qualifying) {
+                       if (time > this.respawn_time) {
+                               STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second
+                               respawn(this);
+                               CS(this).impulse = CHIMPULSE_SPEEDRUN.impulse;
+                       }
+               } else {
+                       if (frametime) player_anim(this);
+                       if (this.respawn_flags & RESPAWN_DENY)
+                       {
+                               STAT(RESPAWN_TIME, this) = 0;
+                               return false;
+                       }
+                       bool button_pressed = (PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this) || PHYS_INPUT_BUTTON_ATCK2(this) || PHYS_INPUT_BUTTON_HOOK(this) || PHYS_INPUT_BUTTON_USE(this));
+                       switch(this.deadflag)
+                       {
+                               case DEAD_DYING:
+                               {
+                                       if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max))
+                                               this.deadflag = DEAD_RESPAWNING;
+                                       else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)))
+                                               this.deadflag = DEAD_DEAD;
+                                       break;
+                               }
+                               case DEAD_DEAD:
+                               {
+                                       if (button_pressed)
+                                               this.deadflag = DEAD_RESPAWNABLE;
+                                       else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))
+                                               this.deadflag = DEAD_RESPAWNING;
+                                       break;
+                               }
+                               case DEAD_RESPAWNABLE:
+                               {
+                                       if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE))
+                                               this.deadflag = DEAD_RESPAWNING;
+                                       break;
+                               }
+                               case DEAD_RESPAWNING:
+                               {
+                                       if (time > this.respawn_time)
+                                       {
+                                               this.respawn_time = time + 1; // only retry once a second
+                                               this.respawn_time_max = this.respawn_time;
+                                               respawn(this);
+                                       }
+                                       break;
+                               }
+                       }
+                       ShowRespawnCountdown(this);
+                       if (this.respawn_flags & RESPAWN_SILENT)
+                               STAT(RESPAWN_TIME, this) = 0;
+                       else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max)
+                       {
+                               if (time < this.respawn_time)
+                                       STAT(RESPAWN_TIME, this) = this.respawn_time;
+                               else if (this.deadflag != DEAD_RESPAWNING)
+                                       STAT(RESPAWN_TIME, this) = -this.respawn_time_max;
+                       }
+                       else
+                               STAT(RESPAWN_TIME, this) = this.respawn_time;
+               }
+               // if respawning, invert stat_respawn_time to indicate this, the client translates it
+               if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0)
+                       STAT(RESPAWN_TIME, this) *= -1;
+               return false;
+       }
+       bool have_hook = false;
+       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+       {
+               .entity weaponentity = weaponentities[slot];
+               if(this.(weaponentity).hook.state)
+               {
+                       have_hook = true;
+                       break;
+               }
+       }
+       bool do_crouch = PHYS_INPUT_BUTTON_CROUCH(this);
+       if (have_hook) {
+               do_crouch = false;
+       } else if (this.waterlevel >= WATERLEVEL_SWIMMING) {
+               do_crouch = false;
+       } else if (this.vehicle) {
+               do_crouch = false;
+       } else if (STAT(FROZEN, this)) {
+               do_crouch = false;
+     }
+       if (do_crouch) {
+               if (!this.crouch) {
+                       this.crouch = true;
+                       this.view_ofs = STAT(PL_CROUCH_VIEW_OFS, this);
+                       setsize(this, STAT(PL_CROUCH_MIN, this), STAT(PL_CROUCH_MAX, this));
+                       // setanim(this, this.anim_duck, false, true, true); // this anim is BROKEN anyway
+               }
+       } else if (this.crouch) {
+         tracebox(this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), this.origin, false, this);
+         if (!trace_startsolid) {
+             this.crouch = false;
+             this.view_ofs = STAT(PL_VIEW_OFS, this);
+             setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
+         }
+       }
+       FixPlayermodel(this);
+       // 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];
+                       W_WeaponFrame(this, weaponentity);
+                       if(slot == 0)
+                       {
+                               this.clip_load = this.(weaponentity).clip_load;
+                               this.clip_size = this.(weaponentity).clip_size;
+                       }
+               }
+               this.items_added = 0;
+               if ((this.items & ITEM_Jetpack.m_itemid) && ((this.items & ITEM_JetpackRegen.m_itemid) || this.ammo_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)
+       {
+               .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);
+       // secret status
+       secrets_setstatus(this);
+       // monsters status
+       monsters_setstatus(this);
+       this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
+       return true;
+ }
  void ObserverThink(entity this)
  {
-       if ( this.impulse )
+       if ( CS(this).impulse )
        {
-               MinigameImpulse(this, this.impulse);
-               this.impulse = 0;
+               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) && !this.version_mismatch) {
+               } else if(PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) {
                        this.flags &= ~FL_JUMPRELEASED;
                        if(SpectateNext(this)) {
                                TRANSMUTE(Spectator, this);
  
  void SpectatorThink(entity this)
  {
-       if ( this.impulse )
+       if ( CS(this).impulse )
        {
-               if(MinigameImpulse(this, this.impulse))
-                       this.impulse = 0;
+               if(MinigameImpulse(this, CS(this).impulse))
+                       CS(this).impulse = 0;
  
-               if (this.impulse == IMP_weapon_drop.impulse)
+               if (CS(this).impulse == IMP_weapon_drop.impulse)
                {
                        STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
-                       this.impulse = 0;
+                       CS(this).impulse = 0;
                        return;
                }
        }
                if (PHYS_INPUT_BUTTON_JUMP(this) && joinAllowed(this)) {
                        this.flags &= ~FL_JUMPRELEASED;
                        this.flags |= FL_SPAWNING;
-               } else if(PHYS_INPUT_BUTTON_ATCK(this) || this.impulse == 10 || this.impulse == 15 || this.impulse == 18 || (this.impulse >= 200 && this.impulse <= 209)) {
+               } 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)) {
                        this.flags &= ~FL_JUMPRELEASED;
                        if(SpectateNext(this)) {
                                TRANSMUTE(Spectator, this);
                                TRANSMUTE(Observer, this);
                                PutClientInServer(this);
                        }
-                       this.impulse = 0;
-               } else if(this.impulse == 12 || this.impulse == 16  || this.impulse == 19 || (this.impulse >= 220 && this.impulse <= 229)) {
+                       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);
                                TRANSMUTE(Observer, this);
                                PutClientInServer(this);
                        }
-                       this.impulse = 0;
+                       CS(this).impulse = 0;
                } else if (PHYS_INPUT_BUTTON_ATCK2(this)) {
                        this.flags &= ~FL_JUMPRELEASED;
                        TRANSMUTE(Observer, this);
@@@ -2246,20 -2443,11 +2447,11 @@@ PlayerPreThin
  Called every frame for each client before the physics are run
  =============
  */
- .float usekeypressed;
  .float last_vehiclecheck;
- .int items_added;
  void PlayerPreThink (entity this)
  {
        WarpZone_PlayerPhysics_FixVAngle(this);
  
-     STAT(GAMESTARTTIME, this) = game_starttime;
-       STAT(ROUNDSTARTTIME, this) = round_starttime;
-       STAT(ALLOW_OLDVORTEXBEAM, this) = autocvar_g_allow_oldvortexbeam;
-       STAT(LEADLIMIT, this) = autocvar_leadlimit;
-       STAT(WEAPONSINMAP, this) = weaponsInMap;
        if (frametime) {
                // physics frames: update anticheat stuff
                anticheat_prethink(this);
                this.netname = strzone(sprintf("Player#%d", this.playerid));
                // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
        }
-       if (this.netname != this.netname_previous) {
+       if (this.netname != CS(this).netname_previous) {
                if (autocvar_sv_eventlog) {
                        GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this, false)));
          }
-               if (this.netname_previous) strunzone(this.netname_previous);
-               this.netname_previous = strzone(this.netname);
+               if (CS(this).netname_previous) strunzone(CS(this).netname_previous);
+               CS(this).netname_previous = strzone(this.netname);
        }
  
        // version nagging
-       if (this.version_nagtime && this.cvar_g_xonoticversion && time > this.version_nagtime) {
-         this.version_nagtime = 0;
+       if (CS(this).version_nagtime && this.cvar_g_xonoticversion && time > CS(this).version_nagtime) {
+         CS(this).version_nagtime = 0;
          if (strstrofs(this.cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(this.cvar_g_xonoticversion, "autobuild", 0) >= 0) {
              // git client
          } else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0) {
                this.max_armorvalue = 0;
        }
  
-       if (STAT(FROZEN, this) == 2)
-       {
-               this.revive_progress = bound(0, this.revive_progress + frametime * this.revive_speed, 1);
-               this.health = max(1, this.revive_progress * start_health);
-               this.iceblock.alpha = bound(0.2, 1 - this.revive_progress, 1);
-               if (this.revive_progress >= 1)
-                       Unfreeze(this);
-       }
-       else if (STAT(FROZEN, this) == 3)
+       if(IS_PLAYER(this))
        {
-               this.revive_progress = bound(0, this.revive_progress - frametime * this.revive_speed, 1);
-               this.health = max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * this.revive_progress );
+               if (STAT(FROZEN, this) == 2)
+               {
+                       this.revive_progress = bound(0, this.revive_progress + frametime * this.revive_speed, 1);
+                       this.health = max(1, this.revive_progress * start_health);
+                       this.iceblock.alpha = bound(0.2, 1 - this.revive_progress, 1);
  
-               if (this.health < 1)
+                       if (this.revive_progress >= 1)
+                               Unfreeze(this);
+               }
+               else if (STAT(FROZEN, this) == 3)
                {
-                       if (this.vehicle)
-                               vehicles_exit(this.vehicle, VHEF_RELEASE);
-                       if(this.event_damage)
-                               this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, this.origin, '0 0 0');
+                       this.revive_progress = bound(0, this.revive_progress - frametime * this.revive_speed, 1);
+                       this.health = max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * this.revive_progress );
+                       if (this.health < 1)
+                       {
+                               if (this.vehicle)
+                                       vehicles_exit(this.vehicle, VHEF_RELEASE);
+                               if(this.event_damage)
+                                       this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, this.origin, '0 0 0');
+                       }
+                       else if (this.revive_progress <= 0)
+                               Unfreeze(this);
                }
-               else if (this.revive_progress <= 0)
-                       Unfreeze(this);
        }
  
        MUTATOR_CALLHOOK(PlayerPreThink, this);
  
        if(!this.cvar_cl_newusekeysupported) // FIXME remove this - it was a stupid idea to begin with, we can JUST use the button
        {
-               if(PHYS_INPUT_BUTTON_USE(this) && !this.usekeypressed)
+               if(PHYS_INPUT_BUTTON_USE(this) && !CS(this).usekeypressed)
                        PlayerUseKey(this);
-               this.usekeypressed = PHYS_INPUT_BUTTON_USE(this);
+               CS(this).usekeypressed = PHYS_INPUT_BUTTON_USE(this);
        }
  
        if (IS_REAL_CLIENT(this))
                PrintWelcomeMessage(this);
  
        if (IS_PLAYER(this)) {
-               CheckRules_Player(this);
-               if (game_stopped || intermission_running) {
-                       this.modelflags &= ~MF_ROCKET;
-                       if(intermission_running)
-                               IntermissionThink(this);
-                       return;
-               }
-               if (timeout_status == TIMEOUT_ACTIVE) {
-             // don't allow the player to turn around while game is paused
-                       // FIXME turn this into CSQC stuff
-                       this.v_angle = this.lastV_angle;
-                       this.angles = this.lastV_angle;
-                       this.fixangle = true;
-               }
-               if (frametime) player_powerups(this);
-               if (IS_DEAD(this)) {
-                       if (this.personal && g_race_qualifying) {
-                               if (time > this.respawn_time) {
-                                       STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second
-                                       respawn(this);
-                                       this.impulse = CHIMPULSE_SPEEDRUN.impulse;
-                               }
-                       } else {
-                               if (frametime) player_anim(this);
-                               if (this.respawn_flags & RESPAWN_DENY)
-                               {
-                                       STAT(RESPAWN_TIME, this) = 0;
-                                       return;
-                               }
-                               bool button_pressed = (PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this) || PHYS_INPUT_BUTTON_ATCK2(this) || PHYS_INPUT_BUTTON_HOOK(this) || PHYS_INPUT_BUTTON_USE(this));
-                               switch(this.deadflag)
-                               {
-                                       case DEAD_DYING:
-                                       {
-                                               if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max))
-                                                       this.deadflag = DEAD_RESPAWNING;
-                                               else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)))
-                                                       this.deadflag = DEAD_DEAD;
-                                               break;
-                                       }
-                                       case DEAD_DEAD:
-                                       {
-                                               if (button_pressed)
-                                                       this.deadflag = DEAD_RESPAWNABLE;
-                                               else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))
-                                                       this.deadflag = DEAD_RESPAWNING;
-                                               break;
-                                       }
-                                       case DEAD_RESPAWNABLE:
-                                       {
-                                               if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE))
-                                                       this.deadflag = DEAD_RESPAWNING;
-                                               break;
-                                       }
-                                       case DEAD_RESPAWNING:
-                                       {
-                                               if (time > this.respawn_time)
-                                               {
-                                                       this.respawn_time = time + 1; // only retry once a second
-                                                       this.respawn_time_max = this.respawn_time;
-                                                       respawn(this);
-                                               }
-                                               break;
-                                       }
-                               }
-                               ShowRespawnCountdown(this);
-                               if (this.respawn_flags & RESPAWN_SILENT)
-                                       STAT(RESPAWN_TIME, this) = 0;
-                               else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max)
-                               {
-                                       if (time < this.respawn_time)
-                                               STAT(RESPAWN_TIME, this) = this.respawn_time;
-                                       else if (this.deadflag != DEAD_RESPAWNING)
-                                               STAT(RESPAWN_TIME, this) = -this.respawn_time_max;
-                               }
-                               else
-                                       STAT(RESPAWN_TIME, this) = this.respawn_time;
-                       }
-                       // if respawning, invert stat_respawn_time to indicate this, the client translates it
-                       if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0)
-                               STAT(RESPAWN_TIME, this) *= -1;
+               if(!PlayerThink(this))
                        return;
-               }
-               this.prevorigin = this.origin;
-               bool have_hook = false;
-               for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
-               {
-                       .entity weaponentity = weaponentities[slot];
-                       if(this.(weaponentity).hook.state)
-                       {
-                               have_hook = true;
-                               break;
-                       }
-               }
-               bool do_crouch = PHYS_INPUT_BUTTON_CROUCH(this);
-               if (have_hook) {
-                       do_crouch = false;
-               } else if (this.waterlevel >= WATERLEVEL_SWIMMING) {
-                       do_crouch = false;
-               } else if (this.vehicle) {
-                       do_crouch = false;
-               } else if (STAT(FROZEN, this)) {
-                       do_crouch = false;
-         }
-               if (do_crouch) {
-                       if (!this.crouch) {
-                               this.crouch = true;
-                               this.view_ofs = STAT(PL_CROUCH_VIEW_OFS, this);
-                               setsize(this, STAT(PL_CROUCH_MIN, this), STAT(PL_CROUCH_MAX, this));
-                               // setanim(this, this.anim_duck, false, true, true); // this anim is BROKEN anyway
-                       }
-               } else if (this.crouch) {
-             tracebox(this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), this.origin, false, this);
-             if (!trace_startsolid) {
-                 this.crouch = false;
-                 this.view_ofs = STAT(PL_VIEW_OFS, this);
-                 setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
-             }
-               }
-               FixPlayermodel(this);
-               // 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];
-                               W_WeaponFrame(this, weaponentity);
-                               if(slot == 0)
-                               {
-                                       this.clip_load = this.(weaponentity).clip_load;
-                                       this.clip_size = this.(weaponentity).clip_size;
-                               }
-                       }
-                       this.items_added = 0;
-                       if (this.items & ITEM_Jetpack.m_itemid && (this.items & ITEM_JetpackRegen.m_itemid || this.ammo_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)
-               {
-                       .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);
-               // secret status
-               secrets_setstatus(this);
-               // monsters status
-               monsters_setstatus(this);
-               this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
        }
        else if (game_stopped || intermission_running) {
                if(intermission_running)
                SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
      }
  
-       if (this.teamkill_soundtime && time > this.teamkill_soundtime)
+       if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
        {
-               this.teamkill_soundtime = 0;
+               CS(this).teamkill_soundtime = 0;
  
-               entity e = this.teamkill_soundsource;
+               entity e = CS(this).teamkill_soundsource;
                entity oldpusher = e.pusher;
                e.pusher = this;
                PlayerSound(e, playersound_teamshoot, CH_VOICE, VOL_BASEVOICE, VOICETYPE_LASTATTACKER_ONLY);
                e.pusher = oldpusher;
        }
  
-       if (this.taunt_soundtime && time > this.taunt_soundtime) {
-               this.taunt_soundtime = 0;
+       if (CS(this).taunt_soundtime && time > CS(this).taunt_soundtime) {
+               CS(this).taunt_soundtime = 0;
                PlayerSound(this, playersound_taunt, CH_VOICE, VOL_BASEVOICE, VOICETYPE_AUTOTAUNT);
        }
  
@@@ -2636,12 -2649,12 +2653,12 @@@ void Player_Physics(entity this
        if(!this.move_qcphysics)
                return;
  
-       if(!frametime && !this.pm_frametime)
+       if(!frametime && !CS(this).pm_frametime)
                return;
  
-       Movetype_Physics_NoMatchTicrate(this, this.pm_frametime, true);
+       Movetype_Physics_NoMatchTicrate(this, CS(this).pm_frametime, true);
  
-       this.pm_frametime = 0;
+       CS(this).pm_frametime = 0;
  }
  
  /*
@@@ -2651,7 -2664,6 +2668,6 @@@ PlayerPostThin
  Called every frame for each client after the physics are run
  =============
  */
- .float idlekick_lasttimeleft;
  void PlayerPostThink (entity this)
  {
        Player_Physics(this);
  
                if (sv_maxidle_slots > 0 && (maxclients - totalClients) > sv_maxidle_slots)
                { /* do nothing */ }
-               else if (time - this.parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
+               else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
                {
-                       if (this.idlekick_lasttimeleft)
+                       if (CS(this).idlekick_lasttimeleft)
                        {
-                               this.idlekick_lasttimeleft = 0;
+                               CS(this).idlekick_lasttimeleft = 0;
                                Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_IDLING);
                        }
                }
                else
                {
-                       float timeleft = ceil(sv_maxidle - (time - this.parm_idlesince));
+                       float timeleft = ceil(sv_maxidle - (time - CS(this).parm_idlesince));
                        if (timeleft == min(10, sv_maxidle - 1)) { // - 1 to support sv_maxidle <= 10
-                               if (!this.idlekick_lasttimeleft)
+                               if (!CS(this).idlekick_lasttimeleft)
                                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
                        }
                        if (timeleft <= 0) {
                                return;
                        }
                        else if (timeleft <= 10) {
-                               if (timeleft != this.idlekick_lasttimeleft) {
+                               if (timeleft != CS(this).idlekick_lasttimeleft) {
                                    Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_IDLE, timeleft));
                  }
-                               this.idlekick_lasttimeleft = timeleft;
+                               CS(this).idlekick_lasttimeleft = timeleft;
                        }
                }
        }
  
        CheatFrame(this);
  
-       //CheckPlayerJump();
        if (game_stopped)
        {
                this.solid = SOLID_NOT;
  
        if (IS_PLAYER(this)) {
                DrownPlayer(this);
-               CheckRules_Player(this);
                UpdateChatBubble(this);
-               if (this.impulse) ImpulseCommands(this);
+               if (CS(this).impulse) ImpulseCommands(this);
                if (game_stopped)
                {
                        CSQCMODEL_AUTOUPDATE(this);
  
        CSQCMODEL_AUTOUPDATE(this);
  }
+ // hack to copy the button fields from the client entity to the Client State
+ void PM_UpdateButtons(entity this, entity store)
+ {
+       if(this.impulse)
+               store.impulse = this.impulse;
+       this.impulse = 0;
+       store.button0 = this.button0;
+       store.button2 = this.button2;
+       store.button3 = this.button3;
+       store.button4 = this.button4;
+       store.button5 = this.button5;
+       store.button6 = this.button6;
+       store.button7 = this.button7;
+       store.button8 = this.button8;
+       store.button9 = this.button9;
+       store.button10 = this.button10;
+       store.button11 = this.button11;
+       store.button12 = this.button12;
+       store.button13 = this.button13;
+       store.button14 = this.button14;
+       store.button15 = this.button15;
+       store.button16 = this.button16;
+       store.buttonuse = this.buttonuse;
+       store.buttonchat = this.buttonchat;
+       store.cursor_active = this.cursor_active;
+       store.cursor_screen = this.cursor_screen;
+       store.cursor_trace_start = this.cursor_trace_start;
+       store.cursor_trace_endpos = this.cursor_trace_endpos;
+       store.cursor_trace_ent = this.cursor_trace_ent;
+       store.ping = this.ping;
+       store.ping_packetloss = this.ping_packetloss;
+       store.ping_movementloss = this.ping_movementloss;
+       store.v_angle = this.v_angle;
+       store.movement = this.movement;
+ }
index 51c2fa4f312c8c791b3f53c03f8b97dc9bd79743,03f484d2e68f566e42c526a7239a88bacf0312ec..1d5ab7b3980b63f24ac4d72371a070466a9e9185
@@@ -101,11 -101,11 +101,11 @@@ void ClientCommand_clientversion(entit
                        {
                                if (IS_CLIENT(caller))
                                {
-                                       caller.version = ((argv(1) == "$gameversion") ? 1 : stof(argv(1)));
+                                       CS(caller).version = ((argv(1) == "$gameversion") ? 1 : stof(argv(1)));
  
-                                       if (caller.version < autocvar_gameversion_min || caller.version > autocvar_gameversion_max)
+                                       if (CS(caller).version < autocvar_gameversion_min || CS(caller).version > autocvar_gameversion_max)
                                        {
-                                               caller.version_mismatch = 1;
+                                               CS(caller).version_mismatch = true;
                                                ClientKill_TeamChange(caller, -2);  // observe
                                        }
                                        else if (autocvar_g_campaign || autocvar_g_balance_teams)
@@@ -319,98 -319,82 +319,98 @@@ void ClientCommand_selectteam(entity ca
        {
                case CMD_REQUEST_COMMAND:
                {
 -                      if (argv(1) != "")
 +                      if (argv(1) == "")
                        {
 -                              if (IS_CLIENT(caller))
 +                              return;
 +                      }
 +                      if (!IS_CLIENT(caller))
 +                      {
 +                              return;
 +                      }
 +                      if (!teamplay)
 +                      {
 +                              sprint(caller, "^7selectteam can only be used in teamgames\n");
 +                              return;
 +                      }
 +                      if (caller.team_forced > 0)
 +                      {
 +                              sprint(caller, "^7selectteam can not be used as your team is forced\n");
 +                              return;
 +                      }
 +                      if (lockteams)
 +                      {
 +                              sprint(caller, "^7The game has already begun, you must wait until the next map to be able to join a team.\n");
 +                              return;
 +                      }
 +                      float selection;
 +                      switch (argv(1))
 +                      {
 +                              case "red":
                                {
 -                                      if (teamplay)
 -                                      {
 -                                              if (caller.team_forced <= 0)
 -                                              {
 -                                                      if (!lockteams)
 -                                                      {
 -                                                              float selection;
 -
 -                                                              switch (argv(1))
 -                                                              {
 -                                                                      case "red": selection = NUM_TEAM_1;
 -                                                                              break;
 -                                                                      case "blue": selection = NUM_TEAM_2;
 -                                                                              break;
 -                                                                      case "yellow": selection = NUM_TEAM_3;
 -                                                                              break;
 -                                                                      case "pink": selection = NUM_TEAM_4;
 -                                                                              break;
 -                                                                      case "auto": selection = (-1);
 -                                                                              break;
 -
 -                                                                      default: selection = 0;
 -                                                                              break;
 -                                                              }
 -
 -                                                              if (selection)
 -                                                              {
 -                                                                      if (caller.team == selection && selection != -1 && !IS_DEAD(caller))
 -                                                                      {
 -                                                                              sprint(caller, "^7You already are on that team.\n");
 -                                                                      }
 -                                                                      else if (CS(caller).wasplayer && autocvar_g_changeteam_banned)
 -                                                                      {
 -                                                                              sprint(caller, "^1You cannot change team, forbidden by the server.\n");
 -                                                                      }
 -                                                                      else
 -                                                                      {
 -                                                                              if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
 -                                                                              {
 -                                                                                      CheckAllowedTeams(caller);
 -                                                                                      GetTeamCounts(caller);
 -                                                                                      if (!TeamSmallerEqThanTeam(Team_TeamToNumber(selection), Team_TeamToNumber(caller.team), caller))
 -                                                                                      {
 -                                                                                              Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
 -                                                                                              return;
 -                                                                                      }
 -                                                                              }
 -                                                                              ClientKill_TeamChange(caller, selection);
 -                                                                      }
 -                                                                      if(!IS_PLAYER(caller))
 -                                                                              caller.team_selected = true; // avoids asking again for team selection on join
 -                                                              }
 -                                                      }
 -                                                      else
 -                                                      {
 -                                                              sprint(caller, "^7The game has already begun, you must wait until the next map to be able to join a team.\n");
 -                                                      }
 -                                              }
 -                                              else
 -                                              {
 -                                                      sprint(caller, "^7selectteam can not be used as your team is forced\n");
 -                                              }
 -                                      }
 -                                      else
 -                                      {
 -                                              sprint(caller, "^7selectteam can only be used in teamgames\n");
 -                                      }
 +                                      selection = NUM_TEAM_1;
 +                                      break;
                                }
 +                              case "blue":
 +                              {
 +                                      selection = NUM_TEAM_2;
 +                                      break;
 +                              }
 +                              case "yellow":
 +                              {
 +                                      selection = NUM_TEAM_3;
 +                                      break;
 +                              }
 +                              case "pink":
 +                              {
 +                                      selection = NUM_TEAM_4;
 +                                      break;
 +                              }
 +                              case "auto":
 +                              {
 +                                      selection = (-1);
 +                                      break;
 +                              }
 +                              default:
 +                              {
 +                                      return;
 +                              }
 +                      }
 +                      if (caller.team == selection && selection != -1 && !IS_DEAD(caller))
 +                      {
 +                              sprint(caller, "^7You already are on that team.\n");
 +                              return;
 +                      }
 +                      if (caller.wasplayer && autocvar_g_changeteam_banned)
 +                      {
 +                              sprint(caller, "^1You cannot change team, forbidden by the server.\n");
                                return;
                        }
 +                      if (selection == -1)
 +                      {
 +                              ClientKill_TeamChange(caller, selection);
 +                              if (!IS_PLAYER(caller))
 +                              {
 +                                      caller.team_selected = true; // avoids asking again for team selection on join
 +                              }
 +                              return;
 +                      }
 +                      if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
 +                      {
 +                              CheckAllowedTeams(caller);
 +                              GetTeamCounts(caller);
 +                              if ((BIT(Team_TeamToNumber(selection) - 1) & FindBestTeams(caller, false)) == 0)
 +                              {
 +                                      Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
 +                                      return;
 +                              }
 +                      }
 +                      ClientKill_TeamChange(caller, selection);
 +                      if (!IS_PLAYER(caller))
 +                      {
 +                              caller.team_selected = true; // avoids asking again for team selection on join
 +                      }
 +                      return;
                }
 -
                default:
                        sprint(caller, "Incorrect parameters for ^2selectteam^7\n");
                case CMD_REQUEST_USAGE:
index 44949da8ac8729d513cff5b317f5b7d35fdb4a43,4d181c4659d7dca887f99ca1c1c6f888911b7781..dba41cc9cee11d5897f58d569ff60695000b3878
@@@ -1091,15 -1091,9 +1091,15 @@@ void GameCommand_moveplayer(float reque
  
                                                                // If so, lets continue and finally move the player
                                                                client.team_forced = 0;
 -                                                              MoveToTeam(client, team_id, 6);
 -                                                              successful = strcat(successful, (successful ? ", " : ""), playername(client, false));
 -                                                              LOG_INFO("Player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ") has been moved to the ", Team_ColoredFullName(team_id), "^7.\n");
 +                                                              if (MoveToTeam(client, team_id, 6))
 +                                                              {
 +                                                                      successful = strcat(successful, (successful ? ", " : ""), playername(client, false));
 +                                                                      LOG_INFO("Player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ") has been moved to the ", Team_ColoredFullName(team_id), "^7.\n");
 +                                                              }
 +                                                              else
 +                                                              {
 +                                                                      LOG_INFO("Unable to move player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ")");
 +                                                              }
                                                                continue;
                                                        }
                                                        else
@@@ -1150,7 -1144,7 +1150,7 @@@ void GameCommand_nospectators(float req
                        FOREACH_CLIENT(IS_REAL_CLIENT(it) && (IS_SPEC(it) || IS_OBSERVER(it)) && !it.caplayer, LAMBDA(
                                if(!it.caplayer)
                                {
-                                       it.spectatortime = time;
+                                       CS(it).spectatortime = time;
                                        Send_Notification(NOTIF_ONE_ONLY, it, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
                                }
                        ));
index c8879918c3a89acb1a3cbf519ca96b002b5fd4ed,bc09d7a8cd9d92d3da6340807e8a09fd13b384cc..6522c46b32b0a55cd0c4c94e9b2f2d6ebb346a0f
@@@ -64,7 -64,7 +64,7 @@@ MUTATOR_HOOKABLE(ClientDisconnect, EV_C
      /** attacker      */ i(entity, MUTATOR_ARGV_1_entity) \
      /** target                */ i(entity, MUTATOR_ARGV_2_entity) \
      /** deathtype             */ i(float,  MUTATOR_ARGV_3_float) \
-     /** damage          */ i(float,  MUTATOR_ARGV_4_float) \
+     /** damage         */ i(float,  MUTATOR_ARGV_4_float) \
      /** damage                */ o(float,  MUTATOR_ARGV_4_float) \
      /**/
  MUTATOR_HOOKABLE(PlayerDies, EV_PlayerDies);
@@@ -130,31 -130,6 +130,31 @@@ MUTATOR_HOOKABLE(MatchEnd, EV_NO_ARGS)
      /**/
  MUTATOR_HOOKABLE(CheckAllowedTeams, EV_CheckAllowedTeams);
  
 +/** return true to manually override team counts */
 +MUTATOR_HOOKABLE(GetTeamCounts, EV_NO_ARGS);
 +
 +/** allow overriding of team counts */
 +#define EV_GetTeamCount(i, o) \
 +    /** team to count                   */ i(float, MUTATOR_ARGV_0_float) \
 +    /** player to ignore                */ i(entity, MUTATOR_ARGV_1_entity) \
 +    /** number of players in a team     */ i(float, MUTATOR_ARGV_2_float) \
 +    /**/                                   o(float, MUTATOR_ARGV_2_float) \
 +    /** number of bots in a team        */ i(float, MUTATOR_ARGV_3_float) \
 +    /**/                                   o(float, MUTATOR_ARGV_3_float) \
 +    /** lowest scoring player in a team */ i(entity, MUTATOR_ARGV_4_entity) \
 +    /**/                                   o(entity, MUTATOR_ARGV_4_entity) \
 +    /** lowest scoring bot in a team    */ i(entity, MUTATOR_ARGV_5_entity) \
 +    /**/                                   o(entity, MUTATOR_ARGV_5_entity) \
 +    /**/
 +MUTATOR_HOOKABLE(GetTeamCount, EV_GetTeamCount);
 +
 +/** allows overriding best teams */
 +#define EV_FindBestTeams(i, o) \
 +    /** player checked   */ i(entity, MUTATOR_ARGV_0_entity) \
 +    /** bitmask of teams */ o(float, MUTATOR_ARGV_1_float) \
 +    /**/
 +MUTATOR_HOOKABLE(FindBestTeams, EV_FindBestTeams);
 +
  /** copies variables for spectating "spectatee" to "this" */
  #define EV_SpectateCopy(i, o) \
      /** spectatee   */ i(entity, MUTATOR_ARGV_0_entity) \
@@@ -753,12 -728,6 +753,12 @@@ MUTATOR_HOOKABLE(Race_FinalCheckpoint, 
      /**/
  MUTATOR_HOOKABLE(ClientKill, EV_ClientKill);
  
 +/** called when player is about to be killed during kill command or changing teams */
 +#define EV_ClientKill_Now(i, o) \
 +    /** player */        i(entity, MUTATOR_ARGV_0_entity) \
 +    /**/
 +MUTATOR_HOOKABLE(ClientKill_Now, EV_ClientKill_Now);
 +
  #define EV_FixClientCvars(i, o) \
      /** player */        i(entity, MUTATOR_ARGV_0_entity) \
      /**/
@@@ -832,7 -801,7 +832,7 @@@ MUTATOR_HOOKABLE(CheckRules_World, EV_C
  MUTATOR_HOOKABLE(WantWeapon, EV_WantWeapon);
  
  #define EV_AddPlayerScore(i, o) \
-     /** score field */  i(PlayerScoreField, MUTATOR_ARGV_0_entity) \
+     /** score field */  i(entity, MUTATOR_ARGV_0_entity) \
      /** score */        i(float, MUTATOR_ARGV_1_float) \
      /**/                o(float, MUTATOR_ARGV_1_float) \
      /** player */       i(entity, MUTATOR_ARGV_2_entity) \
@@@ -906,9 -875,7 +906,9 @@@ MUTATOR_HOOKABLE(PrepareExplosionByDama
      /**/
  MUTATOR_HOOKABLE(MonsterModel, EV_MonsterModel);
  
 -/**/
 +/**
 + * Called before player changes their team. Return true to block team change.
 + */
  #define EV_Player_ChangeTeam(i, o) \
      /** player */         i(entity, MUTATOR_ARGV_0_entity) \
        /** current team */   i(float, MUTATOR_ARGV_1_float) \
      /**/
  MUTATOR_HOOKABLE(Player_ChangeTeam, EV_Player_ChangeTeam);
  
 +/**
 + * Called after player has changed their team.
 + */
 +#define EV_Player_ChangedTeam(i, o) \
 +    /** player */         i(entity, MUTATOR_ARGV_0_entity) \
 +      /** old team */       i(float, MUTATOR_ARGV_1_float) \
 +      /** current team */   i(float, MUTATOR_ARGV_2_float) \
 +    /**/
 +MUTATOR_HOOKABLE(Player_ChangedTeam, EV_Player_ChangedTeam);
 +
 +/**
 + * Called when player is about to be killed when changing teams. Return true to block killing.
 + */
 +#define EV_Player_ChangeTeamKill(i, o) \
 +    /** player */    i(entity, MUTATOR_ARGV_0_entity) \
 +    /**/
 +MUTATOR_HOOKABLE(Player_ChangeTeamKill, EV_Player_ChangeTeamKill);
 +
  /**/
  #define EV_URI_GetCallback(i, o) \
        /** id */       i(float, MUTATOR_ARGV_0_float) \
diff --combined qcsrc/server/player.qc
index 7bd683c3065954f8a0260ae4bf5a368a97e1c8b5,4e8663d24af0a5aaf8b6b3f574fdd9acd9dab8d8..cd6c23edb2f764ec325af110c56c55a598ac2294
@@@ -97,7 -97,6 +97,6 @@@ void CopyBody(entity this, float keepve
        clone.move_qcphysics = false; // don't run gamecode logic on clones, too many
        set_movetype(clone, this.move_movetype);
        clone.solid = this.solid;
-       clone.ballistics_density = this.ballistics_density;
        clone.takedamage = this.takedamage;
        setcefc(clone, getcefc(this));
        clone.uncustomizeentityforclient = this.uncustomizeentityforclient;
        //clone.weapon = this.weapon;
        setorigin(clone, this.origin);
        setsize(clone, this.mins, this.maxs);
-       clone.prevorigin = this.origin;
        clone.reset = SUB_Remove;
        clone._ps = this._ps;
  
@@@ -536,7 -534,7 +534,7 @@@ void PlayerDamage(entity this, entity i
                {
                        delete(this.killindicator);
                        this.killindicator = NULL;
-                       if(this.killindicator_teamchange)
+                       if(CS(this).killindicator_teamchange)
                                defer_ClientKill_Now_TeamChange = true;
  
                        if(this.classname == "body")
          // increment frag counter for used weapon type
          Weapon w = DEATH_WEAPONOF(deathtype);
                if(w != WEP_Null && accuracy_isgooddamage(attacker, this))
-                       attacker.accuracy.(accuracy_frags[w.m_id-1]) += 1;
+                       CS(attacker).accuracy.(accuracy_frags[w.m_id-1]) += 1;
  
                MUTATOR_CALLHOOK(PlayerDies, inflictor, attacker, this, deathtype, damage);
-               excess = M_ARGV(4, float);
+               damage = M_ARGV(4, float);
+               excess = max(0, damage - take - save);
  
                //Weapon wep = this.(weaponentity).m_weapon;
                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                set_movetype(this, MOVETYPE_TOSS);
                // shootable corpse
                this.solid = SOLID_CORPSE;
-               this.ballistics_density = autocvar_g_ballistics_density_corpse;
+               PS(this).ballistics_density = autocvar_g_ballistics_density_corpse;
                // don't stick to the floor
                UNSET_ONGROUND(this);
                // dying animation
                this.deadflag = DEAD_DYING;
  
+               STAT(MOVEVARS_SPECIALCOMMAND, this) = false; // sweet release
                // when to allow respawn
                calculate_player_respawn_time(this);
  
        }
  }
  
 -void MoveToTeam(entity client, int team_colour, int type)
 +bool MoveToTeam(entity client, int team_colour, int type)
  {
        int lockteams_backup = lockteams;  // backup any team lock
        lockteams = 0;  // disable locked teams
        TeamchangeFrags(client);  // move the players frags
 -      SetPlayerColors(client, team_colour - 1);  // set the players colour
 +      if (!SetPlayerTeamSimple(client, team_colour))
 +      {
 +              return false;
 +      }
        Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, client.origin, '0 0 0');  // kill the player
        lockteams = lockteams_backup;  // restore the team lock
        LogTeamchange(client.playerid, client.team, type);
 +      return true;
  }
  
  /** print(), but only print if the server is not local */
@@@ -747,7 -744,8 +748,8 @@@ int Say(entity source, int teamsay, ent
        if (!teamsay && !privatesay && substring(msgin, 0, 1) == " ")
          msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
  
-       msgin = formatmessage(source, msgin);
+     if(source)
+               msgin = formatmessage(source, msgin);
  
      string colorstr;
        if (!IS_PLAYER(source))
        // FLOOD CONTROL
        int flood = 0;
        var .float flood_field = floodcontrol_chat;
-       if(floodcontrol)
+       if(floodcontrol && source)
        {
                float flood_spl;
                float flood_burst;
                sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
  
      int ret;
-       if(source.muted)
+       if(source && CS(source).muted)
        {
                // always fake the message
                ret = -1;
                                        centerprint(privatesay, cmsgstr);
                        }
                }
-               else if ( teamsay && source.active_minigame )
+               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 && it.active_minigame == source.active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), sprint(it, msgstr));
+                       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && CS(it).active_minigame == CS(source).active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), sprint(it, msgstr));
                }
                else if(teamsay > 0) // team message, only sent to team mates
                {
diff --combined qcsrc/server/player.qh
index 3b520dabcea413b61ce234bac27ea6732b7770d7,1834bb865ffbb668801e525eb40b7e90f5afa341..dfa485e5f11fddad33799689e9d354fae640e572
@@@ -69,15 -69,8 +69,13 @@@ void calculate_player_respawn_time(enti
  
  void ClientKill_Now_TeamChange(entity this);
  
 -void MoveToTeam(entity client, float team_colour, float type);
 +/// \brief Moves player to the specified team.
 +/// \param[in,out] client Client to move.
 +/// \param[in] team_colour Color of the team.
 +/// \param[in] type ???
 +/// \return True on success, false otherwise.
 +bool MoveToTeam(entity client, float team_colour, float type);
  
  void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force);
  
- /** to be used by `prvm_edictset server playernumber muted 1` */
- .float muted;
  int Say(entity source, float teamsay, entity privatesay, string msgin, float floodcontrol);
diff --combined qcsrc/server/teamplay.qc
index 985c683e8c2cc4d0dcd03fd42c3919fe0e041330,2d6ceb8e0257f3546820544f276cb873d946e863..eadbe0f688f096666b2f0ed674174aa144619376
@@@ -87,8 -87,8 +87,8 @@@ void InitGameplayMode(
  
  string GetClientVersionMessage(entity this)
  {
-       if (this.version_mismatch) {
-               if(this.version < autocvar_gameversion) {
+       if (CS(this).version_mismatch) {
+               if(CS(this).version < autocvar_gameversion) {
                        return strcat("This is Xonotic ", autocvar_g_xonoticversion,
                                "\n^3Your client version is outdated.\n\n\n### YOU WON'T BE ABLE TO PLAY ON THIS SERVER ###\n\n\nPlease update!!!^8");
                } else {
@@@ -169,54 -169,49 +169,54 @@@ void setcolor(entity this, int clr
  #endif
  }
  
 -void SetPlayerColors(entity pl, float _color)
 +void SetPlayerColors(entity player, float _color)
  {
 -      /*string s;
 -      s = ftos(cl);
 -      stuffcmd(pl, strcat("color ", s, " ", s, "\n")  );
 -      pl.team = cl + 1;
 -      //pl.clientcolors = pl.clientcolors - (pl.clientcolors & 15) + cl;
 -      pl.clientcolors = 16*cl + cl;*/
 -
 -      float pants, shirt;
 -      pants = _color & 0x0F;
 -      shirt = _color & 0xF0;
 -
 -
 -      if(teamplay) {
 -              setcolor(pl, 16*pants + pants);
 -      } else {
 -              setcolor(pl, shirt + pants);
 +      float pants = _color & 0x0F;
 +      float shirt = _color & 0xF0;
 +      if (teamplay)
 +      {
 +              setcolor(player, 16 * pants + pants);
 +      }
 +      else
 +      {
 +              setcolor(player, shirt + pants);
        }
  }
  
 -void SetPlayerTeam(entity pl, float t, float s, float noprint)
 +bool SetPlayerTeamSimple(entity player, int teamnum)
  {
 -      float _color;
 -
 -      if(t == 4)
 -              _color = NUM_TEAM_4 - 1;
 -      else if(t == 3)
 -              _color = NUM_TEAM_3 - 1;
 -      else if(t == 2)
 -              _color = NUM_TEAM_2 - 1;
 -      else
 -              _color = NUM_TEAM_1 - 1;
 -
 -      SetPlayerColors(pl,_color);
 -
 -      if(t != s) {
 -              LogTeamchange(pl.playerid, pl.team, 3);  // log manual team join
 -
 -              if(!noprint)
 -                      bprint(playername(pl, false), "^7 has changed from ", Team_NumberToColoredFullName(s), "^7 to ", Team_NumberToColoredFullName(t), "\n");
 +      if (player.team == teamnum)
 +      {
 +              // This is important when players join the game and one of their color
 +              // matches the team color while other doesn't. For example [BOT]Lion.
 +              SetPlayerColors(player, teamnum - 1);
 +              return true;
        }
 +      if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, Team_TeamToNumber(
 +              player.team), Team_TeamToNumber(teamnum)) == true)
 +      {
 +              // Mutator has blocked team change.
 +              return false;
 +      }
 +      int oldteam = player.team;
 +      SetPlayerColors(player, teamnum - 1);
 +      MUTATOR_CALLHOOK(Player_ChangedTeam, player, oldteam, player.team);
 +      return true;
 +}
  
 +void SetPlayerTeam(entity player, int destinationteam, int sourceteam, bool noprint)
 +{
 +      int teamnum = Team_NumberToTeam(destinationteam);
 +      if (!SetPlayerTeamSimple(player, teamnum))
 +      {
 +              return;
 +      }
 +      LogTeamchange(player.playerid, player.team, 3);  // log manual team join
 +      if (noprint)
 +      {
 +              return;
 +      }
 +      bprint(playername(player, false), "^7 has changed from ", Team_NumberToColoredFullName(sourceteam), "^7 to ", Team_NumberToColoredFullName(destinationteam), "\n");
  }
  
  // set c1...c4 to show what teams are allowed
@@@ -225,7 -220,7 +225,7 @@@ void CheckAllowedTeams (entity for_whom
        int teams_mask = 0;
  
        c1 = c2 = c3 = c4 = -1;
 -      cb1 = cb2 = cb3 = cb4 = 0;
 +      numbotsteam1 = numbotsteam2 = numbotsteam3 = numbotsteam4 = 0;
  
        string teament_name = string_null;
  
@@@ -327,430 -322,218 +327,430 @@@ float PlayerValue(entity p
  // teams that are allowed will now have their player counts stored in c1...c4
  void GetTeamCounts(entity ignore)
  {
 -      float value, bvalue;
 -      // now count how many players are on each team already
 -
 -      // FIXME: also find and memorize the lowest-scoring bot on each team (in case players must be shuffled around)
 -      // also remember the lowest-scoring player
 -
 -      FOREACH_CLIENT(true, LAMBDA(
 -              float t;
 -              if(IS_PLAYER(it) || it.caplayer)
 -                      t = it.team;
 -              else if(it.team_forced > 0)
 -                      t = it.team_forced; // reserve the spot
 -              else
 -                      continue;
 -              if(it != ignore)// && it.netname != "")
 +      if (MUTATOR_CALLHOOK(GetTeamCounts) == true)
 +      {
 +              if (c1 >= 0)
 +              {
 +                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_1, ignore, c1, numbotsteam1,
 +                              lowestplayerteam1, lowestbotteam1);
 +                      c1 = M_ARGV(2, float);
 +                      numbotsteam1 = M_ARGV(3, float);
 +                      lowestplayerteam1 = M_ARGV(4, entity);
 +                      lowestbotteam1 = M_ARGV(5, entity);
 +              }
 +              if (c2 >= 0)
 +              {
 +                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_2, ignore, c2, numbotsteam2,
 +                              lowestplayerteam2, lowestbotteam2);
 +                      c2 = M_ARGV(2, float);
 +                      numbotsteam2 = M_ARGV(3, float);
 +                      lowestplayerteam2 = M_ARGV(4, entity);
 +                      lowestbotteam2 = M_ARGV(5, entity);
 +              }
 +              if (c3 >= 0)
                {
 +                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_3, ignore, c3, numbotsteam3,
 +                              lowestplayerteam3, lowestbotteam3);
 +                      c3 = M_ARGV(2, float);
 +                      numbotsteam3 = M_ARGV(3, float);
 +                      lowestplayerteam3 = M_ARGV(4, entity);
 +                      lowestbotteam3 = M_ARGV(5, entity);
 +              }
 +              if (c4 >= 0)
 +              {
 +                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_4, ignore, c4, numbotsteam4,
 +                              lowestplayerteam4, lowestbotteam4);
 +                      c4 = M_ARGV(2, float);
 +                      numbotsteam4 = M_ARGV(3, float);
 +                      lowestplayerteam4 = M_ARGV(4, entity);
 +                      lowestbotteam4 = M_ARGV(5, entity);
 +              }
 +      }
 +      else
 +      {
 +              float value, bvalue;
 +              // now count how many players are on each team already
 +              float lowestplayerscore1 = FLOAT_MAX;
 +              float lowestbotscore1 = FLOAT_MAX;
 +              float lowestplayerscore2 = FLOAT_MAX;
 +              float lowestbotscore2 = FLOAT_MAX;
 +              float lowestplayerscore3 = FLOAT_MAX;
 +              float lowestbotscore3 = FLOAT_MAX;
 +              float lowestplayerscore4 = FLOAT_MAX;
 +              float lowestbotscore4 = FLOAT_MAX;
 +              FOREACH_CLIENT(true, LAMBDA(
 +                      float t;
 +                      if (IS_PLAYER(it) || it.caplayer)
 +                      {
 +                              t = it.team;
 +                      }
 +                      else if (it.team_forced > 0)
 +                      {
 +                              t = it.team_forced; // reserve the spot
 +                      }
 +                      else
 +                      {
 +                              continue;
 +                      }
 +                      if (it == ignore)
 +                      {
 +                              continue;
 +                      }
                        value = PlayerValue(it);
 -                      if(IS_BOT_CLIENT(it))
 +                      if (IS_BOT_CLIENT(it))
 +                      {
                                bvalue = value;
 +                      }
                        else
 +                      {
                                bvalue = 0;
 -                      if(t == NUM_TEAM_1)
 +                      }
 +                      if (value == 0)
                        {
 -                              if(c1 >= 0)
 -                              {
 -                                      c1 = c1 + value;
 -                                      cb1 = cb1 + bvalue;
 -                              }
 +                              continue;
                        }
 -                      else if(t == NUM_TEAM_2)
 +                      switch (t)
                        {
 -                              if(c2 >= 0)
 +                              case NUM_TEAM_1:
                                {
 -                                      c2 = c2 + value;
 -                                      cb2 = cb2 + bvalue;
 +                                      if (c1 < 0)
 +                                      {
 +                                              break;
 +                                      }
 +                                      c1 += value;
 +                                      numbotsteam1 += bvalue;
 +                                      float tempscore = PlayerScore_Get(it, SP_SCORE);
 +                                      if (!bvalue)
 +                                      {
 +                                              if (tempscore < lowestplayerscore1)
 +                                              {
 +                                                      lowestplayerteam1 = it;
 +                                                      lowestplayerscore1 = tempscore;
 +                                              }
 +                                              break;
 +                                      }
 +                                      if (tempscore < lowestbotscore1)
 +                                      {
 +                                              lowestbotteam1 = it;
 +                                              lowestbotscore1 = tempscore;
 +                                      }
 +                                      break;
                                }
 -                      }
 -                      else if(t == NUM_TEAM_3)
 -                      {
 -                              if(c3 >= 0)
 +                              case NUM_TEAM_2:
                                {
 -                                      c3 = c3 + value;
 -                                      cb3 = cb3 + bvalue;
 +                                      if (c2 < 0)
 +                                      {
 +                                              break;
 +                                      }
 +                                      c2 += value;
 +                                      numbotsteam2 += bvalue;
 +                                      float tempscore = PlayerScore_Get(it, SP_SCORE);
 +                                      if (!bvalue)
 +                                      {
 +                                              if (tempscore < lowestplayerscore2)
 +                                              {
 +                                                      lowestplayerteam2 = it;
 +                                                      lowestplayerscore2 = tempscore;
 +                                              }
 +                                              break;
 +                                      }
 +                                      if (tempscore < lowestbotscore2)
 +                                      {
 +                                              lowestbotteam2 = it;
 +                                              lowestbotscore2 = tempscore;
 +                                      }
 +                                      break;
                                }
 -                      }
 -                      else if(t == NUM_TEAM_4)
 -                      {
 -                              if(c4 >= 0)
 +                              case NUM_TEAM_3:
                                {
 -                                      c4 = c4 + value;
 -                                      cb4 = cb4 + bvalue;
 +                                      if (c3 < 0)
 +                                      {
 +                                              break;
 +                                      }
 +                                      c3 += value;
 +                                      numbotsteam3 += bvalue;
 +                                      float tempscore = PlayerScore_Get(it, SP_SCORE);
 +                                      if (!bvalue)
 +                                      {
 +                                              if (tempscore < lowestplayerscore3)
 +                                              {
 +                                                      lowestplayerteam3 = it;
 +                                                      lowestplayerscore3 = tempscore;
 +                                              }
 +                                              break;
 +                                      }
 +                                      if (tempscore < lowestbotscore3)
 +                                      {
 +                                              lowestbotteam3 = it;
 +                                              lowestbotscore3 = tempscore;
 +                                      }
 +                                      break;
 +                              }
 +                              case NUM_TEAM_4:
 +                              {
 +                                      if (c4 < 0)
 +                                      {
 +                                              break;
 +                                      }
 +                                      c4 += value;
 +                                      numbotsteam4 += bvalue;
 +                                      float tempscore = PlayerScore_Get(it, SP_SCORE);
 +                                      if (!bvalue)
 +                                      {
 +                                              if (tempscore < lowestplayerscore4)
 +                                              {
 +                                                      lowestplayerteam4 = it;
 +                                                      lowestplayerscore4 = tempscore;
 +                                              }
 +                                              break;
 +                                      }
 +                                      if (tempscore < lowestbotscore4)
 +                                      {
 +                                              lowestbotteam4 = it;
 +                                              lowestbotscore4 = tempscore;
 +                                      }
 +                                      break;
                                }
                        }
 -              }
 -      ));
 +              ));
 +      }
  
        // if the player who has a forced team has not joined yet, reserve the spot
        if(autocvar_g_campaign)
        {
                switch(autocvar_g_campaign_forceteam)
                {
 -                      case 1: if(c1 == cb1) ++c1; break;
 -                      case 2: if(c2 == cb2) ++c2; break;
 -                      case 3: if(c3 == cb3) ++c3; break;
 -                      case 4: if(c4 == cb4) ++c4; break;
 +                      case 1: if(c1 == numbotsteam1) ++c1; break;
 +                      case 2: if(c2 == numbotsteam2) ++c2; break;
 +                      case 3: if(c3 == numbotsteam3) ++c3; break;
 +                      case 4: if(c4 == numbotsteam4) ++c4; break;
                }
        }
  }
  
 -float TeamSmallerEqThanTeam(float ta, float tb, entity e)
 +bool IsTeamSmallerThanTeam(int teama, int teamb, entity e, bool usescore)
  {
 +      // equal
 +      if (teama == teamb)
 +      {
 +              return false;
 +      }
        // we assume that CheckAllowedTeams and GetTeamCounts have already been called
 -      float f;
 -      float ca = -1, cb = -1, cba = 0, cbb = 0, sa = 0, sb = 0;
 +      float numplayersteama = -1, numplayersteamb = -1;
 +      float numbotsteama = 0, numbotsteamb = 0;
 +      float scoreteama = 0, scoreteamb = 0;
  
 -      switch(ta)
 +      switch (teama)
        {
 -              case 1: ca = c1; cba = cb1; sa = team1_score; break;
 -              case 2: ca = c2; cba = cb2; sa = team2_score; break;
 -              case 3: ca = c3; cba = cb3; sa = team3_score; break;
 -              case 4: ca = c4; cba = cb4; sa = team4_score; break;
 +              case 1: numplayersteama = c1; numbotsteama = numbotsteam1; scoreteama = team1_score; break;
 +              case 2: numplayersteama = c2; numbotsteama = numbotsteam2; scoreteama = team2_score; break;
 +              case 3: numplayersteama = c3; numbotsteama = numbotsteam3; scoreteama = team3_score; break;
 +              case 4: numplayersteama = c4; numbotsteama = numbotsteam4; scoreteama = team4_score; break;
        }
 -      switch(tb)
 +      switch (teamb)
        {
 -              case 1: cb = c1; cbb = cb1; sb = team1_score; break;
 -              case 2: cb = c2; cbb = cb2; sb = team2_score; break;
 -              case 3: cb = c3; cbb = cb3; sb = team3_score; break;
 -              case 4: cb = c4; cbb = cb4; sb = team4_score; break;
 +              case 1: numplayersteamb = c1; numbotsteamb = numbotsteam1; scoreteamb = team1_score; break;
 +              case 2: numplayersteamb = c2; numbotsteamb = numbotsteam2; scoreteamb = team2_score; break;
 +              case 3: numplayersteamb = c3; numbotsteamb = numbotsteam3; scoreteamb = team3_score; break;
 +              case 4: numplayersteamb = c4; numbotsteamb = numbotsteam4; scoreteamb = team4_score; break;
        }
  
        // invalid
 -      if(ca < 0 || cb < 0)
 +      if (numplayersteama < 0 || numplayersteamb < 0)
                return false;
  
 -      // equal
 -      if(ta == tb)
 +      if ((IS_REAL_CLIENT(e) && bots_would_leave))
 +      {
 +              numplayersteama -= numbotsteama;
 +              numplayersteamb -= numbotsteamb;
 +      }
 +      if (!usescore)
 +      {
 +              return numplayersteama < numplayersteamb;
 +      }
 +      if (numplayersteama < numplayersteamb)
 +      {
                return true;
 -
 -      if(IS_REAL_CLIENT(e))
 +      }
 +      if (numplayersteama > numplayersteamb)
        {
 -              if(bots_would_leave)
 -              {
 -                      ca -= cba * 0.999;
 -                      cb -= cbb * 0.999;
 -              }
 +              return false;
        }
 -
 -      // keep teams alive (teams of size 0 always count as smaller, ignoring score)
 -      if(ca < 1)
 -              if(cb >= 1)
 -                      return true;
 -      if(ca >= 1)
 -              if(cb < 1)
 -                      return false;
 -
 -      // first, normalize
 -      f = max(ca, cb, 1);
 -      ca /= f;
 -      cb /= f;
 -      f = max(sa, sb, 1);
 -      sa /= f;
 -      sb /= f;
 -
 -      // the more we're at the end of the match, the more take scores into account
 -      f = bound(0, game_completion_ratio * autocvar_g_balance_teams_scorefactor, 1);
 -      ca += (sa - ca) * f;
 -      cb += (sb - cb) * f;
 -
 -      return ca <= cb;
 +      return scoreteama < scoreteamb; 
  }
  
 -// returns # of smallest team (1, 2, 3, 4)
 -// NOTE: Assumes CheckAllowedTeams has already been called!
 -float FindSmallestTeam(entity pl, float ignore_pl)
 +bool IsTeamEqualToTeam(int teama, int teamb, entity e, bool usescore)
  {
 -      int totalteams = 0;
 -      int t = 1; // initialize with a random team?
 -      if(c4 >= 0) t = 4;
 -      if(c3 >= 0) t = 3;
 -      if(c2 >= 0) t = 2;
 -      if(c1 >= 0) t = 1;
 +      // equal
 +      if (teama == teamb)
 +      {
 +              return true;
 +      }
 +      // we assume that CheckAllowedTeams and GetTeamCounts have already been called
 +      float numplayersteama = -1, numplayersteamb = -1;
 +      float numbotsteama = 0, numbotsteamb = 0;
 +      float scoreteama = 0, scoreteamb = 0;
  
 -      // find out what teams are available
 -      //CheckAllowedTeams();
 +      switch (teama)
 +      {
 +              case 1: numplayersteama = c1; numbotsteama = numbotsteam1; scoreteama = team1_score; break;
 +              case 2: numplayersteama = c2; numbotsteama = numbotsteam2; scoreteama = team2_score; break;
 +              case 3: numplayersteama = c3; numbotsteama = numbotsteam3; scoreteama = team3_score; break;
 +              case 4: numplayersteama = c4; numbotsteama = numbotsteam4; scoreteama = team4_score; break;
 +      }
 +      switch (teamb)
 +      {
 +              case 1: numplayersteamb = c1; numbotsteamb = numbotsteam1; scoreteamb = team1_score; break;
 +              case 2: numplayersteamb = c2; numbotsteamb = numbotsteam2; scoreteamb = team2_score; break;
 +              case 3: numplayersteamb = c3; numbotsteamb = numbotsteam3; scoreteamb = team3_score; break;
 +              case 4: numplayersteamb = c4; numbotsteamb = numbotsteam4; scoreteamb = team4_score; break;
 +      }
  
 -      // make sure there are at least 2 teams to join
 -      if(c1 >= 0)
 -              totalteams = totalteams + 1;
 -      if(c2 >= 0)
 -              totalteams = totalteams + 1;
 -      if(c3 >= 0)
 -              totalteams = totalteams + 1;
 -      if(c4 >= 0)
 -              totalteams = totalteams + 1;
 +      // invalid
 +      if (numplayersteama < 0 || numplayersteamb < 0)
 +              return false;
  
 -      if((autocvar_bot_vs_human || pl.team_forced > 0) && totalteams == 1)
 -              totalteams += 1;
 +      if ((IS_REAL_CLIENT(e) && bots_would_leave))
 +      {
 +              numplayersteama -= numbotsteama;
 +              numplayersteamb -= numbotsteamb;
 +      }
 +      if (!usescore)
 +      {
 +              return numplayersteama == numplayersteamb;
 +      }
 +      if (numplayersteama < numplayersteamb)
 +      {
 +              return false;
 +      }
 +      if (numplayersteama > numplayersteamb)
 +      {
 +              return false;
 +      }
 +      return scoreteama == scoreteamb;        
 +}
  
 -      if(totalteams <= 1)
 +int FindBestTeams(entity player, bool usescore)
 +{
 +      if (MUTATOR_CALLHOOK(FindBestTeams, player) == true)
 +      {
 +              return M_ARGV(1, float);
 +      }
 +      int teambits = 0;
 +      int previousteam = 0;
 +      if (c1 >= 0)
 +      {
 +              teambits = BIT(0);
 +              previousteam = 1;
 +      }
 +      if (c2 >= 0)
 +      {
 +              if (previousteam == 0)
 +              {
 +                      teambits = BIT(1);
 +                      previousteam = 2;
 +              }
 +              else if (IsTeamSmallerThanTeam(2, previousteam, player, usescore))
 +              {
 +                      teambits = BIT(1);
 +                      previousteam = 2;
 +              }
 +              else if (IsTeamEqualToTeam(2, previousteam, player, usescore))
 +              {
 +                      teambits |= BIT(1);
 +                      previousteam = 2;
 +              }
 +      }
 +      if (c3 >= 0)
 +      {
 +              if (previousteam == 0)
 +              {
 +                      teambits = BIT(2);
 +                      previousteam = 3;
 +              }
 +              else if (IsTeamSmallerThanTeam(3, previousteam, player, usescore))
 +              {
 +                      teambits = BIT(2);
 +                      previousteam = 3;
 +              }
 +              else if (IsTeamEqualToTeam(3, previousteam, player, usescore))
 +              {
 +                      teambits |= BIT(2);
 +                      previousteam = 3;
 +              }
 +      }
 +      if (c4 >= 0)
        {
 -              if(autocvar_g_campaign && pl && IS_REAL_CLIENT(pl))
 -                      return 1; // special case for campaign and player joining
 -              else if(totalteams == 1) // single team
 -                      LOG_TRACEF("Only 1 team available for %s, you may need to fix your map", MapInfo_Type_ToString(MapInfo_CurrentGametype()));
 -              else // no teams, major no no
 -                      error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
 +              if (previousteam == 0)
 +              {
 +                      teambits = BIT(3);
 +              }
 +              else if (IsTeamSmallerThanTeam(4, previousteam, player, usescore))
 +              {
 +                      teambits = BIT(3);
 +              }
 +              else if (IsTeamEqualToTeam(4, previousteam, player, usescore))
 +              {
 +                      teambits |= BIT(3);
 +              }
        }
 +      return teambits;
 +}
  
 +// returns # of smallest team (1, 2, 3, 4)
 +// NOTE: Assumes CheckAllowedTeams has already been called!
 +float FindSmallestTeam(entity pl, float ignore_pl)
 +{
        // count how many players are in each team
 -      if(ignore_pl)
 +      if (ignore_pl)
 +      {
                GetTeamCounts(pl);
 +      }
        else
 +      {
                GetTeamCounts(NULL);
 -
 +      }
 +      int teambits = FindBestTeams(pl, true);
 +      if (teambits == 0)
 +      {
 +              error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
 +      }
        RandomSelection_Init();
 -
 -      if(TeamSmallerEqThanTeam(1, t, pl))
 -              t = 1;
 -      if(TeamSmallerEqThanTeam(2, t, pl))
 -              t = 2;
 -      if(TeamSmallerEqThanTeam(3, t, pl))
 -              t = 3;
 -      if(TeamSmallerEqThanTeam(4, t, pl))
 -              t = 4;
 -
 -      // now t is the minimum, or A minimum!
 -      if(t == 1 || TeamSmallerEqThanTeam(1, t, pl))
 +      if ((teambits & BIT(0)) != 0)
 +      {
                RandomSelection_AddFloat(1, 1, 1);
 -      if(t == 2 || TeamSmallerEqThanTeam(2, t, pl))
 +      }
 +      if ((teambits & BIT(1)) != 0)
 +      {
                RandomSelection_AddFloat(2, 1, 1);
 -      if(t == 3 || TeamSmallerEqThanTeam(3, t, pl))
 +      }
 +      if ((teambits & BIT(2)) != 0)
 +      {
                RandomSelection_AddFloat(3, 1, 1);
 -      if(t == 4 || TeamSmallerEqThanTeam(4, t, pl))
 +      }
 +      if ((teambits & BIT(3)) != 0)
 +      {
                RandomSelection_AddFloat(4, 1, 1);
 -
 +      }
        return RandomSelection_chosen_float;
  }
  
  int JoinBestTeam(entity this, bool only_return_best, bool forcebestteam)
  {
 -      float smallest, selectedteam;
 -
        // don't join a team if we're not playing a team game
 -      if(!teamplay)
 +      if (!teamplay)
 +      {
                return 0;
 +      }
  
        // find out what teams are available
        CheckAllowedTeams(this);
  
 +      float selectedteam;
 +
        // if we don't care what team he ends up on, put him on whatever team he entered as.
        // if he's not on a valid team, then let other code put him on the smallest team
 -      if(!forcebestteam)
 +      if (!forcebestteam)
        {
                if(     c1 >= 0 && this.team == NUM_TEAM_1)
                        selectedteam = this.team;
                else
                        selectedteam = -1;
  
 -              if(selectedteam > 0)
 +              if (selectedteam > 0)
                {
 -                      if(!only_return_best)
 +                      if (!only_return_best)
                        {
 -                              SetPlayerColors(this, selectedteam - 1);
 +                              SetPlayerTeamSimple(this, selectedteam);
  
                                // when JoinBestTeam is called by client.qc/ClientKill_Now_TeamChange the players team is -1 and thus skipped
                                // when JoinBestTeam is called by client.qc/ClientConnect the player_id is 0 the log attempt is rejected
                // otherwise end up on the smallest team (handled below)
        }
  
 -      smallest = FindSmallestTeam(this, true);
 -
 -      if(!only_return_best && !this.bot_forced_team)
 +      float bestteam = FindSmallestTeam(this, true);
 +      if (only_return_best || this.bot_forced_team)
        {
 -              TeamchangeFrags(this);
 -              if(smallest == 1)
 -              {
 -                      SetPlayerColors(this, NUM_TEAM_1 - 1);
 -              }
 -              else if(smallest == 2)
 -              {
 -                      SetPlayerColors(this, NUM_TEAM_2 - 1);
 -              }
 -              else if(smallest == 3)
 -              {
 -                      SetPlayerColors(this, NUM_TEAM_3 - 1);
 -              }
 -              else if(smallest == 4)
 -              {
 -                      SetPlayerColors(this, NUM_TEAM_4 - 1);
 -              }
 -              else
 -              {
 -                      error("smallest team: invalid team\n");
 -              }
 -
 -              LogTeamchange(this.playerid, this.team, 2); // log auto join
 -
 -              if(!IS_DEAD(this))
 -                      Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
 +              return bestteam;
        }
 -
 -      return smallest;
 +      bestteam = Team_NumberToTeam(bestteam);
 +      if (bestteam == -1)
 +      {
 +              error("JoinBestTeam: invalid team\n");
 +      }
 +      int oldteam = Team_TeamToNumber(this.team);
 +      TeamchangeFrags(this);
 +      SetPlayerTeamSimple(this, bestteam);
 +      LogTeamchange(this.playerid, this.team, 2); // log auto join
 +      if (!IS_BOT_CLIENT(this))
 +      {
 +              AutoBalanceBots(oldteam, Team_TeamToNumber(bestteam));
 +      }
 +      if (!IS_DEAD(this) && (MUTATOR_CALLHOOK(Player_ChangeTeamKill, this) ==
 +              false))
 +      {
 +              Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
 +      }
 +      return bestteam;
  }
  
 -//void() ctf_playerchanged;
  void SV_ChangeTeam(entity this, float _color)
  {
 -      float scolor, dcolor, steam, dteam; //, dbotcount, scount, dcount;
 +      float sourcecolor, destinationcolor, sourceteam, destinationteam;
  
        // in normal deathmatch we can just apply the color and we're done
        if(!teamplay)
        if(!teamplay)
                return;
  
 -      scolor = this.clientcolors & 0x0F;
 -      dcolor = _color & 0x0F;
 -
 -      if(scolor == NUM_TEAM_1 - 1)
 -              steam = 1;
 -      else if(scolor == NUM_TEAM_2 - 1)
 -              steam = 2;
 -      else if(scolor == NUM_TEAM_3 - 1)
 -              steam = 3;
 -      else // if(scolor == NUM_TEAM_4 - 1)
 -              steam = 4;
 -      if(dcolor == NUM_TEAM_1 - 1)
 -              dteam = 1;
 -      else if(dcolor == NUM_TEAM_2 - 1)
 -              dteam = 2;
 -      else if(dcolor == NUM_TEAM_3 - 1)
 -              dteam = 3;
 -      else // if(dcolor == NUM_TEAM_4 - 1)
 -              dteam = 4;
 +      sourcecolor = this.clientcolors & 0x0F;
 +      destinationcolor = _color & 0x0F;
  
 +      sourceteam = Team_TeamToNumber(sourcecolor + 1);
 +      destinationteam = Team_TeamToNumber(destinationcolor + 1);
 +      
        CheckAllowedTeams(this);
  
 -      if(dteam == 1 && c1 < 0) dteam = 4;
 -      if(dteam == 4 && c4 < 0) dteam = 3;
 -      if(dteam == 3 && c3 < 0) dteam = 2;
 -      if(dteam == 2 && c2 < 0) dteam = 1;
 +      if (destinationteam == 1 && c1 < 0) destinationteam = 4;
 +      if (destinationteam == 4 && c4 < 0) destinationteam = 3;
 +      if (destinationteam == 3 && c3 < 0) destinationteam = 2;
 +      if (destinationteam == 2 && c2 < 0) destinationteam = 1;
  
        // not changing teams
 -      if(scolor == dcolor)
 +      if (sourcecolor == destinationcolor)
        {
 -              //bprint("same team change\n");
 -              SetPlayerTeam(this, dteam, steam, true);
 +              SetPlayerTeam(this, destinationteam, sourceteam, true);
                return;
        }
  
-       if (autocvar_g_campaign || (autocvar_g_changeteam_banned && this.wasplayer))
-       {
+       if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) {
                Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
                return; // changing teams is not allowed
        }
  
        // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
 -      if(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
 +      if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
        {
                GetTeamCounts(this);
 -              if(!TeamSmallerEqThanTeam(dteam, steam, this))
 +              if ((BIT(destinationteam - 1) & FindBestTeams(this, false)) == 0)
                {
                        Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
                        return;
                }
        }
 -
 -//    bprint("allow change teams from ", ftos(steam), " to ", ftos(dteam), "\n");
 -
 -      if(IS_PLAYER(this) && steam != dteam)
 +      if(IS_PLAYER(this) && sourceteam != destinationteam)
        {
                // reduce frags during a team change
                TeamchangeFrags(this);
        }
 +      SetPlayerTeam(this, destinationteam, sourceteam, !IS_CLIENT(this));
 +      AutoBalanceBots(sourceteam, destinationteam);
 +      if (!IS_PLAYER(this) || (sourceteam == destinationteam))
 +      {
 +              return;
 +      }
 +      // kill player when changing teams
 +      if (IS_DEAD(this) || (MUTATOR_CALLHOOK(Player_ChangeTeamKill, this) == true))
 +      {
 +              return;
 +      }
 +      Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
 +}
  
 -      MUTATOR_CALLHOOK(Player_ChangeTeam, this, steam, dteam);
 -
 -      SetPlayerTeam(this, dteam, steam, !IS_CLIENT(this));
 -
 -      if(IS_PLAYER(this) && steam != dteam)
 +void AutoBalanceBots(int sourceteam, int destinationteam)
 +{
 +      if ((sourceteam == -1) || (destinationteam == -1))
 +      {
 +              return;
 +      }
 +      if (!autocvar_g_balance_teams ||
 +              !autocvar_g_balance_teams_prevent_imbalance)
 +      {
 +              return;
 +      }
 +      int numplayerssourceteam = 0;
 +      int numplayersdestinationteam = 0;
 +      entity lowestbotdestinationteam = NULL;
 +      switch (sourceteam)
 +      {
 +              case 1:
 +              {
 +                      numplayerssourceteam = c1;
 +                      break;
 +              }
 +              case 2:
 +              {
 +                      numplayerssourceteam = c2;
 +                      break;
 +              }
 +              case 3:
 +              {
 +                      numplayerssourceteam = c3;
 +                      break;
 +              }
 +              case 4:
 +              {
 +                      numplayerssourceteam = c4;
 +                      break;
 +              }
 +      }
 +      switch (destinationteam)
 +      {
 +              case 1:
 +              {
 +                      numplayersdestinationteam = c1;
 +                      lowestbotdestinationteam = lowestbotteam1;
 +                      break;
 +              }
 +              case 2:
 +              {
 +                      numplayersdestinationteam = c2;
 +                      lowestbotdestinationteam = lowestbotteam2;
 +                      break;
 +              }
 +              case 3:
 +              {
 +                      numplayersdestinationteam = c3;
 +                      lowestbotdestinationteam = lowestbotteam3;
 +                      break;
 +              }
 +              case 4:
 +              {
 +                      numplayersdestinationteam = c4;
 +                      lowestbotdestinationteam = lowestbotteam4;
 +                      break;
 +              }
 +      }
 +      if ((numplayersdestinationteam <= numplayerssourceteam) ||
 +              (lowestbotdestinationteam == NULL))
        {
 -              // kill player when changing teams
 -              if(!IS_DEAD(this))
 -                      Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
 +              return;
 +      }
 +      SetPlayerTeamSimple(lowestbotdestinationteam, Team_NumberToTeam(sourceteam));
 +      if (IS_DEAD(lowestbotdestinationteam) || (MUTATOR_CALLHOOK(
 +              Player_ChangeTeamKill, lowestbotdestinationteam) == true))
 +      {
 +              return;
        }
 +      Damage(lowestbotdestinationteam, lowestbotdestinationteam,
 +              lowestbotdestinationteam, 100000, DEATH_TEAMCHANGE.m_id,
 +              lowestbotdestinationteam.origin, '0 0 0');
  }
  
  void ShufflePlayerOutOfTeam (float source_team)
        TeamchangeFrags(selected);
        SetPlayerTeam(selected, smallestteam, source_team, false);
  
 -      if(!IS_DEAD(selected))
 -              Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE.m_id, selected.origin, '0 0 0');
 +      if (IS_DEAD(selected) || MUTATOR_CALLHOOK(Player_ChangeTeamKill, selected) == true)
 +      {
 +              return;
 +      }
 +      Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE.m_id, selected.origin, '0 0 0');
        Send_Notification(NOTIF_ONE, selected, MSG_CENTER, CENTER_DEATH_SELF_AUTOTEAMCHANGE, selected.team);
  }