]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into Lyberta/TeamplayOverhaul
authorLyberta <lyberta@lyberta.net>
Wed, 6 Jun 2018 10:54:50 +0000 (13:54 +0300)
committerLyberta <lyberta@lyberta.net>
Wed, 6 Jun 2018 10:54:50 +0000 (13:54 +0300)
1  2 
qcsrc/common/gamemodes/gamemode/nexball/nexball.qc
qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc
qcsrc/common/t_items.qc
qcsrc/server/client.qc
qcsrc/server/command/cmd.qc
qcsrc/server/defs.qh
qcsrc/server/g_world.qc
qcsrc/server/mutators/events.qh
qcsrc/server/mutators/mutator/gamemode_ca.qc
qcsrc/server/player.qc

index f208c151212f669c9ba8d21b128c9274dbd4d2bc,426b341a63a077b1316305a435d0975241b490ea..82e5ed8cf16f683a219912910d873f86426a57f2
@@@ -195,9 -195,9 +195,9 @@@ void GiveBall(entity plyr, entity ball
                ball.nextthink = time + autocvar_g_nexball_basketball_delay_hold;
        }
  
-       plyr.(weaponentity).weapons = plyr.weapons;
+       STAT(WEAPONS, plyr.(weaponentity)) = STAT(WEAPONS, plyr);
        plyr.m_switchweapon = plyr.(weaponentity).m_weapon;
-       plyr.weapons = WEPSET(NEXBALL);
+       STAT(WEAPONS, plyr) = WEPSET(NEXBALL);
        Weapon w = WEP_NEXBALL;
        w.wr_resetplayer(w, plyr);
        plyr.(weaponentity).m_switchweapon = WEP_NEXBALL;
@@@ -635,7 -635,7 +635,7 @@@ void SpawnGoal(entity this
  
        EXACTTRIGGER_INIT;
  
 -      if(this.team != GOAL_OUT && Team_TeamToNumber(this.team) != -1)
 +      if(this.team != GOAL_OUT && Team_IsValidTeam(this.team))
        {
                entity wp = WaypointSprite_SpawnFixed(WP_NbGoal, (this.absmin + this.absmax) * 0.5, this, sprite, RADARICON_NONE);
                wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 0.5 0');
@@@ -828,15 -828,15 +828,15 @@@ MUTATOR_HOOKFUNCTION(nb, PlayerPreThink
                        {
                                .entity weaponentity = weaponentities[slot];
  
-                               if(player.(weaponentity).weapons)
+                               if(STAT(WEAPONS, player.(weaponentity)))
                                {
-                                       player.weapons = player.(weaponentity).weapons;
+                                       STAT(WEAPONS, player) = STAT(WEAPONS, player.(weaponentity));
                                        Weapon w = WEP_NEXBALL;
                                        w.wr_resetplayer(w, player);
                                        player.(weaponentity).m_switchweapon = player.m_switchweapon;
                                        W_SwitchWeapon(player, player.(weaponentity).m_switchweapon, weaponentity);
  
-                                       player.(weaponentity).weapons = '0 0 0';
+                                       STAT(WEAPONS, player.(weaponentity)) = '0 0 0';
                                }
                        }
                }
@@@ -862,13 -862,13 +862,13 @@@ MUTATOR_HOOKFUNCTION(nb, PlayerSpawn
        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
                .entity weaponentity = weaponentities[slot];
-               player.(weaponentity).weapons = '0 0 0';
+               STAT(WEAPONS, player.(weaponentity)) = '0 0 0';
        }
  
        if (nexball_mode & NBM_BASKETBALL)
-               player.weapons |= WEPSET(NEXBALL);
+               STAT(WEAPONS, player) |= WEPSET(NEXBALL);
        else
-               player.weapons = '0 0 0';
+               STAT(WEAPONS, player) = '0 0 0';
  
        return false;
  }
@@@ -920,7 -920,7 +920,7 @@@ MUTATOR_HOOKFUNCTION(nb, ItemTouch
        return MUT_ITEMTOUCH_CONTINUE;
  }
  
 -MUTATOR_HOOKFUNCTION(nb, CheckAllowedTeams)
 +MUTATOR_HOOKFUNCTION(nb, TeamBalance_CheckAllowedTeams)
  {
        M_ARGV(1, string) = "nexball_team";
        return true;
index 06fb06d185e5786d3a5ebd3205293be5a7bbdfe3,c4f4d32c4262f282d49a12872d9f23a7970ae313..61309cf62418d9958f6b2b8874b6e75809f04258
@@@ -1075,52 -1075,46 +1075,52 @@@ int total_generators
  void Onslaught_count_generators()
  {
        entity e;
 -      total_generators = redowned = blueowned = yellowowned = pinkowned = 0;
 +      total_generators = 0;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              Team_SetNumberOfControlPoints(Team_GetTeamFromIndex(i), 0);
 +      }
        for(e = ons_worldgeneratorlist; e; e = e.ons_worldgeneratornext)
        {
                ++total_generators;
 -              redowned += (e.team == NUM_TEAM_1 && e.health > 0);
 -              blueowned += (e.team == NUM_TEAM_2 && e.health > 0);
 -              yellowowned += (e.team == NUM_TEAM_3 && e.health > 0);
 -              pinkowned += (e.team == NUM_TEAM_4 && e.health > 0);
 +              if (GetResourceAmount(e, RESOURCE_HEALTH) < 1)
 +              {
 +                      continue;
 +              }
 +              entity team_ = Entity_GetTeam(e);
 +              int num_control_points = Team_GetNumberOfControlPoints(team_);
 +              ++num_control_points;
 +              Team_SetNumberOfControlPoints(team_, num_control_points);
        }
  }
  
  int Onslaught_GetWinnerTeam()
  {
        int winner_team = 0;
 -      if(redowned > 0)
 -              winner_team = NUM_TEAM_1;
 -      if(blueowned > 0)
 +      if (Team_GetNumberOfControlPoints(Team_GetTeamFromIndex(1)) >= 1)
        {
 -              if(winner_team) return 0;
 -              winner_team = NUM_TEAM_2;
 +              winner_team = NUM_TEAM_1;
        }
 -      if(yellowowned > 0)
 +      for (int i = 2; i <= NUM_TEAMS; ++i)
        {
 -              if(winner_team) return 0;
 -              winner_team = NUM_TEAM_3;
 +              if (Team_GetNumberOfControlPoints(Team_GetTeamFromIndex(i)) >= 1)
 +              {
 +                      if (winner_team != 0)
 +                      {
 +                              return 0;
 +                      }
 +                      winner_team = Team_IndexToTeam(i);
 +              }
        }
 -      if(pinkowned > 0)
 +      if (winner_team)
        {
 -              if(winner_team) return 0;
 -              winner_team = NUM_TEAM_4;
 -      }
 -      if(winner_team)
                return winner_team;
 +      }
        return -1; // no generators left?
  }
  
  void nades_Clear(entity e);
  
 -#define ONS_OWNED_GENERATORS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
 -#define ONS_OWNED_GENERATORS_OK() (ONS_OWNED_GENERATORS() > 1)
  bool Onslaught_CheckWinner()
  {
        if ((autocvar_timelimit && time > game_starttime + autocvar_timelimit * 60) || (round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0))
  
        Onslaught_count_generators();
  
 -      if(ONS_OWNED_GENERATORS_OK())
 +      if (Team_GetNumberOfTeamsWithControlPoints() > 1)
 +      {
                return 0;
 +      }
  
        int winner_team = Onslaught_GetWinnerTeam();
  
@@@ -1237,7 -1229,7 +1237,7 @@@ void havocbot_goalrating_ons_offenseite
        // Needs weapons?
        int c = 0;
        FOREACH(Weapons, it != WEP_Null, {
-               if(this.weapons & (it.m_wepset))
+               if(STAT(WEAPONS, this) & (it.m_wepset))
                if(++c >= 4)
                        break;
        });
        {
                // gather health and armor only
                if (it.solid)
-               if ( ((it.health || it.armorvalue) && needarmor) || (it.weapons && needweapons ) )
+               if ( ((it.health || it.armorvalue) && needarmor) || (STAT(WEAPONS, it) && needweapons ) )
                if (vdist(it.origin - org, <, sradius))
                {
                        int t = it.bot_pickupevalfunc(this, it);
@@@ -1930,14 -1922,17 +1930,14 @@@ MUTATOR_HOOKFUNCTION(ons, HavocBot_Choo
        return true;
  }
  
 -MUTATOR_HOOKFUNCTION(ons, CheckAllowedTeams)
 +MUTATOR_HOOKFUNCTION(ons, TeamBalance_CheckAllowedTeams)
  {
        // onslaught is special
        for(entity tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
        {
 -              switch(tmp_entity.team)
 +              if (Team_IsValidTeam(tmp_entity.team))
                {
 -                      case NUM_TEAM_1: c1 = 0; break;
 -                      case NUM_TEAM_2: c2 = 0; break;
 -                      case NUM_TEAM_3: c3 = 0; break;
 -                      case NUM_TEAM_4: c4 = 0; break;
 +                      M_ARGV(0, float) |= Team_TeamToBit(tmp_entity.team);
                }
        }
  
@@@ -2167,9 -2162,12 +2167,9 @@@ spawnfunc(onslaught_generator
  // scoreboard setup
  void ons_ScoreRules()
  {
 -      CheckAllowedTeams(NULL);
 -      int teams = 0;
 -      if(c1 >= 0) teams |= BIT(0);
 -      if(c2 >= 0) teams |= BIT(1);
 -      if(c3 >= 0) teams |= BIT(2);
 -      if(c4 >= 0) teams |= BIT(3);
 +      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +      int teams = TeamBalance_GetAllowedTeams(balance);
 +      TeamBalance_Destroy(balance);
        GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
            field_team(ST_ONS_CAPS, "destroyed", SFL_SORT_PRIO_PRIMARY);
            field(SP_ONS_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
diff --combined qcsrc/common/t_items.qc
index 2f6b2151fe5829c8ee00f7bd92f7b4cdbae93e38,8be48b53001a313cfb40ad288bb3401430df40b8..8940eaaf736e527160c0071f243f7a0ada36ee8e
@@@ -50,13 -50,8 +50,8 @@@ void Item_SetAlpha(entity this
        }
        else
        {
-               if (autocvar_cl_ghost_items_color)
-               {
-                       this.alpha = autocvar_cl_ghost_items;
-                       this.colormod = this.glowmod = autocvar_cl_ghost_items_color;
-               }
-               else
-                       this.alpha = -1;
+               this.alpha = autocvar_cl_ghost_items;
+               this.colormod = this.glowmod = autocvar_cl_ghost_items_color;
        }
  
        if((!veh_hud) && (this.ItemStatus & ITS_STAYWEP))
@@@ -401,39 -396,12 +396,12 @@@ bool have_pickup_item(entity this
                if(autocvar_g_pickup_items == 0)
                        return false;
                if(g_weaponarena)
-                       if(this.weapons || this.itemdef.instanceOfAmmo) // no item or ammo pickups in weaponarena
+                       if(STAT(WEAPONS, this) || this.itemdef.instanceOfAmmo) // no item or ammo pickups in weaponarena
                                return false;
        }
        return true;
  }
  
- /*
- float Item_Customize()
- {
-       if(this.spawnshieldtime)
-               return true;
-       if(this.weapons & ~other.weapons)
-       {
-               this.colormod = '0 0 0';
-               this.glowmod = this.colormod;
-               this.alpha = 0.5 + 0.5 * g_ghost_items; // halfway more alpha
-               return true;
-       }
-       else
-       {
-               if(g_ghost_items)
-               {
-                       this.colormod = stov(autocvar_g_ghost_items_color);
-                       this.glowmod = this.colormod;
-                       this.alpha = g_ghost_items;
-                       return true;
-               }
-               else
-                       return false;
-       }
- }
- */
  void Item_Show (entity e, float mode)
  {
        e.effects &= ~(EF_ADDITIVE | EF_STARDUST | EF_FULLBRIGHT | EF_NODEPTHTEST);
        }
        else
        {
-               bool nostay = def.instanceOfWeaponPickup ? !!(def.m_weapon.weapons & WEPSET_SUPERWEAPONS) : false // no weapon-stay on superweapons
+               bool nostay = def.instanceOfWeaponPickup ? !!(def.m_weapon.m_wepset & WEPSET_SUPERWEAPONS) : false // no weapon-stay on superweapons
                        || e.team // weapon stay isn't supported for teamed weapons
                        ;
                if(def.instanceOfWeaponPickup && !nostay && g_weapon_stay)
@@@ -517,7 -485,7 +485,7 @@@ void Item_Respawn (entity this
        sound(this, CH_TRIGGER, this.itemdef.m_respawnsound, VOL_BASE, ATTEN_NORM);     // play respawn sound
        setorigin(this, this.origin);
  
-     if (Item_ItemsTime_Allow(this.itemdef) || (this.weapons & WEPSET_SUPERWEAPONS))
+     if (Item_ItemsTime_Allow(this.itemdef) || (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
        {
                float t = Item_ItemsTime_UpdateTime(this, 0);
                Item_ItemsTime_SetTime(this, t);
@@@ -602,13 -570,13 +570,13 @@@ void Item_RespawnThink(entity this
  void Item_ScheduleRespawnIn(entity e, float t)
  {
        // if the respawn time is longer than 10 seconds, show a waypoint, otherwise, just respawn normally
-       if ((Item_ItemsTime_Allow(e.itemdef) || (e.weapons & WEPSET_SUPERWEAPONS) || MUTATOR_CALLHOOK(Item_ScheduleRespawn, e, t)) && (t - ITEM_RESPAWN_TICKS) > 0)
+       if ((Item_ItemsTime_Allow(e.itemdef) || (STAT(WEAPONS, e) & WEPSET_SUPERWEAPONS) || MUTATOR_CALLHOOK(Item_ScheduleRespawn, e, t)) && (t - ITEM_RESPAWN_TICKS) > 0)
        {
                setthink(e, Item_RespawnCountdown);
                e.nextthink = time + max(0, t - ITEM_RESPAWN_TICKS);
                e.scheduledrespawntime = e.nextthink + ITEM_RESPAWN_TICKS;
                e.item_respawncounter = 0;
-               if(Item_ItemsTime_Allow(e.itemdef) || (e.weapons & WEPSET_SUPERWEAPONS))
+               if(Item_ItemsTime_Allow(e.itemdef) || (STAT(WEAPONS, e) & WEPSET_SUPERWEAPONS))
                {
                        t = Item_ItemsTime_UpdateTime(e, e.scheduledrespawntime);
                        Item_ItemsTime_SetTime(e, t);
                e.scheduledrespawntime = time + t;
                e.wait = time + t;
  
-               if(Item_ItemsTime_Allow(e.itemdef) || (e.weapons & WEPSET_SUPERWEAPONS))
+               if(Item_ItemsTime_Allow(e.itemdef) || (STAT(WEAPONS, e) & WEPSET_SUPERWEAPONS))
                {
                        t = Item_ItemsTime_UpdateTime(e, e.scheduledrespawntime);
                        Item_ItemsTime_SetTime(e, t);
@@@ -645,18 -613,14 +613,18 @@@ float adjust_respawntime(float normal_r
                return normal_respawntime;
        }
  
 -      CheckAllowedTeams(NULL);
 -      GetTeamCounts(NULL);
 +      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +      TeamBalance_GetTeamCounts(balance, NULL);
        int players = 0;
 -      if (c1 != -1) players += c1;
 -      if (c2 != -1) players += c2;
 -      if (c3 != -1) players += c3;
 -      if (c4 != -1) players += c4;
 -
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              if (TeamBalance_IsTeamAllowed(balance, i))
 +              {
 +                      players += TeamBalance_GetNumberOfPlayers(balance, i);
 +              }
 +      }
 +      TeamBalance_Destroy(balance);
 +      
        if (players >= 2) {
                return normal_respawntime * (r / (players + o) + l);
        } else {
@@@ -737,7 -701,7 +705,7 @@@ void GiveRandomWeapons(entity receiver
                        FOREACH(Weapons, it != WEP_Null,
                        {
                                // Finding a weapon which player doesn't have.
-                               if (!(receiver.weapons & it.m_wepset) && (it.netname == weapon))
+                               if (!(STAT(WEAPONS, receiver) & it.m_wepset) && (it.netname == weapon))
                                {
                                        RandomSelection_AddEnt(it, 1, 1);
                                        break;
                {
                        return;
                }
-               receiver.weapons |= RandomSelection_chosen_ent.m_wepset;
+               STAT(WEAPONS, receiver) |= RandomSelection_chosen_ent.m_wepset;
                if (RandomSelection_chosen_ent.ammo_type == RESOURCE_NONE)
                {
                        continue;
@@@ -810,7 -774,7 +778,7 @@@ float Item_GiveTo(entity item, entity p
                                if(player.(weaponentity).m_switchweapon == w_getbestweapon(player, weaponentity))
                                        _switchweapon |= BIT(slot);
  
-                               if(!(player.weapons & WepSet_FromWeapon(player.(weaponentity).m_switchweapon)))
+                               if(!(STAT(WEAPONS, player) & WepSet_FromWeapon(player.(weaponentity).m_switchweapon)))
                                        _switchweapon |= BIT(slot);
                        }
                }
        if (item.itemdef.instanceOfWeaponPickup)
        {
                WepSet w;
-               w = item.weapons;
-               w &= ~player.weapons;
+               w = STAT(WEAPONS, item);
+               w &= ~STAT(WEAPONS, player);
  
                if (w || (item.spawnshieldtime && item.pickup_anyway > 0))
                {
@@@ -1017,7 -981,7 +985,7 @@@ void Item_Reset(entity this
        {
                WaypointSprite_Kill(this.waypointsprite_attached);
        }
-       if (this.itemdef.instanceOfPowerup || (this.weapons & WEPSET_SUPERWEAPONS)) // do not spawn powerups initially!
+       if (this.itemdef.instanceOfPowerup || (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)) // do not spawn powerups initially!
        {
                Item_ScheduleInitialRespawn(this);
        }
@@@ -1076,7 -1040,7 +1044,7 @@@ float generic_pickupevalfunc(entity pla
  float weapon_pickupevalfunc(entity player, entity item)
  {
        // See if I have it already
-       if(player.weapons & item.weapons)
+       if(STAT(WEAPONS, player) & STAT(WEAPONS, item))
        {
                // If I can pick it up
                if(!item.spawnshieldtime)
        // reduce weapon value if bot already got a good arsenal
        float c = 1;
        int weapons_value = 0;
-       FOREACH(Weapons, it != WEP_Null && (player.weapons & it.m_wepset), {
+       FOREACH(Weapons, it != WEP_Null && (STAT(WEAPONS, player) & it.m_wepset), {
                weapons_value += it.bot_pickupbasevalue;
        });
        c -= bound(0, weapons_value / 20000, 1) * 0.5;
@@@ -1121,7 -1085,7 +1089,7 @@@ float ammo_pickupevalfunc(entity player
        else
        {
                FOREACH(Weapons, it != WEP_Null, {
-                       if(!(player.weapons & (it.m_wepset)))
+                       if(!(STAT(WEAPONS, player) & (it.m_wepset)))
                                continue;
  
                        switch(it.ammo_type)
@@@ -1237,7 -1201,7 +1205,7 @@@ void _StartItem(entity this, entity def
        }
  
        if(weaponid)
-               this.weapons = WepSet_FromWeapon(Weapons_from(weaponid));
+               STAT(WEAPONS, this) = WepSet_FromWeapon(Weapons_from(weaponid));
  
        this.flags = FL_ITEM | itemflags;
        IL_PUSH(g_items, this);
@@@ -1546,7 -1510,7 +1514,7 @@@ spawnfunc(target_items
                                        s = W_UndeprecateName(argv(j));
                                        if(s == it.netname)
                                        {
-                                               this.weapons |= (it.m_wepset);
+                                               STAT(WEAPONS, this) |= (it.m_wepset);
                                                if(this.spawnflags == 0 || this.spawnflags == 2)
                                                        it.wr_init(it);
                                                break;
                if(this.health != 0) this.netname = sprintf("%s %s%d %s", this.netname, valueprefix, max(0, this.health), "health");
                if(this.armorvalue != 0) this.netname = sprintf("%s %s%d %s", this.netname, valueprefix, max(0, this.armorvalue), "armor");
                FOREACH(Buffs, it != BUFF_Null, this.netname = sprintf("%s %s%d %s", this.netname, itemprefix, !!(STAT(BUFFS, this) & (it.m_itemid)), it.m_name));
-               FOREACH(Weapons, it != WEP_Null, this.netname = sprintf("%s %s%d %s", this.netname, itemprefix, !!(this.weapons & (it.m_wepset)), it.netname));
+               FOREACH(Weapons, it != WEP_Null, this.netname = sprintf("%s %s%d %s", this.netname, itemprefix, !!(STAT(WEAPONS, this) & (it.m_wepset)), it.netname));
        }
        this.netname = strzone(this.netname);
        //print(this.netname, "\n");
@@@ -1618,30 -1582,30 +1586,30 @@@ float GiveWeapon(entity e, float wpn, f
  {
        WepSet v0, v1;
        WepSet s = WepSet_FromWeapon(Weapons_from(wpn));
-       v0 = (e.weapons & s);
+       v0 = (STAT(WEAPONS, e) & s);
        switch(op)
        {
                case OP_SET:
                        if(val > 0)
-                               e.weapons |= s;
+                               STAT(WEAPONS, e) |= s;
                        else
-                               e.weapons &= ~s;
+                               STAT(WEAPONS, e) &= ~s;
                        break;
                case OP_MIN:
                case OP_PLUS:
                        if(val > 0)
-                               e.weapons |= s;
+                               STAT(WEAPONS, e) |= s;
                        break;
                case OP_MAX:
                        if(val <= 0)
-                               e.weapons &= ~s;
+                               STAT(WEAPONS, e) &= ~s;
                        break;
                case OP_MINUS:
                        if(val > 0)
-                               e.weapons &= ~s;
+                               STAT(WEAPONS, e) &= ~s;
                        break;
        }
-       v1 = (e.weapons & s);
+       v1 = (STAT(WEAPONS, e) & s);
        return (v0 != v1);
  }
  
@@@ -1859,7 -1823,7 +1827,7 @@@ float GiveItems(entity e, float beginar
        FOREACH(Weapons, it != WEP_Null, {
                POSTGIVE_WEAPON(e, it, SND_WEAPONPICKUP, SND_Null);
                if(!(save_weapons & (it.m_wepset)))
-                       if(e.weapons & (it.m_wepset))
+                       if(STAT(WEAPONS, e) & (it.m_wepset))
                                it.wr_init(it);
        });
        POSTGIVE_VALUE(e, strength_finished, 1, SND_POWERUP, SND_POWEROFF);
        POSTGIVE_VALUE_ROT(e, health, 1, pauserothealth_finished, autocvar_g_balance_pause_health_rot, pauseregen_finished, autocvar_g_balance_pause_health_regen, SND_MEGAHEALTH, SND_Null);
  
        if(e.superweapons_finished <= 0)
-               if(e.weapons & WEPSET_SUPERWEAPONS)
+               if(STAT(WEAPONS, e) & WEPSET_SUPERWEAPONS)
                        e.superweapons_finished = autocvar_g_balance_superweapons_time;
  
        if(e.strength_finished <= 0)
        {
                .entity weaponentity = weaponentities[slot];
                if(e.(weaponentity).m_weapon != WEP_Null || slot == 0)
-               if(!(e.weapons & WepSet_FromWeapon(e.(weaponentity).m_switchweapon)))
+               if(!(STAT(WEAPONS, e) & WepSet_FromWeapon(e.(weaponentity).m_switchweapon)))
                        _switchweapon |= BIT(slot);
        }
  
diff --combined qcsrc/server/client.qc
index 097a9ad358329dbe138e33c4224e6290cf0c566e,bd71d982d0402085f96235874e79e2fc8bb46b97..2f50f6df4ea2e59a14d9a211cc2ba6e3bcea1fbd
@@@ -49,6 -49,7 +49,7 @@@
  #include "../common/items/_mod.qh"
  
  #include "../common/mutators/mutator/waypoints/all.qh"
+ #include "../common/mutators/mutator/instagib/sv_instagib.qh"
  
  #include "../common/triggers/subs.qh"
  #include "../common/triggers/triggers.qh"
@@@ -62,6 -63,8 +63,8 @@@
  
  #include "../lib/warpzone/server.qh"
  
+ #include <common/mutators/mutator/overkill/oknex.qh>
  STATIC_METHOD(Client, Add, void(Client this, int _team))
  {
      ClientConnect(this);
@@@ -152,10 -155,15 +155,15 @@@ void ClientData_Detach(entity this
  
  void ClientData_Touch(entity e)
  {
-       CS(e).clientdata.SendFlags = 1;
+       entity cd = CS(e).clientdata;
+       if (cd) { cd.SendFlags = 1; }
  
        // make it spectatable
-       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != e && IS_SPEC(it) && it.enemy == e, { CS(it).clientdata.SendFlags = 1; });
+       FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != e && IS_SPEC(it) && it.enemy == e,
+       {
+               entity cd = CS(it).clientdata;
+               if (cd) { cd.SendFlags = 1; }
+       });
  }
  
  void SetSpectatee(entity this, entity spectatee);
@@@ -282,8 -290,10 +290,8 @@@ void PutObserverInServer(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;
 +              Player_SetTeamIndex(this, -1);
 +              this.frags = FRAGS_SPECTATOR;
          PlayerScore_Clear(this);  // clear scores when needed
      }
  
                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS);
  
                if(!CS(this).just_joined)
 -                      LogTeamchange(this.playerid, -1, 4);
 +                      LogTeamchange(this.playerid, -1, TEAM_CHANGE_SPECTATOR);
                else
                        CS(this).just_joined = false;
        }
        this.revival_time = 0;
  
        this.items = 0;
-       this.weapons = '0 0 0';
+       STAT(WEAPONS, this) = '0 0 0';
        this.drawonlytoclient = this;
  
        this.viewloc = NULL;
@@@ -514,7 -524,7 +522,7 @@@ void PutPlayerInServer(entity this
        accuracy_resend(this);
  
        if (this.team < 0)
 -              JoinBestTeam(this, true);
 +              TeamBalance_JoinBestTeam(this, true);
  
        entity spot = SelectSpawnPoint(this, false);
        if (!spot) {
                this.ammo_fuel = warmup_start_ammo_fuel;
                this.health = warmup_start_health;
                this.armorvalue = warmup_start_armorvalue;
-               this.weapons = WARMUP_START_WEAPONS;
+               STAT(WEAPONS, this) = WARMUP_START_WEAPONS;
        } else {
                this.ammo_shells = start_ammo_shells;
                this.ammo_nails = start_ammo_nails;
                this.ammo_fuel = start_ammo_fuel;
                this.health = start_health;
                this.armorvalue = start_armorvalue;
-               this.weapons = start_weapons;
+               STAT(WEAPONS, this) = start_weapons;
                if (MUTATOR_CALLHOOK(ForbidRandomStartWeapons, this) == false)
                {
                        GiveRandomWeapons(this, random_start_weapons_count,
  
        PS(this).dual_weapons = '0 0 0';
  
-       this.superweapons_finished = (this.weapons & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
+       this.superweapons_finished = (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
  
        this.items = start_items;
  
@@@ -901,7 -911,7 +909,7 @@@ void ClientKill_Now_TeamChange(entity t
  {
        if(this.killindicator_teamchange == -1)
        {
 -              JoinBestTeam( this, true );
 +              TeamBalance_JoinBestTeam(this, true);
        }
        else if(this.killindicator_teamchange == -2)
        {
@@@ -1159,76 -1169,6 +1167,76 @@@ void ClientPreConnect(entity this
  }
  #endif
  
 +string GetClientVersionMessage(entity this)
 +{
 +      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 {
 +                      return strcat("This is Xonotic ", autocvar_g_xonoticversion,
 +                              "\n^3This server is using an outdated Xonotic version.\n\n\n ### THIS SERVER IS INCOMPATIBLE AND THUS YOU CANNOT JOIN ###.^8");
 +              }
 +      } else {
 +              return strcat("Welcome to Xonotic ", autocvar_g_xonoticversion);
 +      }
 +}
 +
 +string getwelcomemessage(entity this)
 +{
 +      MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
 +      string modifications = M_ARGV(0, string);
 +
 +      if(g_weaponarena)
 +      {
 +              if(g_weaponarena_random)
 +                      modifications = strcat(modifications, ", ", ftos(g_weaponarena_random), " of ", g_weaponarena_list, " Arena");
 +              else
 +                      modifications = strcat(modifications, ", ", g_weaponarena_list, " Arena");
 +      }
 +      else if(cvar("g_balance_blaster_weaponstartoverride") == 0)
 +              modifications = strcat(modifications, ", No start weapons");
 +      if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
 +              modifications = strcat(modifications, ", Low gravity");
 +      if(g_weapon_stay && !g_cts)
 +              modifications = strcat(modifications, ", Weapons stay");
 +      if(g_jetpack)
 +              modifications = strcat(modifications, ", Jet pack");
 +      if(autocvar_g_powerups == 0)
 +              modifications = strcat(modifications, ", No powerups");
 +      if(autocvar_g_powerups > 0)
 +              modifications = strcat(modifications, ", Powerups");
 +      modifications = substring(modifications, 2, strlen(modifications) - 2);
 +
 +      string versionmessage = GetClientVersionMessage(this);
 +      string s = strcat(versionmessage, "^8\n^8\nmatch type is ^1", gamemode_name, "^8\n");
 +
 +      if(modifications != "")
 +              s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
 +
 +      if(cache_lastmutatormsg != autocvar_g_mutatormsg)
 +      {
 +              strcpy(cache_lastmutatormsg, autocvar_g_mutatormsg);
 +              strcpy(cache_mutatormsg, cache_lastmutatormsg);
 +      }
 +
 +      if (cache_mutatormsg != "") {
 +              s = strcat(s, "\n\n^8special gameplay tips: ^7", cache_mutatormsg);
 +      }
 +
 +      string mutator_msg = "";
 +      MUTATOR_CALLHOOK(BuildGameplayTipsString, mutator_msg);
 +      mutator_msg = M_ARGV(0, string);
 +
 +      s = strcat(s, mutator_msg); // trust that the mutator will do proper formatting
 +
 +      string motd = autocvar_sv_motd;
 +      if (motd != "") {
 +              s = strcat(s, "\n\n^8MOTD: ^7", strreplace("\\n", "\n", motd));
 +      }
 +      return s;
 +}
 +
  /**
  =============
  ClientConnect
@@@ -1284,7 -1224,7 +1292,7 @@@ void ClientConnect(entity this
  
        int playerid_save = this.playerid;
        this.playerid = 0; // silent
 -      JoinBestTeam(this, false); // if the team number is valid, keep it
 +      TeamBalance_JoinBestTeam(this, false); // if the team number is valid, keep it
        this.playerid = playerid_save;
  
        if (autocvar_sv_spectate || autocvar_g_campaign || this.team_forced < 0) {
        if (autocvar_sv_eventlog)
                GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? this.netaddress : "bot"), ":", playername(this, false)));
  
 -      LogTeamchange(this.playerid, this.team, 1);
 +      LogTeamchange(this.playerid, this.team, TEAM_CHANGE_CONNECT);
  
        CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
  
        // notify about available teams
        if (teamplay)
        {
 -              CheckAllowedTeams(this);
 -              int t = 0;
 -              if (c1 >= 0) t |= BIT(0);
 -              if (c2 >= 0) t |= BIT(1);
 -              if (c3 >= 0) t |= BIT(2);
 -              if (c4 >= 0) t |= BIT(3);
 +              entity balance = TeamBalance_CheckAllowedTeams(this);
 +              int t = TeamBalance_GetAllowedTeams(balance);
 +              TeamBalance_Destroy(balance);
                stuffcmd(this, sprintf("set _teams_available %d\n", t));
        }
        else
@@@ -1561,7 -1504,7 +1569,7 @@@ void player_powerups(entity this
        Fire_ApplyDamage(this);
        Fire_ApplyEffect(this);
  
-       if (!autocvar_g_instagib)
+       if (!MUTATOR_IS_ENABLED(mutator_instagib))
        {
                if (this.items & ITEM_Strength.m_itemid)
                {
                }
                if (this.items & IT_SUPERWEAPON)
                {
-                       if (!(this.weapons & WEPSET_SUPERWEAPONS))
+                       if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
                        {
                                this.superweapons_finished = 0;
                                this.items = this.items - (this.items & IT_SUPERWEAPON);
                                if (time > this.superweapons_finished)
                                {
                                        this.items = this.items - (this.items & IT_SUPERWEAPON);
-                                       this.weapons &= ~WEPSET_SUPERWEAPONS;
+                                       STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
                                        //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_BROKEN, this.netname);
                                        Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_BROKEN);
                                }
                        }
                }
-               else if(this.weapons & WEPSET_SUPERWEAPONS)
+               else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
                {
                        if (time < this.superweapons_finished || (this.items & IT_UNLIMITED_SUPERWEAPONS))
                        {
                        else
                        {
                                this.superweapons_finished = 0;
-                               this.weapons &= ~WEPSET_SUPERWEAPONS;
+                               STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
                        }
                }
                else
@@@ -1844,7 -1787,7 +1852,7 @@@ void SpectateCopy(entity this, entity s
        this.invincible_finished = spectatee.invincible_finished;
        this.superweapons_finished = spectatee.superweapons_finished;
        STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
-       this.weapons = spectatee.weapons;
+       STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
        this.punchangle = spectatee.punchangle;
        this.view_ofs = spectatee.view_ofs;
        this.velocity = spectatee.velocity;
@@@ -2075,7 -2018,7 +2083,7 @@@ void Join(entity this
  
        if(!this.team_selected)
        if(autocvar_g_campaign || autocvar_g_balance_teams)
 -              JoinBestTeam(this, true);
 +              TeamBalance_JoinBestTeam(this, true);
  
        if(autocvar_g_campaign)
                campaign_bots_may_start = true;
  
        if(IS_PLAYER(this))
        if(teamplay && this.team != -1)
 -              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
 +      {
 +              //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
 +      }
        else
                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
        this.team_selected = false;
@@@ -2211,8 -2152,8 +2219,8 @@@ bool joinAllowed(entity this
        if (CS(this).version_mismatch) return false;
        if (!nJoinAllowed(this, this)) return false;
        if (teamplay && lockteams) return false;
-       if (ShowTeamSelection(this)) return false;
        if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
+       if (ShowTeamSelection(this)) return false;
        return true;
  }
  
@@@ -2362,6 -2303,7 +2370,7 @@@ bool PlayerThink(entity this
        return true;
  }
  
+ .bool would_spectate;
  void ObserverThink(entity this)
  {
        if ( CS(this).impulse )
                if (PHYS_INPUT_BUTTON_JUMP(this) && joinAllowed(this)) {
                        this.flags &= ~FL_JUMPRELEASED;
                        this.flags |= FL_SPAWNING;
-               } else if(PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) {
+               } else if(PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch || this.would_spectate) {
                        this.flags &= ~FL_JUMPRELEASED;
                        if(SpectateNext(this)) {
                                TRANSMUTE(Spectator, this);
@@@ -2434,12 -2376,19 +2443,19 @@@ void SpectatorThink(entity this
                        }
                        CS(this).impulse = 0;
                } else if (PHYS_INPUT_BUTTON_ATCK2(this)) {
+                       this.would_spectate = false;
                        this.flags &= ~FL_JUMPRELEASED;
                        TRANSMUTE(Observer, this);
                        PutClientInServer(this);
                } else {
                        if(!SpectateUpdate(this))
-                               PutObserverInServer(this);
+                       {
+                               if(!SpectateNext(this))
+                               {
+                                       PutObserverInServer(this);
+                                       this.would_spectate = true;
+                               }
+                       }
                }
        } else {
                if (!(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this))) {
index b6a821539c66226f72c4ad523f3a0287986e5c99,e0c72dbc2731493d3e7e22834918f5c7765c73b3..1ec7233ca40e47d96f2718734fd0a95c36818e39
@@@ -248,12 -248,14 +248,14 @@@ void ClientCommand_ready(entity caller
                                                if (caller.ready)            // toggle
                                                {
                                                        caller.ready = false;
-                                                       bprint(playername(caller, false), "^2 is ^1NOT^2 ready\n");
+                                                       if(IS_PLAYER(caller) || caller.caplayer == 1)
+                                                               bprint(playername(caller, false), "^2 is ^1NOT^2 ready\n");
                                                }
                                                else
                                                {
                                                        caller.ready = true;
-                                                       bprint(playername(caller, false), "^2 is ready\n");
+                                                       if(IS_PLAYER(caller) || caller.caplayer == 1)
+                                                               bprint(playername(caller, false), "^2 is ready\n");
                                                }
  
                                                // cannot reset the game while a timeout is active!
@@@ -394,16 -396,13 +396,16 @@@ void ClientCommand_selectteam(entity ca
                        if ((selection != -1) && autocvar_g_balance_teams &&
                                autocvar_g_balance_teams_prevent_imbalance)
                        {
 -                              CheckAllowedTeams(caller);
 -                              GetTeamCounts(caller);
 -                              if ((BIT(Team_TeamToNumber(selection) - 1) & FindBestTeams(caller, false)) == 0)
 +                              entity balance = TeamBalance_CheckAllowedTeams(caller);
 +                              TeamBalance_GetTeamCounts(balance, caller);
 +                              if ((Team_IndexToBit(Team_TeamToIndex(selection)) &
 +                                      TeamBalance_FindBestTeams(balance, caller, false)) == 0)
                                {
                                        Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
 +                                      TeamBalance_Destroy(balance);
                                        return;
                                }
 +                              TeamBalance_Destroy(balance);
                        }
                        ClientKill_TeamChange(caller, selection);
                        if (!IS_PLAYER(caller))
diff --combined qcsrc/server/defs.qh
index 9a01a79660e78dfd796e1fe6935ede8a8c9edf8b,5230bd8ea6ac1279c964cf61a8f052cedfa5806a..d699d03af247583775901248108ee2c0ec7c184a
@@@ -27,6 -27,8 +27,6 @@@ float bots_would_leave
  void UpdateFrags(entity player, int f);
  .float totalfrags;
  
 -float team1_score, team2_score, team3_score, team4_score;
 -
  // flag set on worldspawn so that the code knows if it is dedicated or not
  float server_is_dedicated;
  
@@@ -188,8 -190,6 +188,6 @@@ void FixClientCvars(entity e)
  // WEAPONTODO: remove this
  //WepSet weaponsInMap;
  
- #define weapons _STAT(WEAPONS)
  .float respawn_countdown; // next number to count
  
  float bot_waypoints_for_items;
@@@ -229,6 -229,8 +227,6 @@@ void Damage (entity targ, entity inflic
  // WEAPONTODO
  #define DMG_NOWEP (weaponentities[0])
  
 -float lockteams;
 -
  float sv_maxidle;
  float sv_maxidle_spectatorsareidle;
  int sv_maxidle_slots;
@@@ -319,6 -321,9 +317,9 @@@ float client_cefc_accumulatortime
  .float vortex_charge;
  .float vortex_charge_rottime;
  .float vortex_chargepool_ammo;
+ .float oknex_charge;
+ .float oknex_charge_rottime;
+ .float oknex_chargepool_ammo;
  .int hagar_load;
  
  .int grab; // 0 = can't grab, 1 = owner can grab, 2 = owner and team mates can grab, 3 = anyone can grab
diff --combined qcsrc/server/g_world.qc
index 312ffd44244fa914dc30cb94105d78b212ee7161,a613edc14c7861581e16833dc0278413caf96f68..7cbbb95d59fa20b848b0ee8a1cfa53d1db4433e1
@@@ -249,6 -249,7 +249,7 @@@ void cvar_changes_init(
                // these can contain player IDs, so better hide
                BADPREFIX("g_forced_team_");
                BADCVAR("sv_muteban_list");
+               BADCVAR("sv_voteban_list");
                BADCVAR("sv_allow_customplayermodels_idlist");
                BADCVAR("sv_allow_customplayermodels_speciallist");
  
@@@ -609,57 -610,6 +610,57 @@@ STATIC_INIT_EARLY(maxclients
        }
  }
  
 +void default_delayedinit(entity this)
 +{
 +      if(!scores_initialized)
 +              ScoreRules_generic();
 +}
 +
 +void InitGameplayMode()
 +{
 +      VoteReset();
 +
 +      // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
 +      get_mi_min_max(1);
 +      // assign reflectively to avoid "assignment to world" warning
 +      int done = 0; for (int i = 0, n = numentityfields(); i < n; ++i) {
 +          string k = entityfieldname(i); vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
 +          if (v) {
 +            putentityfieldstring(i, world, sprintf("%v", v));
 +            if (++done == 2) break;
 +        }
 +      }
 +      // currently, NetRadiant's limit is 131072 qu for each side
 +      // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
 +      // set the distance according to map size but don't go over the limit to avoid issues with float precision
 +      // in case somebody makes extremely large maps
 +      max_shot_distance = min(230000, vlen(world.maxs - world.mins));
 +
 +      MapInfo_LoadMapSettings(mapname);
 +      GameRules_teams(false);
 +
 +      if (!cvar_value_issafe(world.fog))
 +      {
 +              LOG_INFO("The current map contains a potentially harmful fog setting, ignored");
 +              world.fog = string_null;
 +      }
 +      if(MapInfo_Map_fog != "")
 +              if(MapInfo_Map_fog == "none")
 +                      world.fog = string_null;
 +              else
 +                      world.fog = strzone(MapInfo_Map_fog);
 +      clientstuff = strzone(MapInfo_Map_clientstuff);
 +
 +      MapInfo_ClearTemps();
 +
 +      gamemode_name = MapInfo_Type_ToText(MapInfo_LoadedGametype);
 +
 +      cache_mutatormsg = strzone("");
 +      cache_lastmutatormsg = strzone("");
 +
 +      InitializeEntity(NULL, default_delayedinit, INITPRIO_GAMETYPE_FALLBACK);
 +}
 +
  void Map_MarkAsRecent(string m);
  float world_already_spawned;
  void Nagger_Init();
@@@ -1735,11 -1685,10 +1736,11 @@@ float WinningCondition_Scores(float lim
  
        if(teamplay)
        {
 -              team1_score = TeamScore_GetCompareValue(NUM_TEAM_1);
 -              team2_score = TeamScore_GetCompareValue(NUM_TEAM_2);
 -              team3_score = TeamScore_GetCompareValue(NUM_TEAM_3);
 -              team4_score = TeamScore_GetCompareValue(NUM_TEAM_4);
 +              for (int i = 1; i < 5; ++i)
 +              {
 +                      Team_SetTeamScore(Team_GetTeamFromIndex(i),
 +                              TeamScore_GetCompareValue(Team_IndexToTeam(i)));
 +              }
        }
  
        ClearWinners();
@@@ -1809,32 -1758,30 +1810,32 @@@ float WinningCondition_RanOutOfSpawns(
        if(!some_spawn_has_been_used)
                return WINNING_NO;
  
 -      team1_score = team2_score = team3_score = team4_score = 0;
 -
 -      FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
 -              switch(it.team)
 +      for (int i = 1; i < 5; ++i)
 +      {
 +              Team_SetTeamScore(Team_GetTeamFromIndex(i), 0);
 +      }
 +      
 +      FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
 +      {
 +              if (Team_IsValidTeam(it.team))
                {
 -                      case NUM_TEAM_1: team1_score = 1; break;
 -                      case NUM_TEAM_2: team2_score = 1; break;
 -                      case NUM_TEAM_3: team3_score = 1; break;
 -                      case NUM_TEAM_4: team4_score = 1; break;
 +                      Team_SetTeamScore(Team_GetTeam(it.team), 1);
                }
        });
  
        IL_EACH(g_spawnpoints, true,
        {
 -              switch(it.team)
 +              if (Team_IsValidTeam(it.team))
                {
 -                      case NUM_TEAM_1: team1_score = 1; break;
 -                      case NUM_TEAM_2: team2_score = 1; break;
 -                      case NUM_TEAM_3: team3_score = 1; break;
 -                      case NUM_TEAM_4: team4_score = 1; break;
 +                      Team_SetTeamScore(Team_GetTeam(it.team), 1);
                }
        });
  
        ClearWinners();
 +      float team1_score = Team_GetTeamScore(Team_GetTeamFromIndex(1));
 +      float team2_score = Team_GetTeamScore(Team_GetTeamFromIndex(2));
 +      float team3_score = Team_GetTeamScore(Team_GetTeamFromIndex(3));
 +      float team4_score = Team_GetTeamScore(Team_GetTeamFromIndex(4));
        if(team1_score + team2_score + team3_score + team4_score == 0)
        {
                checkrules_equality = true;
        {
                float t, i;
                if(team1_score)
 -                      t = NUM_TEAM_1;
 +                      t = 1;
                else if(team2_score)
 -                      t = NUM_TEAM_2;
 +                      t = 2;
                else if(team3_score)
 -                      t = NUM_TEAM_3;
 +                      t = 3;
                else // if(team4_score)
 -                      t = NUM_TEAM_4;
 -              CheckAllowedTeams(NULL);
 +                      t = 4;
 +              entity balance = TeamBalance_CheckAllowedTeams(NULL);
                for(i = 0; i < MAX_TEAMSCORE; ++i)
                {
 -                      if(t != NUM_TEAM_1) if(c1 >= 0) TeamScore_AddToTeam(NUM_TEAM_1, i, -1000);
 -                      if(t != NUM_TEAM_2) if(c2 >= 0) TeamScore_AddToTeam(NUM_TEAM_2, i, -1000);
 -                      if(t != NUM_TEAM_3) if(c3 >= 0) TeamScore_AddToTeam(NUM_TEAM_3, i, -1000);
 -                      if(t != NUM_TEAM_4) if(c4 >= 0) TeamScore_AddToTeam(NUM_TEAM_4, i, -1000);
 +                      for (int j = 1; j <= NUM_TEAMS; ++j)
 +                      {
 +                              if (t == j)
 +                              {
 +                                      continue;
 +                              }
 +                              if (!TeamBalance_IsTeamAllowed(balance, j))
 +                              {
 +                                      continue;
 +                              }
 +                              TeamScore_AddToTeam(Team_IndexToTeam(j), i, -1000);
 +                      }
                }
  
                AddWinners(team, t);
index 0757cdcf8a985d4cd124417cc055dea8570805d9,3f35fe9eaa39e816f3a8bf9c063cc4977c735087..87e2123f7a0a6300ec7b9b14ad8b080bf35f660e
@@@ -129,51 -129,40 +129,51 @@@ MUTATOR_HOOKABLE(GiveFragsForKill, EV_G
  /** called when the match ends */
  MUTATOR_HOOKABLE(MatchEnd, EV_NO_ARGS);
  
 -/** allows adjusting allowed teams */
 -#define EV_CheckAllowedTeams(i, o) \
 +/** Allows adjusting allowed teams. Return true to use the bitmask value and set
 + * non-empty string to use team entity name. Both behaviors can be active at the
 + * same time and will stack allowed teams.
 + */
 +#define EV_TeamBalance_CheckAllowedTeams(i, o) \
      /** mask of teams      */ i(float, MUTATOR_ARGV_0_float) \
      /**/                      o(float, MUTATOR_ARGV_0_float) \
      /** team entity name   */ i(string, MUTATOR_ARGV_1_string) \
      /**/                      o(string, MUTATOR_ARGV_1_string) \
      /** player checked     */ i(entity, MUTATOR_ARGV_2_entity) \
      /**/
 -MUTATOR_HOOKABLE(CheckAllowedTeams, EV_CheckAllowedTeams);
 +MUTATOR_HOOKABLE(TeamBalance_CheckAllowedTeams,
 +      EV_TeamBalance_CheckAllowedTeams);
  
  /** return true to manually override team counts */
 -MUTATOR_HOOKABLE(GetTeamCounts, EV_NO_ARGS);
 +MUTATOR_HOOKABLE(TeamBalance_GetTeamCounts, EV_NO_ARGS);
  
 -/** allow overriding of team counts */
 -#define EV_GetTeamCount(i, o) \
 -    /** team to count                   */ i(float, MUTATOR_ARGV_0_float) \
 +/** allows overriding of team counts */
 +#define EV_TeamBalance_GetTeamCount(i, o) \
 +    /** team index 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 human 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) \
 +    /** number of players in a team     */ o(float, MUTATOR_ARGV_2_float) \
 +    /** number of bots in a team        */ o(float, MUTATOR_ARGV_3_float) \
 +    /**/
 +MUTATOR_HOOKABLE(TeamBalance_GetTeamCount, EV_TeamBalance_GetTeamCount);
 +
 +/** allows overriding the teams that will make the game most balanced if the
 + *  player joins any of them.
 + */
 +#define EV_TeamBalance_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);
 +MUTATOR_HOOKABLE(TeamBalance_FindBestTeams, EV_TeamBalance_FindBestTeams);
 +
 +/** Called during autobalance. Return true to override the player that will be
 +switched. */
 +#define EV_TeamBalance_GetPlayerForTeamSwitch(i, o) \
 +    /** source team index      */ i(int, MUTATOR_ARGV_0_int) \
 +    /** destination team index */ i(int, MUTATOR_ARGV_1_int) \
 +    /** is looking for bot     */ i(bool, MUTATOR_ARGV_2_bool) \
 +    /** player to switch       */ o(entity, MUTATOR_ARGV_3_entity) \
 +    /**/
 +MUTATOR_HOOKABLE(TeamBalance_GetPlayerForTeamSwitch,
 +      EV_TeamBalance_GetPlayerForTeamSwitch);
  
  /** copies variables for spectating "spectatee" to "this" */
  #define EV_SpectateCopy(i, o) \
@@@ -252,7 -241,7 +252,7 @@@ MUTATOR_HOOKABLE(CustomizeWaypoint, EV_
  MUTATOR_HOOKABLE(FilterItemDefinition, EV_FilterItemDefinition);
  
  /**
-  * checks if the current item may be spawned (.items and .weapons may be read and written to, as well as the ammo_ fields)
+  * checks if the current item may be spawned (.items may be read and written to, as well as the ammo_ fields)
   * return error to request removal
   */
  #define EV_FilterItem(i, o) \
@@@ -422,7 -411,8 +422,8 @@@ MUTATOR_HOOKABLE(PlayerDamage_SplitHeal
      /** mirrordamage    */ i(float,  MUTATOR_ARGV_5_float) \
      /** mirrordamage  */ o(float,  MUTATOR_ARGV_5_float) \
      /** force           */ i(vector, MUTATOR_ARGV_6_vector) \
-     /** force                         */ o(vector, MUTATOR_ARGV_6_vector) \
+     /** force           */ o(vector, MUTATOR_ARGV_6_vector) \
+     /** weapon entity         */ i(entity, MUTATOR_ARGV_7_entity) \
      /**/
  MUTATOR_HOOKABLE(Damage_Calculate, EV_Damage_Calculate);
  
@@@ -1019,9 -1009,9 +1020,9 @@@ MUTATOR_HOOKABLE(MonsterModel, EV_Monst
   * 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) \
 -      /** new team */       i(float, MUTATOR_ARGV_2_float) \
 +    /** player */             i(entity, MUTATOR_ARGV_0_entity) \
 +    /** current team index */ i(float, MUTATOR_ARGV_1_float) \
 +    /** new team index */     i(float, MUTATOR_ARGV_2_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) \
 +    /** player */             i(entity, MUTATOR_ARGV_0_entity) \
 +    /** old team index */     i(float, MUTATOR_ARGV_1_float) \
 +    /** current team index */ i(float, MUTATOR_ARGV_2_float) \
      /**/
  MUTATOR_HOOKABLE(Player_ChangedTeam, EV_Player_ChangedTeam);
  
index a35962e4e67e3899ebceb99d8883c1844c651852,176661ac9888944de75a17cb9bd8916a9b5f5224..52fc272dcb46182ac457c43706d230eeb57af031
@@@ -5,64 -5,53 +5,64 @@@ bool autocvar_g_ca_spectate_enemies
  
  void CA_count_alive_players()
  {
 -      total_players = redalive = bluealive = yellowalive = pinkalive = 0;
 -      FOREACH_CLIENT(IS_PLAYER(it), {
 -              switch(it.team)
 +      total_players = 0;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              Team_SetNumberOfAlivePlayers(Team_GetTeamFromIndex(i), 0);
 +      }
 +      FOREACH_CLIENT(IS_PLAYER(it) && Entity_HasValidTeam(it),
 +      {
 +              ++total_players;
 +              if (IS_DEAD(it))
                {
 -                      case NUM_TEAM_1: ++total_players; if(!IS_DEAD(it)) ++redalive; break;
 -                      case NUM_TEAM_2: ++total_players; if(!IS_DEAD(it)) ++bluealive; break;
 -                      case NUM_TEAM_3: ++total_players; if(!IS_DEAD(it)) ++yellowalive; break;
 -                      case NUM_TEAM_4: ++total_players; if(!IS_DEAD(it)) ++pinkalive; break;
 +                      continue;
                }
 +              entity team_ = Entity_GetTeam(it);
 +              int num_alive = Team_GetNumberOfAlivePlayers(team_);
 +              ++num_alive;
 +              Team_SetNumberOfAlivePlayers(team_, num_alive);
        });
 -      FOREACH_CLIENT(IS_REAL_CLIENT(it), {
 -              STAT(REDALIVE, it) = redalive;
 -              STAT(BLUEALIVE, it) = bluealive;
 -              STAT(YELLOWALIVE, it) = yellowalive;
 -              STAT(PINKALIVE, it) = pinkalive;
 +      FOREACH_CLIENT(IS_REAL_CLIENT(it),
 +      {
 +              STAT(REDALIVE, it) = Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(
 +                      1));
 +              STAT(BLUEALIVE, it) = Team_GetNumberOfAlivePlayers(
 +                      Team_GetTeamFromIndex(2));
 +              STAT(YELLOWALIVE, it) = Team_GetNumberOfAlivePlayers(
 +                      Team_GetTeamFromIndex(3));
 +              STAT(PINKALIVE, it) = Team_GetNumberOfAlivePlayers(
 +                      Team_GetTeamFromIndex(4));
        });
  }
  
 -float CA_GetWinnerTeam()
 +int CA_GetWinnerTeam()
  {
 -      float winner_team = 0;
 -      if(redalive >= 1)
 -              winner_team = NUM_TEAM_1;
 -      if(bluealive >= 1)
 +      int winner_team = 0;
 +      if (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(1)) >= 1)
        {
 -              if(winner_team) return 0;
 -              winner_team = NUM_TEAM_2;
 +              winner_team = NUM_TEAM_1;
        }
 -      if(yellowalive >= 1)
 +      for (int i = 2; i <= NUM_TEAMS; ++i)
        {
 -              if(winner_team) return 0;
 -              winner_team = NUM_TEAM_3;
 +              if (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(i)) >= 1)
 +              {
 +                      if (winner_team != 0)
 +                      {
 +                              return 0;
 +                      }
 +                      winner_team = Team_IndexToTeam(i);
 +              }
        }
 -      if(pinkalive >= 1)
 +      if (winner_team)
        {
 -              if(winner_team) return 0;
 -              winner_team = NUM_TEAM_4;
 -      }
 -      if(winner_team)
                return winner_team;
 +      }
        return -1; // no player left
  }
  
  void nades_Clear(entity player);
  
 -#define CA_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0) + (yellowalive > 0) + (pinkalive > 0))
 -#define CA_ALIVE_TEAMS_OK() (CA_ALIVE_TEAMS() == NumTeams(ca_teams))
 +#define CA_ALIVE_TEAMS_OK() (Team_GetNumberOfAliveTeams() == NumTeams(ca_teams))
  float CA_CheckWinner()
  {
        if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
        }
  
        CA_count_alive_players();
 -      if(CA_ALIVE_TEAMS() > 1)
 +      if (Team_GetNumberOfAliveTeams() > 1)
 +      {
                return 0;
 +      }
  
        int winner_team = CA_GetWinnerTeam();
        if(winner_team > 0)
@@@ -130,14 -117,14 +130,14 @@@ bool CA_CheckTeams(
                return false;
        }
        int missing_teams_mask = 0;
 -      if(ca_teams & BIT(0))
 -              missing_teams_mask += (!redalive) * 1;
 -      if(ca_teams & BIT(1))
 -              missing_teams_mask += (!bluealive) * 2;
 -      if(ca_teams & BIT(2))
 -              missing_teams_mask += (!yellowalive) * 4;
 -      if(ca_teams & BIT(3))
 -              missing_teams_mask += (!pinkalive) * 8;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              if ((ca_teams & Team_IndexToBit(i)) &&
 +                      (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(i)) == 0))
 +              {
 +                      missing_teams_mask |= Team_IndexToBit(i);
 +              }
 +      }
        if(prev_missing_teams_mask != missing_teams_mask)
        {
                Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask);
@@@ -243,10 -230,9 +243,10 @@@ MUTATOR_HOOKFUNCTION(ca, reset_map_glob
        return true;
  }
  
 -MUTATOR_HOOKFUNCTION(ca, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 +MUTATOR_HOOKFUNCTION(ca, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
  {
        M_ARGV(0, float) = ca_teams;
 +      return true;
  }
  
  entity ca_LastPlayerForTeam(entity this)
@@@ -282,9 -268,10 +282,10 @@@ MUTATOR_HOOKFUNCTION(ca, PlayerDies
        if (!allowed_to_spawn)
        {
                frag_target.respawn_flags = RESPAWN_SILENT;
-               // prevent unwanted sudden rejoin as spectator and move of spectator camera
+               // prevent unwanted sudden rejoin as spectator and movement of spectator camera
                frag_target.respawn_time = time + 2;
        }
+       frag_target.respawn_flags |= RESPAWN_FORCE;
        if (!warmup_stage)
                eliminatedPlayers.SendFlags |= 1;
        if(IS_BOT_CLIENT(frag_target))
diff --combined qcsrc/server/player.qc
index 2a8710137554406ebbd6a2a256e6357c701bb8a9,2fcd3016395bf1492bfe209242392909a1a816c8..38631188ed4800ad0d682fefa8ccdac86f75b195
@@@ -664,6 -664,21 +664,6 @@@ void PlayerDamage(entity this, entity i
        }
  }
  
 -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
 -      if (!SetPlayerTeamSimple(client, team_colour))
 -      {
 -              return false;
 -      }
 -      Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, DMG_NOWEP, 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 */
  void dedicated_print(string input)
  {
@@@ -954,7 -969,7 +954,7 @@@ int Say(entity source, int teamsay, ent
        if (privatesay && source && !IS_PLAYER(source))
        {
                if (!game_stopped)
-               if ((privatesay && !IS_PLAYER(privatesay)) || (autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !warmup_stage))
+               if ((privatesay && IS_PLAYER(privatesay)) && ((autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !warmup_stage)))
                        ret = -1; // just hide the message completely
        }