PRoConEvents.MULTIbalancer.BalanceAndUnstack C# (CSharp) Method

BalanceAndUnstack() private method

private BalanceAndUnstack ( String name ) : void
name String
return void
        private void BalanceAndUnstack(String name)
        {
            /* Useful variables */

            PlayerModel player = null;
            String simpleMode = String.Empty;
            PerModeSettings perMode = null;
            bool isStrong = false; // this player
            int winningTeam = 0;
            int losingTeam = 0;
            int biggestTeam = 0;
            int smallestTeam = 0;
            int[] ascendingSize = null;
            int[] descendingTickets = null;
            String strongMsg = String.Empty;
            int diff = 0;
            DateTime now = DateTime.Now;
            bool needsBalancing = false;
            bool loggedStats = false;
            bool isSQDM = IsSQDM();
            bool isNonBalancingMode = IsNonBalancingMode();
            String log = String.Empty;

            /* Sanity checks */

            if (fServerInfo == null) {
            return;
            }

            int totalPlayerCount = TotalPlayerCount();

            if (DebugLevel >= 8) DebugBalance("BalanceAndUnstack(^b" + name + "^n), " + totalPlayerCount + " players");

            if (totalPlayerCount >= (MaximumServerSize-1)) {
            if (DebugLevel >= 6) DebugBalance("Server is full, no balancing or unstacking will be attempted!");
            IncrementTotal(); // no matching stat, reflect total deaths handled
            CheckDeativateBalancer("Full");
            return;
            }

            if (totalPlayerCount < 4) {
            if (DebugLevel >= 6) DebugBalance("Server is in warmup, less than 4 players");
            CheckDeativateBalancer("Warmup");
            return;
            }

            if (totalPlayerCount > 0) {
            AnalyzeTeams(out diff, out ascendingSize, out descendingTickets, out biggestTeam, out smallestTeam, out winningTeam, out losingTeam);
            } else {
            CheckDeativateBalancer("Empty");
            return;
            }

            if (EnableAdminKillForFastBalance && !isNonBalancingMode && diff > MaxFastDiff()) {
            DebugBalance("Fast balance is enabled and active, skipping normal balancing and unstacking");
            CheckDeativateBalancer("Fast balance is active");
            return;
            }

            /* Pre-conditions */

            player = GetPlayer(name);
            if (player == null) {
            CheckDeativateBalancer("Unknown player " + name);
            return;
            }

            if (!fModeToSimple.TryGetValue(fServerInfo.GameMode, out simpleMode)) {
            DebugBalance("Unknown game mode: " + fServerInfo.GameMode);
            simpleMode = fServerInfo.GameMode;
            }
            if (String.IsNullOrEmpty(simpleMode)) {
            DebugBalance("Simple mode is null: " + fServerInfo.GameMode);
            CheckDeativateBalancer("Unknown mode");
            return;
            }
            if (!fPerMode.TryGetValue(simpleMode, out perMode)) {
            DebugBalance("No per-mode settings for " + simpleMode + ", using defaults");
            perMode = new PerModeSettings();
            }
            if (perMode == null) {
            DebugBalance("Per-mode settings null for " + simpleMode + ", using defaults");
            perMode = new PerModeSettings();
            }

            if (fGameVersion == GameVersion.BFH && isNonBalancingMode) {
            DebugWrite("^5(AUTO)^9 Server is in ^b" + simpleMode + "^n mode, which should not be balanced! Deactivating balancer!", 4);
            CheckDeativateBalancer("BFH Competitive Mode");
            return;
            }

            /* Per-mode and player info */

            String extractedTag = ExtractTag(player);
            Speed balanceSpeed = GetBalanceSpeed(perMode);
            double unstackTicketRatio = GetUnstackTicketRatio(perMode);
            int lastMoveFrom = player.LastMoveFrom;

            if (totalPlayerCount >= (perMode.MaxPlayers-1)) {
            if (DebugLevel >= 6) DebugBalance("Server is full by per-mode Max Players, no balancing or unstacking will be attempted!");
            IncrementTotal(); // no matching stat, reflect total deaths handled
            CheckDeativateBalancer("Full per-mode");
            return;
            }

            int floorPlayers = (perMode.EnableLowPopulationAdjustments) ? 4 : 6;
            if (totalPlayerCount < floorPlayers) {
            if (DebugLevel >= 6) DebugBalance("Not enough players in server, minimum is " + floorPlayers);
            IncrementTotal(); // no matching stat, reflect total deaths handled
            CheckDeativateBalancer("Not enough players");
            return;
            }

            /* Check dispersals */

            bool mustMove = false;
            bool lenient = false;
            int maxDispersalMoves = 2;
            bool isDisperseByRank = IsRankDispersal(player);
            bool isDisperseByList = IsInDispersalList(player, false);
            /* DCE */
            bool isDisperseByClanPop = false;
            if (!isDisperseByList) {
            isDisperseByClanPop = IsClanDispersal(player, false);
            }

            if (isDisperseByList) {
            lenient = !perMode.EnableStrictDispersal; // the opposite of strict is lenient
            String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
            ConsoleDebug("ON MUST MOVE LIST ^b" + player.FullName + "^n T:" + player.Team + ", disperse evenly enabled, " + dispersalMode);
            mustMove = true;
            maxDispersalMoves = (lenient) ? 1 : 2;
            } else if (isDisperseByClanPop) {
            lenient = !perMode.EnableStrictDispersal; // the opposite of strict is lenient
            String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
            ConsoleDebug("ON MUST MOVE LIST ^b" + player.FullName + "^n T:" + player.Team + ", disperse clan tags evenly enabled, " + dispersalMode);
            mustMove = true;
            maxDispersalMoves = (lenient) ? 1 : 2;
            } else if (isDisperseByRank) {
            lenient = LenientRankDispersal || !perMode.EnableStrictDispersal;
            String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
            ConsoleDebug("ON MUST MOVE LIST ^b" + name + "^n T:" + player.Team + ", Rank " + player.Rank + " >= " + perMode.DisperseEvenlyByRank + ", " + dispersalMode);
            mustMove = true;
            maxDispersalMoves = (lenient) ? 1 : 2;
            }

            /* Check if balancing is needed */

            if (diff > MaxDiff()) {
            needsBalancing = true; // needs balancing set to true, unless speed is Unstack only
            if (balanceSpeed == Speed.Unstack) {
            DebugBalance("Needs balancing, but balance speed is set to Unstack, so no balancing will be done");
            needsBalancing = false;
            }
            }

            /* Per-mode settings */

            // Adjust for duration of balance active
            if (needsBalancing && fBalanceIsActive && balanceSpeed == Speed.Adaptive && fLastBalancedTimestamp != DateTime.MinValue) {
            double secs = now.Subtract(fLastBalancedTimestamp).TotalSeconds;
            if (secs > SecondsUntilAdaptiveSpeedBecomesFast) {
            DebugBalance("^8^bBalancing taking too long (" + secs.ToString("F0") + " secs)!^n^0 Forcing to Fast balance speed.");
            balanceSpeed = Speed.Fast;
            }
            }

            // Adjust speed to Fast if teams differ by 4 or more
            if (needsBalancing && balanceSpeed != Speed.Fast && balanceSpeed != Speed.Stop && !isSQDM && diff >= 4) {
            DebugBalance("^8^bTeam count difference is 4 or more (" + diff + ")!^n^0 Forcing to Fast balance speed.");
            balanceSpeed = Speed.Fast;
            }

            String orSlow = (balanceSpeed == Speed.Slow) ? " or speed is Slow" : String.Empty;

            // Do not disperse mustMove players if speed is Stop or Slow or Phase is Late or Popluation is Low and Enable Low Population Adjustments is True
            if (mustMove && balanceSpeed == Speed.Stop) {
            DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Balance Speed = Stop");
            mustMove = false;
            } else if (mustMove && balanceSpeed == Speed.Slow) {
            DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Balance Speed = Slow");
            mustMove = false;
            } else if (mustMove && GetPhase(perMode, false) == Phase.Late) {
            DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Phase = Late");
            mustMove = false;
            } else if (mustMove && perMode.EnableLowPopulationAdjustments && GetPopulation(perMode, false) == Population.Low) {
            DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Population = Low");
            mustMove = false;
            }

            /* Activation check */

            if (balanceSpeed != Speed.Stop && needsBalancing) {
            if (!fBalanceIsActive) {
            DebugBalance("^2^bActivating autobalance!");
            fLastBalancedTimestamp = now;
            }
            fBalanceIsActive = true;
            } else {
            CheckDeativateBalancer("Deactiving autobalance");
            }

            // Wait for unassigned
            if (!mustMove && needsBalancing && balanceSpeed != Speed.Fast && (diff > MaxDiff()) && fUnassigned.Count >= (diff - MaxDiff())) {
            DebugBalance("Wait for " + fUnassigned.Count + " unassigned players to be assigned before moving active players");
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            }

            /* Early exemptions - avoid doing exclusion computation if unnecessary */

            // Exempt if this player already been moved for balance or unstacking
            if ((!mustMove && GetMovesThisRound(player) >= 1) || (mustMove && GetMovesThisRound(player) >= maxDispersalMoves)) {
            DebugBalance("Exempting ^b" + name + "^n, already moved this round");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            // Exempt if role isn't ordinary player - mustMove always false for this case
            if (player.Role != ROLE_PLAYER) {
            String rn = "UNKNOWN";
            if (player.Role >= 0 && player.Role < ROLE_NAMES.Length) rn = ROLE_NAMES[player.Role];
            DebugBalance("Exempting ^b" + name + "^n, role is " + rn + " for team " + GetTeamName(player.Team));
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            /* Exclusions */

            // Exclude if on Whitelist or Reserved Slots if enabled
            if (OnWhitelist || (needsBalancing && balanceSpeed == Speed.Slow)) {
            if (CheckWhitelist(player, WL_BALANCE)) {
            DebugBalance("Excluding ^b" + player.FullName + "^n: whitelisted" + orSlow);
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }
            }

            // Sort player's team by the strong method
            List<PlayerModel> fromList = GetTeam(player.Team);
            if (fromList == null) {
            DebugBalance("Unknown team " + player.Team + " for player ^b" + player.Name);
            return;
            }
            switch (perMode.DetermineStrongPlayersBy) {
            case DefineStrong.RoundScore:
            fromList.Sort(DescendingRoundScore);
            strongMsg = "Determing strong by: Round Score";
            break;
            case DefineStrong.RoundSPM:
            fromList.Sort(DescendingRoundSPM);
            strongMsg = "Determing strong by: Round SPM";
            break;
            case DefineStrong.RoundKills:
            fromList.Sort(DescendingRoundKills);
            strongMsg = "Determing strong by: Round Kills";
            break;
            case DefineStrong.RoundKDR:
            fromList.Sort(DescendingRoundKDR);
            strongMsg = "Determing strong by: Round KDR";
            break;
            case DefineStrong.PlayerRank:
            fromList.Sort(DescendingPlayerRank);
            strongMsg = "Determing strong by: Player Rank";
            break;
            case DefineStrong.RoundKPM:
            fromList.Sort(DescendingRoundKPM);
            strongMsg = "Determing strong by: Round KPM";
            break;
            case DefineStrong.BattlelogSPM:
            fromList.Sort(DescendingSPM);
            strongMsg = "Determing strong by: Battlelog SPM";
            break;
            case DefineStrong.BattlelogKDR:
            fromList.Sort(DescendingKDR);
            strongMsg = "Determing strong by: Battlelog KDR";
            break;
            case DefineStrong.BattlelogKPM:
            fromList.Sort(DescendingKPM);
            strongMsg = "Determing strong by: Battlelog KPM";
            break;
            default:
            fromList.Sort(DescendingRoundScore);
            strongMsg = "Determing strong by: Round Score";
            break;
            }

            double above = ((fromList.Count * perMode.PercentOfTopOfTeamIsStrong) / 100.0) + 0.5;
            int strongest = Math.Max(0, Convert.ToInt32(above));
            int playerIndex = 0;
            int minPlayers = (isSQDM) ? 5 : fromList.Count; // for SQDM, apply top/strong/weak only if team has 5 or more players

            // Exclude if TopScorers enabled and a top scorer on the team
            int topPlayersPerTeam = 0;
            if (balanceSpeed != Speed.Fast && (TopScorers || balanceSpeed == Speed.Slow)) {
            if (isSQDM) {
            int maxCount = fromList.Count;
            if (maxCount < 5) {
                topPlayersPerTeam = 0;
            } else if (maxCount <= 8) {
                topPlayersPerTeam = 1;
            } else if (totalPlayerCount <= 16) {
                topPlayersPerTeam = 2;
            } else {
                topPlayersPerTeam = 3;
            }
            } else {
            if (totalPlayerCount <= 22) {
                topPlayersPerTeam = 1;
            } else if (totalPlayerCount >= 42) {
                topPlayersPerTeam = 3;
            } else {
                topPlayersPerTeam = 2;
            }
            }
            }
            // Loop is unconditional even when topPlayersPerTeam is zero, due to assigning playerIndex
            for (int i = 0; i < fromList.Count; ++i) {
            if (fromList[i].Name == player.Name) {
            if (!mustMove
            && needsBalancing
            && balanceSpeed != Speed.Fast
            && fromList.Count >= minPlayers
            && topPlayersPerTeam != 0
            && i < topPlayersPerTeam) {
                String why = (balanceSpeed == Speed.Slow) ? "Speed is slow, excluding top scorers" : "Top Scorers enabled";
                if (!loggedStats) {
                    DebugBalance(GetPlayerStatsString(name));
                    loggedStats = true;
                }
                DebugBalance("Excluding ^b" + player.FullName + "^n: " + why + " and this player is #" + (i+1) + " on team " + GetTeamName(player.Team));
                fExcludedRound = fExcludedRound + 1;
                IncrementTotal();
                return;
            } else {
                playerIndex = i;
                break;
            }
            }
            }
            isStrong = (playerIndex < strongest);

            // Exclude if too soon since last move
            if ((!mustMove || lenient) && player.MovedByMBTimestamp != DateTime.MinValue) {
            double mins = now.Subtract(player.MovedByMBTimestamp).TotalMinutes;
            if (mins < MinutesAfterBeingMoved) {
            DebugBalance("Excluding ^b" + player.Name + "^n: last move was " + mins.ToString("F0") + " minutes ago, less than required " + MinutesAfterBeingMoved.ToString("F0") + " minutes");
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }  else {
            // reset
            player.MovedByMBTimestamp = DateTime.MinValue;
            }
            }

            // Exclude if player joined less than MinutesAfterJoining
            double joinedMinutesAgo = GetPlayerJoinedTimeSpan(player).TotalMinutes;
            double enabledForMinutes = now.Subtract(fEnabledTimestamp).TotalMinutes;
            if ((!mustMove || lenient)
            && needsBalancing
            && (enabledForMinutes > MinutesAfterJoining)
            && balanceSpeed != Speed.Fast
            && (joinedMinutesAgo < MinutesAfterJoining)) {
            if (!loggedStats) {
            DebugBalance(GetPlayerStatsString(name));
            loggedStats = true;
            }
            DebugBalance("Excluding ^b" + player.FullName + "^n: joined less than " + MinutesAfterJoining.ToString("F1") + " minutes ago (" + joinedMinutesAgo.ToString("F1") + ")");
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }

            // Special exemption if tag not verified and fetches pending in the queue and joined less than 15 minutes ago
            if (!player.TagVerified && PriorityQueueCount() > 0 && joinedMinutesAgo < 15) {
            if (DebugLevel >= 7) DebugBalance("Skipping ^b" + player.Name + "^n, clan tag not verified yet");
            // Don't count this as an exemption
            // Don't increment the total
            return;
            }

            // Exclude if in squad with same tags
            if ((!mustMove || lenient) && SameClanTagsInSquad && !isDisperseByClanPop) {
            int cmt =  CountMatchingTags(player, Scope.SameSquad);
            if (cmt >= 2) {
            String et = ExtractTag(player);
            DebugBalance("Excluding ^b" + name + "^n, " + cmt + " players in squad with tag [" + et + "]");
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }
            }

            // Exclude if in team with same tags
            if ((!mustMove || lenient) && SameClanTagsInTeam && !isDisperseByClanPop) {
            int cmt =  CountMatchingTags(player, Scope.SameTeam);
            if (cmt >= 5 && !isDisperseByClanPop) {
            String et = ExtractTag(player);
            DebugBalance("Excluding ^b" + name + "^n, " + cmt + " players in team with tag [" + et + "]");
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }
            }

            // Exclude if on friends list
            if ((!mustMove || lenient) && OnFriendsList) {
            int cmf = CountMatchingFriends(player, Scope.SameSquad);
            if (cmf >= 2) {
            DebugBalance("Excluding ^b" + player.FullName + "^n, " + cmf + " players in squad are friends (friendex = " + player.Friendex + ")");
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }
            if (ApplyFriendsListToTeam) {
            cmf = CountMatchingFriends(player, Scope.SameTeam);
            if (cmf >= 5) {
                DebugBalance("Excluding ^b" + player.FullName + "^n, " + cmf + " players in team are friends (friendex = " + player.Friendex + ")");
                fExcludedRound = fExcludedRound + 1;
                IncrementTotal();
                return;
            }
            }
            }

            /* - moved earlier, left here in case need to restore:
            // Exempt if this player already been moved for balance or unstacking
            if ((!mustMove && GetMoves(player) >= 1) || (mustMove && GetMoves(player) >= maxDispersalMoves)) {
            DebugBalance("Exempting ^b" + name + "^n, already moved this round");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }
            */

            /* Balance */

            int toTeamDiff = 0;
            int toTeam = ToTeam(name, player.Team, false, out toTeamDiff, ref mustMove); // take into account dispersal by Rank, etc.

            if (toTeam == 0 || toTeam == player.Team) {
            if (needsBalancing || mustMove) {
            if (DebugLevel >= 7) DebugBalance("Exempting ^b" + name + "^n, target team selected is same or zero");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }
            }

            int numTeams = 2; //(isSQDM) ? 4 : 2; // TBD, what is max squad size for SQDM?
            int maxTeamSlots = (MaximumServerSize/numTeams);
            int maxTeamPerMode = (perMode.MaxPlayers/numTeams);
            List<PlayerModel> lt = GetTeam(toTeam);
            int toTeamSize = (lt == null) ? 0 : lt.Count;

            if (toTeamSize == maxTeamSlots || toTeamSize == maxTeamPerMode) {
            if (DebugLevel >= 8) DebugBalance("Exempting ^b" + name + "^n, target team is full " + toTeamSize);
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            if (mustMove) DebugBalance("^4MUST MOVE^0 ^b" + name + "^n from " + GetTeamName(player.Team) + " to " + GetTeamName(toTeam));

            if ((!mustMove || lenient) && needsBalancing && toTeamDiff <= MaxDiff()) {
            DebugBalance("Exempting ^b" + name + "^n, difference between " + GetTeamName(player.Team) + " team and " + GetTeamName(toTeam) + " team is only " + toTeamDiff);
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            /* Moved ticket ratios up here for Rout Percentage exemption */
            double ratio = 1;
            double t1Tickets = 0;
            double t2Tickets = 0;
            if (IsCTF() || IsCarrierAssault() || IsObliteration()) {
            // Use team points, not tickets
            double usPoints = GetTeamPoints(1);
            double ruPoints = GetTeamPoints(2);
            if (usPoints <= 0) usPoints = 1;
            if (ruPoints <= 0) ruPoints = 1;
            ratio = (usPoints > ruPoints) ? (usPoints/ruPoints) : (ruPoints/usPoints);
            } else {
            // Otherwise use ticket ratio
            if (fTickets[losingTeam] >= 1) {
            if (IsRush()) {
                // normalize Rush ticket ratio
                double attackers = fTickets[1];
                double defenders = fMaxTickets - (fRushMaxTickets - fTickets[2]);
                defenders = Math.Max(defenders, attackers/2);
                ratio = (attackers > defenders) ? (attackers/Math.Max(1, defenders)) : (defenders/Math.Max(1, attackers));
                t1Tickets = attackers;
                t2Tickets = defenders;
            } else {
                t1Tickets = Convert.ToDouble(fTickets[winningTeam]);
                t2Tickets = Convert.ToDouble(fTickets[losingTeam]);
                ratio =  t1Tickets / Math.Max(1, t2Tickets);
            }
            }
            }

            if ((fBalanceIsActive || mustMove) && toTeam != 0 && balanceSpeed != Speed.Stop) {
            String ts = null;
            if (isSQDM) {
            ts = fTeam1.Count + "(A) vs " + fTeam2.Count + "(B) vs " + fTeam3.Count + "(C) vs " + fTeam4.Count + "(D)";
            } else {
            ts = fTeam1.Count + "(" + GetTeamName(1) + ") vs " + fTeam2.Count + "(" + GetTeamName(2) + ")";
            }
            if (mustMove) {
            DebugBalance("Autobalancing because ^b" + name + "^n must be moved");
            } else {
            DebugBalance("Autobalancing because difference of " + diff + " is greater than " + MaxDiff() + ", [" + ts + "]");
            }
            double abTime = now.Subtract(fLastBalancedTimestamp).TotalSeconds;
            if (abTime > 0) {
            DebugBalance("^2^bAutobalance has been active for " + abTime.ToString("F1") + " seconds!");
            }

            if (!loggedStats) {
            DebugBalance(GetPlayerStatsString(name) + ((isStrong) ? " STRONG" : " WEAK"));
            loggedStats = true;
            }

            /* Exemptions */

            // Handle Rout exemptions
            double ratioPercentage = ratio * 100;
            if (perMode.RoutPercentage > 100 && ratioPercentage >= perMode.RoutPercentage) {
            DebugBalance("Rout detected, winning/losing ratio of " + ratioPercentage.ToString("F0") + " is greater than " + perMode.RoutPercentage.ToString("F0"));
            if (isStrong) {
                String si = "(" + playerIndex + " of " + strongest + ")";
                DebugBalance("Exempting ^b" + name + "^n^9 " + si + ", strong players are not moved during a rout");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            } else if (mustMove && lenient) {
                DebugBalance("Exempting ^b" + name + "^n^9, dispersal players are not moved during a rout when dispersal is lenient");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            }
            }

            // Already on the smallest team
            if ((!mustMove || lenient) && player.Team == smallestTeam) {
            DebugBalance("Exempting ^b" + name + "^n, already on the smallest team");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            // SQDM, not on the biggest team
            if (isSQDM && !mustMove && balanceSpeed != Speed.Fast && player.Team != biggestTeam) {
            // Make sure player's team isn't the same size as biggest
            List<PlayerModel> aTeam = GetTeam(player.Team);
            List<PlayerModel> bigTeam = GetTeam(biggestTeam);
            if (aTeam == null || bigTeam == null || (aTeam != null && bigTeam != null && aTeam.Count < bigTeam.Count)) {
                DebugBalance("Exempting ^b" + name + "^n, not on the biggest team");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            }
            }

            // Exempt if only moving weak players and is strong
            if (!mustMove && perMode.OnlyMoveWeakPlayers && isStrong) {
            DebugBalance("Exempting strong ^b" + name + "^n, Only Move Weak Players set to True for " + simpleMode);
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            // Strong/Weak exemptions
            if (!mustMove && balanceSpeed != Speed.Fast && fromList.Count >= minPlayers) {
            if (DebugLevel > 5) DebugBalance(strongMsg);
            // don't move weak player to losing team, unless we are only moving weak players
            if (!isStrong  && toTeam == losingTeam && !perMode.OnlyMoveWeakPlayers) {
                DebugBalance("Exempting ^b" + name + "^n, don't move weak player to losing team (#" + (playerIndex+1) + " of " + fromList.Count + ", top " + (strongest) + ")");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            }

            // don't move strong player to winning team
            if (isStrong && toTeam == winningTeam) {
                DebugBalance("Exempting ^b" + name + "^n, don't move strong player to winning team (#" + (playerIndex+1) + " of " + fromList.Count + ", median " + (strongest) + ")");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            }

            // Don't move to same team
            if (player.Team == toTeam) {
                if (DebugLevel >= 7) DebugBalance("Exempting ^b" + name + "^n, don't move player to his own team!");
                IncrementTotal(); // no matching stat, reflect total deaths handled
                return;
            }
            }

            /* Move for balance */

            int origTeam = player.Team;
            String origName = GetTeamName(player.Team);

            if (lastMoveFrom != 0) {
            origTeam = lastMoveFrom;
            origName = GetTeamName(origTeam);
            }

            MoveInfo move = new MoveInfo(name, player.Tag, origTeam, origName, toTeam, GetTeamName(toTeam), YellDurationSeconds);
            move.For = MoveType.Balance;
            move.Format(this, ChatMovedForBalance, false, false);
            move.Format(this, YellMovedForBalance, true, false);
            String why = (mustMove) ? "to disperse evenly" : ("because difference is " + diff);
            log = "^4^bBALANCE^n^0 moving ^b" + player.FullName + "^n from " + move.SourceName + " team to " + move.DestinationName + " team " + why;
            log = (EnableLoggingOnlyMode) ? "^9(SIMULATING)^0 " + log : log;
            DebugWrite(log, 3);

            DebugWrite("^9" + move, 8);

            player.LastMoveFrom = player.Team;
            StartMoveImmediate(move, false);

            if (EnableLoggingOnlyMode) {
            // Simulate completion of move
            OnPlayerTeamChange(name, toTeam, 0);
            OnPlayerMovedByAdmin(name, toTeam, 0, false); // simulate reverse order
            }
            // no increment total, handled later when move is processed
            return;
            }

            if (!fBalanceIsActive) {
            fLastBalancedTimestamp = now;
            if (DebugLevel >= 8) ConsoleDebug("fLastBalancedTimestamp = " + fLastBalancedTimestamp.ToString("HH:mm:ss"));
            }

            /* Unstack */

            // Not enabled or not full round
            if (!EnableUnstacking) {
            if (DebugLevel >= 8) DebugBalance("Unstack is disabled, Enable Unstacking is set to False");
            IncrementTotal();
            return;
            } else if (!fIsFullRound) {
            if (DebugLevel >= 7) DebugBalance("Unstack is disabled, not a full round");
            IncrementTotal();
            return;
            }

            // Sanity checks
            if (winningTeam <= 0 || winningTeam >= fTickets.Length || losingTeam <= 0 || losingTeam >= fTickets.Length || balanceSpeed == Speed.Stop) {
            if (DebugLevel >= 5) DebugBalance("Skipping unstack for player that was killed ^b" + name +"^n: winning = " + winningTeam + ", losingTeam = " + losingTeam + ", speed = " + balanceSpeed);
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            }

            // Server is full, can't swap
            if (totalPlayerCount > (MaximumServerSize-2) || totalPlayerCount > (perMode.MaxPlayers-2)) {
            // TBD - kick idle players?
            if (DebugLevel >= 7) DebugBalance("No room to swap players for unstacking");
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            }

            // Disabled per-mode
            if (perMode.CheckTeamStackingAfterFirstMinutes == 0) {
            if (DebugLevel >= 5) DebugBalance("Unstacking has been disabled, Check Team Stacking After First Minutes set to zero");
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            }

            double tirMins = GetTimeInRoundMinutes();

            // Too soon to unstack
            if (tirMins < perMode.CheckTeamStackingAfterFirstMinutes) {
            DebugBalance("Too early to check for unstacking, skipping ^b" + name + "^n");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            // Maximum swaps already done
            if ((fUnstackedRound/2) >= perMode.MaxUnstackingSwapsPerRound) {
            if (DebugLevel >= 6) DebugBalance("Maximum swaps have already occurred this round (" + (fUnstackedRound/2) + ")");
            fUnstackState = UnstackState.Off;
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            }

            // Whitelisted
            if (OnWhitelist) {
            if (CheckWhitelist(player, WL_UNSTACK)) {
            DebugBalance("Excluding from unstacking due to being whitelisted, ^b" + name + "^n");
            fExcludedRound = fExcludedRound + 1;
            IncrementTotal();
            return;
            }
            }

            /* - moved earlier, left here in case need to restore:
            double ratio = 1;
            double t1Tickets = 0;
            double t2Tickets = 0;
            if (IsCTF() || IsCarrierAssault()) {
            // Use team points, not tickets
            double usPoints = GetTeamPoints(1);
            double ruPoints = GetTeamPoints(2);
            if (usPoints <= 0) usPoints = 1;
            if (ruPoints <= 0) ruPoints = 1;
            ratio = (usPoints > ruPoints) ? (usPoints/ruPoints) : (ruPoints/usPoints);
            } else {
            // Otherwise use ticket ratio
            if (fTickets[losingTeam] >= 1) {
            if (IsRush()) {
                // normalize Rush ticket ratio
                double attackers = fTickets[1];
                double defenders = fMaxTickets - (fRushMaxTickets - fTickets[2]);
                defenders = Math.Max(defenders, attackers/2);
                ratio = (attackers > defenders) ? (attackers/Math.Max(1, defenders)) : (defenders/Math.Max(1, attackers));
                t1Tickets = attackers;
                t2Tickets = defenders;
            } else {
                t1Tickets = Convert.ToDouble(fTickets[winningTeam]);
                t2Tickets = Convert.ToDouble(fTickets[losingTeam]);
                ratio =  t1Tickets / Math.Max(1, t2Tickets);
            }
            }
            }
            */

            // Ticket difference greater than per-mode maximum for unstacking
            int ticketGap = Convert.ToInt32(Math.Abs(t1Tickets - t2Tickets));
            if (perMode.MaxUnstackingTicketDifference > 0 && ticketGap > perMode.MaxUnstackingTicketDifference) {
            DebugBalance("Ticket difference of " + ticketGap + " exceeds Max Unstacking Ticket Difference of " + perMode.MaxUnstackingTicketDifference + ", skipping ^b" + name + "^n");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            String um = "Current ratio " + (ratio*100.0).ToString("F0") + " vs. unstack ratio of " + (unstackTicketRatio*100.0).ToString("F0");

            // Using player stats instead of ticket ratio
            if (perMode.EnableUnstackingByPlayerStats) {
            double a1 = GetAveragePlayerStats(1, perMode.DetermineStrongPlayersBy);
            double a2 = GetAveragePlayerStats(2, perMode.DetermineStrongPlayersBy);
            ratio = (a1 > a2) ? (a1/Math.Max(0.01, a2)) : (a2/Math.Max(0.01, a1));
            ratio = Math.Min(ratio, 50.0); // cap at 50x

            // Don't unstack if the team with the lowest average stats is the winning team
            // We don't want to send strong players to the team with the highest score!
            if ((a1 < a2 && winningTeam == 1)
            ||  (a2 < a1 && winningTeam == 2)) {
            if (DebugLevel >= 7) DebugBalance("Team with lowest avg. stats is the winning team, do not unstack: " + a1.ToString("F1") + " vs " + a2.ToString("F1") + ", winning team is " + GetTeamName(winningTeam));
            IncrementTotal();
            return;
            }
            String cmp = (a1 > a2) ? (a1.ToString("F1") + "/" + a2.ToString("F1")) : (a2.ToString("F1") + "/" + a1.ToString("F1"));
            um = "Average " + perMode.DetermineStrongPlayersBy + " stats ratio is " + (ratio*100.0).ToString("F0") + " (" + cmp + ") vs. unstack ratio of " + (unstackTicketRatio*100.0).ToString("F0");
            }

            // Using ticket loss instead of ticket ratio?
            if (perMode.EnableTicketLossRatio && false) { // disable for this release
            double a1 = GetAverageTicketLossRate(1, false);
            double a2 = GetAverageTicketLossRate(2, false);
            ratio = (a1 > a2) ? (a1/Math.Max(1, a2)) : (a2/Math.Max(1, a1));
            ratio = Math.Min(ratio, 50.0); // cap at 50x
            um = "Ticket loss ratio is " + (ratio*100.0).ToString("F0") + " vs. unstack ratio of " + (unstackTicketRatio*100.0).ToString("F0");

            // Don't unstack if the team with the highest loss rate is the winning team
            // We don't want to send strong players to the team with the highest score!
            if ((a1 > a2 && winningTeam == 1)
            ||  (a2 > a1 && winningTeam == 2)) {
            if (DebugLevel >= 7) DebugBalance("Team with highest ticket loss rate is the winning team, do not unstack: " + a1.ToString("F1") + " vs " + a2.ToString("F1") + ", winning team is " + GetTeamName(winningTeam));
            IncrementTotal();
            return;
            }
            }

            if (unstackTicketRatio == 0 || ratio < unstackTicketRatio) {
            bool ticketRatioOk = true;
            bool scoreRatioOk = true;
            int maxStages = 4;
            bool isRush = IsRush();
            if (fServerInfo != null && isRush) maxStages = GetRushMaxStages(fServerInfo.Map);
            if (isRush && perMode.EnableAdvancedRushUnstacking && fRushStage > 0 && fRushStage < maxStages) {
            // Check team points as well as tickets
            double usPoints = GetTeamPoints(1);
            double ruPoints = GetTeamPoints(2);
            if (usPoints <= 0) usPoints = 1;
            if (ruPoints <= 0) ruPoints = 1;
            ratio = (usPoints > ruPoints) ? (usPoints/ruPoints) : (ruPoints/usPoints);
            if (DebugLevel >= 6) DebugBalance("Checking Advanced Rush Unstacking (by score): stage = " + fRushStage);
            scoreRatioOk = (unstackTicketRatio == 0 || ratio < unstackTicketRatio);
            if (!scoreRatioOk) {
                um = "(Advanced) score ratio is " + (ratio * 100.0).ToString("F0") + "% (" + usPoints.ToString("F0") + "/" + ruPoints.ToString("F0") + ") vs " + (unstackTicketRatio * 100.0).ToString("F0");
            }
            }
            if (ticketRatioOk && scoreRatioOk) {
            if (DebugLevel >= 6) DebugBalance("No unstacking needed: " + um);
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            }
            }

            // Handle Rout exemptions
            if (perMode.RoutPercentage > 100 && ratio >= perMode.RoutPercentage) {
            DebugBalance("No unstacking during a rout, skipping ^b" + name + "^n");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            /*
            Cases:
            1) Never unstacked before, timer is 0 and group count is 0
            2) Within a group, timer is 0 and group count is > 0 but < max
            3) Between groups, timer is > 0 and group count is 0
            */

            double nsis = NextSwapGroupInSeconds(perMode); // returns 0 for case 1 and case 2

            if (nsis > 0) {
            if (DebugLevel >= 6) DebugBalance("Too soon to do another unstack swap group, wait another " + nsis.ToString("F1") + " seconds!");
            IncrementTotal(); // no matching stat, reflect total deaths handled
            return;
            } else {
            fFullUnstackSwapTimestamp = DateTime.MinValue; // turn off timer
            }

            // Are the minimum number of players present to decide strong vs weak?
            if (!mustMove && balanceSpeed != Speed.Fast && fromList.Count < minPlayers) {
            DebugBalance("Not enough players in team to determine strong vs weak, skipping ^b" + name + "^n, ");
            fExemptRound = fExemptRound + 1;
            IncrementTotal();
            return;
            }

            // Otherwise, unstack!
            DebugBalance("^6Unstacking!^0 " + um);

            if (DebugLevel >= 6) {
            if (isStrong) {
            DebugBalance("Player ^b" + player.Name + "^n is strong: #" + (playerIndex+1) + " of " + fromList.Count + ", above #" + strongest + " at " + perMode.PercentOfTopOfTeamIsStrong.ToString("F0") + "%");
            } else {
            DebugBalance("Player ^b" + player.Name + "^n is weak: #" + (playerIndex+1) + " of " + fromList.Count + ", equal or below #" + strongest + " at " + perMode.PercentOfTopOfTeamIsStrong.ToString("F0") + "%");
            }
            }

            if (!loggedStats) {
            DebugBalance(GetPlayerStatsString(name));
            loggedStats = true;
            }

            MoveInfo moveUnstack = null;

            int origUnTeam = player.Team;
            String origUnName = GetTeamName(player.Team);
            String strength = "strong";

            if (lastMoveFrom != 0) {
            origUnTeam = lastMoveFrom;
            origUnName = GetTeamName(origUnTeam);
            }

            if (fUnstackState == UnstackState.Off) {
            // First swap
            DebugBalance("For ^b" + name + "^n, first swap of " + perMode.NumberOfSwapsPerGroup);
            fUnstackState = UnstackState.SwappedWeak;
            }

            switch (fUnstackState) {
            case UnstackState.SwappedWeak:
            // Swap strong to losing team
            if (isStrong) {
                // Don't move to same team
                if (player.Team == losingTeam) {
                    if (DebugLevel >= 6) DebugBalance("Skipping strong ^b" + name + "^n, don't move player to his own team!");
                    fExemptRound = fExemptRound + 1;
                    IncrementTotal();
                    return;
                }
                DebugBalance("Sending strong player ^0^b" + player.FullName + "^n^9 to losing team " + GetTeamName(losingTeam));
                moveUnstack = new MoveInfo(name, player.Tag, origUnTeam, origUnName, losingTeam, GetTeamName(losingTeam), YellDurationSeconds);
                toTeam = losingTeam;
                fUnstackState = UnstackState.SwappedStrong;
                if (EnableTicketLossRateLogging) UpdateTicketLossRateLog(now, losingTeam, 0);
            } else {
                DebugBalance("Skipping ^b" + name + "^n, don't move weak player to losing team (#" + (playerIndex+1) + " of " + fromList.Count + ", median " + (strongest) + ")");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            }
            break;
            case UnstackState.SwappedStrong:
            // Swap weak to winning team
            if (!isStrong) {
                // Don't move to same team
                if (player.Team == winningTeam) {
                    if (DebugLevel >= 6) DebugBalance("Skipping weak ^b" + name + "^n, don't move player to his own team!");
                    fExemptRound = fExemptRound + 1;
                    IncrementTotal();
                    return;
                }
                DebugBalance("Sending weak player ^0^b" + player.FullName + "^n^9 to winning team " + GetTeamName(winningTeam));
                moveUnstack = new MoveInfo(name, player.Tag, origUnTeam, origUnName, winningTeam, GetTeamName(winningTeam), YellDurationSeconds);
                toTeam = winningTeam;
                fUnstackState = UnstackState.SwappedWeak;
                strength = "weak";
                FinishedFullSwap(name, perMode); // updates group count
                if (EnableTicketLossRateLogging) UpdateTicketLossRateLog(now, 0, winningTeam);
            } else {
                DebugBalance("Skipping ^b" + name + "^n, don't move strong player to winning team (#" + (playerIndex+1) + " of " + fromList.Count + ", median " + (strongest) + ")");
                fExemptRound = fExemptRound + 1;
                IncrementTotal();
                return;
            }
            break;
            case UnstackState.Off:
            // fall thru
            default: return;
            }

            /* Move for unstacking */

            log = "^4^bUNSTACK^n^0 moving " + strength + " ^b" + player.FullName + "^n from " + moveUnstack.SourceName + " to " + moveUnstack.DestinationName + " because: " + um;
            log = (EnableLoggingOnlyMode) ? "^9(SIMULATING)^0 " + log : log;
            DebugWrite(log, 3);
            moveUnstack.For = MoveType.Unstack;
            moveUnstack.Format(this, ChatMovedToUnstack, false, false);
            moveUnstack.Format(this, YellMovedToUnstack, true, false);

            DebugWrite("^9" + moveUnstack, 8);

            if (player.LastMoveFrom == 0) player.LastMoveFrom = player.Team;
            StartMoveImmediate(moveUnstack, false);

            if (EnableLoggingOnlyMode) {
            // Simulate completion of move
            OnPlayerTeamChange(name, toTeam, 0);
            OnPlayerMovedByAdmin(name, toTeam, 0, false); // simulate reverse order
            }
            // no increment total, handled by unstacking move
        }
MULTIbalancer