Merge branch 'master' into Lyberta/TeamplayOverhaul
authorLyberta <lyberta@lyberta.net>
Fri, 15 Jun 2018 04:51:33 +0000 (07:51 +0300)
committerLyberta <lyberta@lyberta.net>
Fri, 15 Jun 2018 04:51:33 +0000 (07:51 +0300)
1  2 
qcsrc/common/gamemodes/gamemode/ctf/ctf.qc
qcsrc/server/client.qc
qcsrc/server/command/cmd.qc
qcsrc/server/command/sv_cmd.qc
qcsrc/server/g_world.qc
qcsrc/server/g_world.qh
qcsrc/server/mutators/events.qh

@@@ -1377,7 -1377,8 +1377,8 @@@ void havocbot_ctf_calculate_middlepoint
        havocbot_middlepoint = s / n;
        havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
  
-       havocbot_symmetryaxis_equation = '0 0 0';
+       havocbot_symmetry_axis_m = 0;
+       havocbot_symmetry_axis_q = 0;
        if(n == 2)
        {
                // for symmetrical editing of waypoints
                entity f2 = f1.ctf_worldflagnext;
                float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
                float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
-               havocbot_symmetryaxis_equation.x = m;
-               havocbot_symmetryaxis_equation.y = q;
+               havocbot_symmetry_axis_m = m;
+               havocbot_symmetry_axis_q = q;
        }
-       // store number of flags in this otherwise unused vector component
-       havocbot_symmetryaxis_equation.z = n;
+       havocbot_symmetry_origin_order = n;
  }
  
  
@@@ -2466,9 -2466,11 +2466,9 @@@ MUTATOR_HOOKFUNCTION(ctf, HavocBot_Choo
        return true;
  }
  
 -MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
 +MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
  {
 -      //M_ARGV(0, float) = ctf_teams;
        M_ARGV(1, string) = "ctf_team";
 -      return true;
  }
  
  MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
@@@ -2694,7 -2696,7 +2694,7 @@@ spawnfunc(team_CTL_bluelolly)  { spawnf
  // scoreboard setup
  void ctf_ScoreRules(int teams)
  {
 -      CheckAllowedTeams(NULL);
 +      //CheckAllowedTeams(NULL); // Bug? Need to get allowed teams?
        GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
          field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
          field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
diff --combined qcsrc/server/client.qc
@@@ -17,6 -17,7 +17,7 @@@
  #include "handicap.qh"
  #include "g_hook.qh"
  #include "command/common.qh"
+ #include "command/vote.qh"
  #include "cheats.qh"
  #include "g_world.qh"
  #include "race.qh"
@@@ -46,6 -47,8 +47,8 @@@
  #include "../common/net_linked.qh"
  #include "../common/physics/player.qh"
  
+ #include <common/vehicles/sv_vehicles.qh>
  #include "../common/items/_mod.qh"
  
  #include "../common/mutators/mutator/waypoints/all.qh"
@@@ -75,8 -78,6 +78,6 @@@ STATIC_METHOD(Client, Add, void(Client 
      PutClientInServer(this);
  }
  
- void PutObserverInServer(entity this);
  STATIC_METHOD(Client, Remove, void(Client this))
  {
      TRANSMUTE(Observer, this);
@@@ -167,9 -168,6 +168,6 @@@ void ClientData_Touch(entity e
        });
  }
  
- void SetSpectatee(entity this, entity spectatee);
- void SetSpectatee_status(entity this, int spectatee_num);
  
  /*
  =============
@@@ -223,7 -221,6 +221,6 @@@ void setplayermodel(entity e, string mo
                UpdatePlayerSounds(e);
  }
  
- void FixPlayermodel(entity player);
  /** putting a client as observer in the server */
  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;
        }
@@@ -523,7 -522,7 +520,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) {
@@@ -789,8 -788,6 +786,6 @@@ void PutClientInServer(entity this
        }
  }
  
- void ClientInit_misc(entity this);
  // TODO do we need all these fields, or should we stop autodetecting runtime
  // changes and just have a console command to update this?
  bool ClientInit_SendEntity(entity this, entity to, int sf)
@@@ -910,7 -907,7 +905,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)
        {
@@@ -1168,76 -1165,6 +1163,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
@@@ -1293,19 -1220,10 +1288,10 @@@ 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) {
-               TRANSMUTE(Observer, this);
-       } else {
-               if (!teamplay || autocvar_g_balance_teams) {
-                       TRANSMUTE(Player, this);
-                       campaign_bots_may_start = true;
-               } else {
-                       TRANSMUTE(Observer, this); // do it anyway
-               }
-       }
+       TRANSMUTE(Observer, this);
  
        PlayerStats_GameReport_AddEvent(sprintf("kills-%d", this.playerid));
  
        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
@@@ -1406,7 -1327,6 +1392,6 @@@ Called when a client disconnects from t
  =============
  */
  .entity chatbubbleentity;
- void ReadyCount();
  void ClientDisconnect(entity this)
  {
        assert(IS_CLIENT(this), return);
@@@ -2132,7 -2052,7 +2117,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;
@@@ -2263,9 -2181,11 +2248,11 @@@ void PrintWelcomeMessage(entity this
        }
  }
  
+ const int MIN_SPEC_TIME = 1;
  bool joinAllowed(entity this)
  {
        if (CS(this).version_mismatch) return false;
+       if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
        if (!nJoinAllowed(this, this)) return false;
        if (teamplay && lockteams) return false;
        if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
@@@ -2523,7 -2443,6 +2510,6 @@@ void SpectatorThink(entity this
        this.flags |= FL_CLIENT | FL_NOTARGET;
  }
  
- void vehicles_enter (entity pl, entity veh);
  void PlayerUseKey(entity this)
  {
        if (!IS_PLAYER(this))
@@@ -2704,6 -2623,8 +2690,8 @@@ void PlayerPreThink (entity this
                PrintWelcomeMessage(this);
  
        if (IS_PLAYER(this)) {
+               if (IS_REAL_CLIENT(this) && time < CS(this).jointime + MIN_SPEC_TIME)
+                       error("Client can't be spawned as player on connection!");
                if(!PlayerThink(this))
                        return;
        }
                        IntermissionThink(this);
                return;
        }
+       else if (IS_REAL_CLIENT(this) && !CS(this).autojoin_checked && time >= CS(this).jointime + MIN_SPEC_TIME)
+       {
+               CS(this).autojoin_checked = true;
+               // don't do this in ClientConnect
+               // many things can go wrong if a client is spawned as player on connection
+               if (MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
+                       || (!(autocvar_sv_spectate || autocvar_g_campaign || this.team_forced < 0)
+                               && (!teamplay || autocvar_g_balance_teams)))
+               {
+                       campaign_bots_may_start = true;
+                       Join(this);
+                       return;
+               }
+       }
        else if (IS_OBSERVER(this)) {
                ObserverThink(this);
        }
@@@ -10,6 -10,7 +10,7 @@@
  
  #include "../campaign.qh"
  #include "../cheats.qh"
+ #include "../client.qh"
  #include "../player.qh"
  #include "../ipban.qh"
  #include "../mapvoting.qh"
@@@ -41,8 -42,6 +42,6 @@@
  
  #include <lib/warpzone/common.qh>
  
- void ClientKill_TeamChange(entity this, float targetteam);  // 0 = don't change, -1 = auto, -2 = spec
  // =========================================================
  //  Server side networked commands code, reworked by Samual
  //  Last updated: December 28th, 2011
@@@ -166,8 -165,6 +165,6 @@@ void ClientCommand_mv_getpicture(entit
        }
  }
  
- bool joinAllowed(entity this);
- void Join(entity this);
  void ClientCommand_join(entity caller, float request)
  {
        switch (request)
@@@ -397,16 -394,13 +394,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))
  
  #include <common/monsters/sv_monsters.qh>
  
- void PutObserverInServer(entity this);
- // =====================================================
- //  Server side game commands code, reworked by Samual
- // =====================================================
  //  used by GameCommand_make_mapinfo()
  void make_mapinfo_Think(entity this)
  {
@@@ -1065,7 -1058,6 +1058,7 @@@ void GameCommand_moveplayer(float reque
  
                                                                // find the team to move the player to
                                                                team_id = Team_ColorToTeam(destination);
 +                                                              entity balance;
                                                                if (team_id == client.team)  // already on the destination team
                                                                {
                                                                        // keep the forcing undone
                                                                }
                                                                else if (team_id == 0)  // auto team
                                                                {
 -                                                                      CheckAllowedTeams(client);
 -                                                                      team_id = Team_NumberToTeam(FindSmallestTeam(client, false));
 +                                                                      balance = TeamBalance_CheckAllowedTeams(client);
 +                                                                      team_id = Team_IndexToTeam(TeamBalance_FindBestTeam(balance, client, false));
                                                                }
                                                                else
                                                                {
 -                                                                      CheckAllowedTeams(client);
 +                                                                      balance = TeamBalance_CheckAllowedTeams(client);
                                                                }
                                                                client.team_forced = save;
  
                                                                // Check to see if the destination team is even available
                                                                switch (team_id)
                                                                {
 -                                                                      case NUM_TEAM_1: if (c1 == -1) { LOG_INFO("Sorry, can't move player to red team if it doesn't exist."); return; } break;
 -                                                                      case NUM_TEAM_2: if (c2 == -1) { LOG_INFO("Sorry, can't move player to blue team if it doesn't exist."); return; } break;
 -                                                                      case NUM_TEAM_3: if (c3 == -1) { LOG_INFO("Sorry, can't move player to yellow team if it doesn't exist."); return; } break;
 -                                                                      case NUM_TEAM_4: if (c4 == -1) { LOG_INFO("Sorry, can't move player to pink team if it doesn't exist."); return; } break;
 -
 -                                                                      default: LOG_INFO("Sorry, can't move player here if team ", destination, " doesn't exist.");
 +                                                                      case NUM_TEAM_1:
 +                                                                      {
 +                                                                              if (!TeamBalance_IsTeamAllowed(balance, 1))
 +                                                                              {
 +                                                                                      LOG_INFO("Sorry, can't move player to red team if it doesn't exist.");
 +                                                                                      TeamBalance_Destroy(balance);
 +                                                                                      return;
 +                                                                              }
 +                                                                              TeamBalance_Destroy(balance);
 +                                                                              break;
 +                                                                      }
 +                                                                      case NUM_TEAM_2:
 +                                                                      {
 +                                                                              if (!TeamBalance_IsTeamAllowed(balance, 2))
 +                                                                              {
 +                                                                                      LOG_INFO("Sorry, can't move player to blue team if it doesn't exist.");
 +                                                                                      TeamBalance_Destroy(balance);
 +                                                                                      return;
 +                                                                              }
 +                                                                              TeamBalance_Destroy(balance);
 +                                                                              break;
 +                                                                      }
 +                                                                      case NUM_TEAM_3:
 +                                                                      {
 +                                                                              if (!TeamBalance_IsTeamAllowed(balance, 3))
 +                                                                              {
 +                                                                                      LOG_INFO("Sorry, can't move player to yellow team if it doesn't exist.");
 +                                                                                      TeamBalance_Destroy(balance);
 +                                                                                      return;
 +                                                                              }
 +                                                                              TeamBalance_Destroy(balance);
 +                                                                              break;
 +                                                                      }
 +                                                                      case NUM_TEAM_4:
 +                                                                      {
 +                                                                              if (!TeamBalance_IsTeamAllowed(balance, 4))
 +                                                                              {
 +                                                                                      LOG_INFO("Sorry, can't move player to pink team if it doesn't exist.");
 +                                                                                      TeamBalance_Destroy(balance);
 +                                                                                      return;
 +                                                                              }
 +                                                                              TeamBalance_Destroy(balance);
 +                                                                              break;
 +                                                                      }
 +                                                                      default:
 +                                                                      {
 +                                                                              LOG_INFO("Sorry, can't move player here if team ", destination, " doesn't exist.");
                                                                                return;
 +                                                                      }
                                                                }
  
                                                                // If so, lets continue and finally move the player
                                                                client.team_forced = 0;
 -                                                              if (MoveToTeam(client, team_id, 6))
 +                                                              if (MoveToTeam(client, Team_TeamToIndex(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.");
@@@ -1410,23 -1360,16 +1403,23 @@@ void GameCommand_shuffleteams(float req
                        });
  
                        int number_of_teams = 0;
 -                      CheckAllowedTeams(NULL);
 -                      if (c1 >= 0) number_of_teams = max(1, number_of_teams);
 -                      if (c2 >= 0) number_of_teams = max(2, number_of_teams);
 -                      if (c3 >= 0) number_of_teams = max(3, number_of_teams);
 -                      if (c4 >= 0) number_of_teams = max(4, number_of_teams);
 +                      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +                      for (int i = 1; i <= NUM_TEAMS; ++i)
 +                      {
 +                              if (TeamBalance_IsTeamAllowed(balance, i))
 +                              {
 +                                      number_of_teams = max(i, number_of_teams);
 +                              }
 +                      }
 +                      TeamBalance_Destroy(balance);
  
                        int team_index = 0;
                        FOREACH_CLIENT_RANDOM(IS_PLAYER(it) || it.caplayer, {
 -                              int target_team_number = Team_NumberToTeam(team_index + 1);
 -                              if (it.team != target_team_number) MoveToTeam(it, target_team_number, 6);
 +                              int target_team_index = team_index + 1;
 +                              if (Entity_GetTeamIndex(it) != target_team_index)
 +                              {
 +                                      MoveToTeam(it, target_team_index, 6);
 +                              }
                                team_index = (team_index + 1) % number_of_teams;
                        });
  
@@@ -1628,10 -1571,13 +1621,13 @@@ void GameCommand_trace(float request, f
                                        if (argc == 4 || argc == 5)
                                        {
                                                e = nextent(NULL);
+                                               int dphitcontentsmask_save = e.dphitcontentsmask;
+                                               e.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
                                                if (tracewalk(e, stov(argv(2)), e.mins, e.maxs, stov(argv(3)), stof(argv(4)), MOVE_NORMAL))
                                                        LOG_INFO("can walk");
                                                else
                                                        LOG_INFO("cannot walk");
+                                               e.dphitcontentsmask = dphitcontentsmask_save;
                                                return;
                                        }
                                }
diff --combined qcsrc/server/g_world.qc
@@@ -92,8 -92,6 +92,6 @@@ const float SPAWNFLAG_NO_WAYPOINTS_FOR_
  string redirection_target;
  float world_initialized;
  
- void ShuffleMaplist();
  void SetDefaultAlpha()
  {
        if (!MUTATOR_CALLHOOK(SetDefaultAlpha))
@@@ -362,6 -360,7 +360,7 @@@ void cvar_changes_init(
                BADPREFIX("gameversion_");
                BADPREFIX("g_chat_");
                BADPREFIX("g_ctf_captimerecord_");
+               BADPREFIX("g_hats_");
                BADPREFIX("g_maplist_");
                BADPREFIX("g_mod_");
                BADPREFIX("g_respawn_");
@@@ -578,63 -577,7 +577,59 @@@ 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();
- void ClientInit_Spawn();
- void WeaponStats_Init();
- void WeaponStats_Shutdown();
  spawnfunc(worldspawn)
  {
        server_is_dedicated = boolean(stof(cvar_defstring("is_dedicated")));
@@@ -1108,7 -1051,7 +1103,7 @@@ void Map_Goto(float reinit
  // return codes of map selectors:
  //   -1 = temporary failure (that is, try some method that is guaranteed to succeed)
  //   -2 = permanent failure
- float() MaplistMethod_Iterate = // usual method
+ float MaplistMethod_Iterate() // usual method
  {
        float pass, i;
  
        return -1;
  }
  
- float() MaplistMethod_Repeat = // fallback method
+ float MaplistMethod_Repeat() // fallback method
  {
        LOG_TRACE("Trying MaplistMethod_Repeat");
  
        return -2;
  }
  
- float() MaplistMethod_Random = // random map selection
+ float MaplistMethod_Random() // random map selection
  {
        float i, imax;
  
        return -1;
  }
  
- float(float exponent) MaplistMethod_Shuffle = // more clever shuffling
+ float MaplistMethod_Shuffle(float exponent) // more clever shuffling
  // the exponent sets a bias on the map selection:
  // the higher the exponent, the less likely "shortly repeated" same maps are
  {
@@@ -1702,11 -1645,10 +1697,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();
@@@ -1776,32 -1718,30 +1771,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);
@@@ -2170,7 -2102,6 +2165,6 @@@ float RedirectionThink(
        return true;
  }
  
- void TargetMusic_RestoreGame();
  void RestoreGame()
  {
        // Loaded from a save game
diff --combined qcsrc/server/g_world.qh
@@@ -5,9 -5,6 +5,9 @@@ float checkrules_suddendeathwarning
  float checkrules_suddendeathend;
  float checkrules_overtimesadded; //how many overtimes have been already added
  
 +string cache_mutatormsg;
 +string cache_lastmutatormsg;
 +
  const int WINNING_NO = 0; // no winner, but time limits may terminate the game
  const int WINNING_YES = 1; // winner found
  const int WINNING_NEVER = 2; // no winner, enter overtime if time limit is reached
@@@ -27,5 -24,7 +27,7 @@@ string GetNextMap()
  void ShuffleMaplist();
  void Map_Goto_SetStr(string nextmapname);
  void Map_Goto(float reinit);
+ void Map_MarkAsRecent(string m);
  float DoNextMapOverride(float reinit);
  void CheckRules_World();
+ float RedirectionThink();
@@@ -27,6 -27,12 +27,12 @@@ MUTATOR_HOOKABLE(PutClientInServer, EV_
      /**/
  MUTATOR_HOOKABLE(ForbidSpawn, EV_ForbidSpawn);
  
+ /** returns true if client should be put as player on connection */
+ #define EV_AutoJoinOnConnection(i, o) \
+     /** player */ i(entity, MUTATOR_ARGV_0_entity) \
+     /**/
+ MUTATOR_HOOKABLE(AutoJoinOnConnection, EV_AutoJoinOnConnection);
  /** called when player spawns to determine whether to give them random start weapons. Return true to forbid giving them. */
  #define EV_ForbidRandomStartWeapons(i, o) \
        /** player */ i(entity, MUTATOR_ARGV_0_entity) \
@@@ -131,51 -137,40 +137,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) \
@@@ -1022,9 -1017,9 +1028,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);