/* ======================== CORE ENGINE ============================= */
        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();
            String log = String.Empty;

            /* Sanity checks */

            if (fServerInfo == null) {
            return;
            }

            int totalPlayerCount = this.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;
            }

            int floorPlayers = 6;
            if (totalPlayerCount < floorPlayers) {
            if (DebugLevel >= 6) DebugBalance("Not enough players in server, minimum is " + floorPlayers);
            CheckDeativateBalancer("Not enough players");
            return;
            }

            if (totalPlayerCount > 0) {
            AnalyzeTeams(out diff, out ascendingSize, out descendingTickets, out biggestTeam, out smallestTeam, out winningTeam, out losingTeam);
            } else {
            CheckDeativateBalancer("Empty");
            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();
            }

            /* 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;
            }

            /* Check dispersals */

            bool mustMove = false;
            bool lenient = false;
            int maxDispersalMoves = 2;
            bool isDisperseByRank = IsRankDispersal(player);
            bool isDisperseByList = IsDispersal(player, false);
            if (isDisperseByList) {
            String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
            DebugBalance("ON MUST MOVE LIST ^b" + player.FullName + "^n T:" + player.Team + ", disperse evenly enabled, " + dispersalMode);
            mustMove = true;
            lenient = !perMode.EnableStrictDispersal; // the opposite of strict is lenient
            maxDispersalMoves = (lenient) ? 1 : 2;
            } else if (isDisperseByRank) {
            DebugBalance("ON MUST MOVE LIST ^b" + name + "^n T:" + player.Team + ", Rank " + player.Rank + " >= " + perMode.DisperseEvenlyByRank);
            mustMove = true;
            lenient = LenientRankDispersal;
            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;
            }
            }
            String orSlow = (balanceSpeed == Speed.Slow) ? " or speed is Slow" : String.Empty;

            // Do not disperse mustMove players if speed is Stop or Phase is Late
            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 && 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;
            }

            /* 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;
            }

            /* 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;
            }
            }
            }
            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 = DateTime.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 first/partial round
            if (!player.TagVerified && player.Rounds <= 1) {
            if (DebugLevel >= 7) DebugBalance("Skipping ^b" + player.Name + "^n, clan tag not verified yet");
            // Don't count this as an excemption
            // Don't increment the total
            return;
            }

            // Exclude if in squad with same tags
            if ((!mustMove || lenient) && SameClanTagsInSquad) {
            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) {
            int cmt =  CountMatchingTags(player, Scope.SameTeam);
            if (cmt >= 5) {
            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;
            }
            }
            }

            // 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 >= 8) 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;
            }

            if ((fBalanceIsActive || mustMove) && toTeam != 0) {
            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 + "(US) vs " + fTeam2.Count + "(RU)";
            }
            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 */

            // 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 and clan tag
            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));
            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;
            }
            }

            double ratio = 1;
            double t1Tickets = 0;
            double t2Tickets = 0;
            if (IsCTF()) {
            // 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 = "Ticket ratio " + (ratio*100.0).ToString("F0") + " 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 " + TeamName(winningTeam));
            IncrementTotal();
            return;
            }
            }

            if (unstackTicketRatio == 0 || ratio < unstackTicketRatio) {
            if (DebugLevel >= 6) DebugBalance("No unstacking needed: " + um);
            IncrementTotal(); // no matching stat, reflect total deaths handled
            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));
                toTeam = losingTeam;
                fUnstackState = UnstackState.SwappedStrong;
                if (EnableTicketLossRateLogging) UpdateTicketLossRateLog(DateTime.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));
                toTeam = winningTeam;
                fUnstackState = UnstackState.SwappedWeak;
                strength = "weak";
                FinishedFullSwap(name, perMode); // updates group count
                if (EnableTicketLossRateLogging) UpdateTicketLossRateLog(DateTime.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
        }
        private bool CheckTeamSwitch(String name, int toTeam)
        {
            if (fPluginState != PluginState.Active || fGameState != GameState.Playing) return false;

            // Get model
            PlayerModel player = GetPlayer(name);
            if (player == null) return false;
            bool bogusMove = false;
            int lastMoveTo = 0;
            int lastMoveFrom = player.LastMoveFrom;

            // Same team?
            if (toTeam == player.Team) {
            /*
            This could happen with the following sequence of actions:
            + Player died and was moved from 1 to 2 for balance immediately, spawn messages set
            + While still dead, player switches himself back to 1 before respawning
            + All of this happens before a listPlayers refresh, so the model still thinks he is in team 1
            We have to detect that the switch is not to the intended team and fix everything up.
            */
            if (player.LastMoveTo != 0 && player.LastMoveTo != toTeam) {
            DebugUnswitch("Player team switch: ^b" + name + "^n trying to switch to " + GetTeamName(toTeam) + " during a plugin move to " + GetTeamName(player.LastMoveTo));
            bogusMove = true;
            lastMoveTo = player.LastMoveTo;
            player.LastMoveTo = 0;
            DebugUnswitch("Ovewriting previous chat message for ^b" + name + "^n: " + player.SpawnChatMessage);
            player.SpawnChatMessage = String.Empty;
            player.SpawnYellMessage = String.Empty;
            } else {
            DebugUnswitch("Player team switch: ^b" + name + "^n, player model already updated to " + GetTeamName(toTeam) + " team");
            return true;
            }
            } else {
            DebugUnswitch("Player team switch: ^b" + name + "^n from " + GetTeamName(player.Team) + " team to " + GetTeamName(toTeam) + " team");
            }

            // Check if move already in progress for this player and abort it
            bool sendAbortMessage = false;
            lock (fMoveStash) {
            if (fMoveStash.Count > 0) {
             // list only ever has one item
            if (fMoveStash[0].Name == name) {
                fMoveStash.Clear();
            }
            }
            }
            if (sendAbortMessage) {
            DebugUnswitch("ABORTED (by move stash): abort previous move by ^b" + name);
            sendAbortMessage = false;
            }

            // Whitelisted?
            if (OnWhitelist) {
            if (CheckWhitelist(player, WL_SWITCH)) {
            DebugUnswitch("ALLOWED: On whitelist: ^b" + name);
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            }

            // Check forbidden cases
            PerModeSettings perMode = GetPerModeSettings();
            bool isSQDM = IsSQDM();
            bool isDispersal = IsDispersal(player, false);
            bool isRank = IsRankDispersal(player);
            bool forbidden = (((isDispersal || isRank) && Forbid(perMode, ForbidSwitchingAfterDispersal)) || (player.MovesByMBRound > 0 && !isSQDM && Forbid(perMode, ForbidSwitchingAfterAutobalance)));

            // Unlimited time?
            if (!forbidden && UnlimitedTeamSwitchingDuringFirstMinutesOfRound > 0 && GetTimeInRoundMinutes() < UnlimitedTeamSwitchingDuringFirstMinutesOfRound) {
            DebugUnswitch("ALLOWED: Time in round " + GetTimeInRoundMinutes().ToString("F0") + " < " + UnlimitedTeamSwitchingDuringFirstMinutesOfRound.ToString("F0"));
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }

            // Helps?
            int diff = 0;
            int biggestTeam = 0;
            int smallestTeam = 0;
            int winningTeam = 0;
            int losingTeam = 0;
            int[] ascendingSize = null;
            int[] descendingTickets = null;
            int fromTeam = player.Team;
            MoveInfo move = null;
            bool toLosing = false;
            bool toSmallest = false;

            /*
            A player that was previously moved by the plugin is forbidden from moving to any
            other team by their own initiative for the rest of the round, unless this is
            SQDM mode. In SQDM, if a player is moved from A to B and then later decides
            to move to C, the losing team, that is allowed. Even in SQDM, though, no player
            is allowed to move to the winning team.

            All dispersal players are forbidden from moving themselves.
            */

            AnalyzeTeams(out diff, out ascendingSize, out descendingTickets, out biggestTeam, out smallestTeam, out winningTeam, out losingTeam);

            int iFrom = 0;
            int iTo = 0;

            if (isSQDM) {
            // Moving to any team with fewer tickets is encouraged
            for (int i = 0; i < descendingTickets.Length; ++i) {
            if (fromTeam == descendingTickets[i]) iFrom = i;
            if (toTeam == descendingTickets[i]) iTo = i;
            }
            toLosing = (iTo > iFrom);
            } else {
            toLosing = (toTeam == losingTeam);
            }

            // Trying to switch to losing team?
            if (!forbidden && toLosing && toTeam != biggestTeam) {
            move = new MoveInfo(player.Name, player.Tag, fromTeam, GetTeamName(fromTeam), toTeam, GetTeamName(toTeam));
            move.Format(this, ChatDetectedGoodTeamSwitch, false, true);
            move.Format(this, YellDetectedGoodTeamSwitch, true, true);
            DebugUnswitch("ALLOWED: Team switch to losing team ^b: " + name);
            SetSpawnMessages(name, move.ChatBefore, move.YellBefore, false);
            CheckAbortMove(name);
            return true;
            }

            if (isSQDM) {
            // Moving to any team with fewer players is encouraged
            for (int i = 0; i < ascendingSize.Length; ++i) {
            if (fromTeam == ascendingSize[i]) iFrom = i;
            if (toTeam == ascendingSize[i]) iTo = i;
            }
            toSmallest = (iTo < iFrom);
            } else {
            toSmallest = (toTeam == smallestTeam);
            }

            // Trying to switch to smallest team?
            if (!forbidden && toSmallest && toTeam != winningTeam) {
            move = new MoveInfo(player.Name, player.Tag, fromTeam, GetTeamName(fromTeam), toTeam, GetTeamName(toTeam));
            move.Format(this, ChatDetectedGoodTeamSwitch, false, true);
            move.Format(this, YellDetectedGoodTeamSwitch, true, true);
            DebugUnswitch("ALLOWED: Team switch to smallest team ^b: " + name);
            SetSpawnMessages(name, move.ChatBefore, move.YellBefore, false);
            CheckAbortMove(name);
            return true;
            }

            // Adjust for SQDM
            if (isSQDM && fServerInfo != null) {
            if (GetPopulation(perMode, true) == Population.Low) {
            // Allow team switch to any team except biggest and winning
            if (!forbidden && toTeam != biggestTeam && toTeam != winningTeam) {
                DebugUnswitch("ALLOWED: SQDM Low population and not switching to biggest or winning team: ^b" + name);
                SetSpawnMessages(name, String.Empty, String.Empty, false);
                CheckAbortMove(name);
                return true;
            }
            }
            }

            // Allow if ticket/point difference is less than allowed margin
            double win = 0;
            double lose = 0;
            double margin = 100;
            if (IsCTF()) {
            win = GetTeamPoints(winningTeam);
            if (win == 0) win = 1;
            lose = GetTeamPoints(losingTeam);
            if (lose == 0) lose = 1;
            margin = ((win > lose) ? win/lose : lose/win);
            // margin is 110%
            if (!forbidden && (margin * 100) <= 110) {
            DebugUnswitch("ALLOWED: CTF move by ^b" + name + "^n because margin is only " + (margin*100).ToString("F0") + "%");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            } else {
            win = fTickets[winningTeam];
            if (win == 0) win = 1;
            lose = fTickets[losingTeam];
            if (lose == 0) lose = 1;
            margin = ((win > lose) ? win/lose : lose/win);
            // margin is 105%
            if (!forbidden && (margin * 100) <= 105) {
            DebugUnswitch("ALLOWED: move by ^b" + name + "^n because margin is only " + (margin*100).ToString("F0") + "%");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            }

            // Otherwise, do not allow the team switch
            int origTeam = player.Team;
            String origName = GetTeamName(player.Team);

            if (lastMoveFrom != 0 && toTeam != lastMoveFrom) {
            DebugUnswitch("Setting toTeam from " + GetTeamName(toTeam) + " to original LastMoveFrom = " + GetTeamName(lastMoveFrom));
            toTeam = lastMoveFrom;
            }

            if (bogusMove) {
            origTeam = lastMoveTo;
            origName = GetTeamName(lastMoveTo);
            }

            // select forbidden message from: moved by autobalance, moved to unstack, dispersal, ...
            String badChat = ChatDetectedBadTeamSwitch;
            String badYell = YellDetectedBadTeamSwitch;

            ForbidBecause why = ForbidBecause.None;

            if (player.MovesByMBRound > 0 && !isSQDM) {
            why = ForbidBecause.MovedByBalancer;
            if (!Forbid(perMode, ForbidSwitchingAfterAutobalance)) {
            DebugUnswitch("ALLOWED: move by ^b" + name + "^n because ^bForbid Switch After Autobalance^n is False");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            } else if (toTeam == winningTeam) {
            why = ForbidBecause.ToWinning;
            if (!Forbid(perMode, ForbidSwitchingToWinningTeam)) {
            DebugUnswitch("ALLOWED: move by ^b" + name + "^n because ^bForbid Switch To Winning Team^n is False");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            } else if (toTeam == biggestTeam) {
            why = ForbidBecause.ToBiggest;
            if (!Forbid(perMode, ForbidSwitchingToBiggestTeam)) {
            DebugUnswitch("ALLOWED: move by ^b" + name + "^n because ^bForbid Switch To Biggest Team^n is False");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            } else if (isDispersal) {
            why = ForbidBecause.DisperseByList;
            if (!Forbid(perMode, ForbidSwitchingAfterDispersal)) {
            DebugUnswitch("ALLOWED: move by ^b" + name + "^n because ^bForbid Switch After Dispersal^n is False");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            } else if (isRank) {
            why = ForbidBecause.DisperseByRank;
            if (!Forbid(perMode, ForbidSwitchingAfterDispersal)) {
            DebugUnswitch("ALLOWED: move by ^b" + name + "^n because ^bForbid Switch After Dispersal^n is False");
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            CheckAbortMove(name);
            return true;
            }
            }

            if (toTeam == origTeam) {
            ConsoleDebug("CheckTeamSwitch: ^b" + name + "^n, can't forbid unswitch to same team " + GetTeamName(toTeam) + "?");
            return true;
            }

            // Tried to switch toTeam from origTeam, so moving from toTeam back to origTeam
            move = new MoveInfo(name, player.Tag, toTeam, GetTeamName(toTeam), origTeam, origName);
            move.For = MoveType.Unswitch;
            move.Because = why;
            move.Format(this, badChat, false, true);
            move.Format(this, badYell, true, true);
            move.Format(this, ChatAfterUnswitching, false, false);
            move.Format(this, YellAfterUnswitching, true, false);
            player.LastMoveFrom = 0;

            if (DebugLevel >= 8) DebugUnswitch(move.ToString());

            if (isSQDM || !EnableImmediateUnswitch) {
            // Delay action until after the player spawns
            DebugUnswitch("FORBIDDEN: delaying unswitch action until spawn of ^b" + name + "^n from " + move.SourceName + " back to " + move.DestinationName);

            if (player.DelayedMove != null) {
            CheckAbortMove(name);
            }
            player.DelayedMove = move;

            if (!String.IsNullOrEmpty(player.SpawnChatMessage)) {
            DebugUnswitch("IGNORED: previously delayed spawn message for ^b" + name + "^n: " + player.SpawnChatMessage);
            SetSpawnMessages(name, String.Empty, String.Empty, false);
            }
            } else {
            // Do the move immediately
            DebugUnswitch("FORBIDDEN: immediately unswitch ^b" + name + "^n from " + move.SourceName + " back to " + move.DestinationName);
            String log = "^4^bUNSWITCHING^n^0 ^b" + player.FullName + "^n from " + move.SourceName + " back to " + move.DestinationName;
            log = (EnableLoggingOnlyMode) ? "^9(SIMULATING)^0 " + log : log;
            DebugWrite(log, 3);
            StartMoveImmediate(move, true);
            }

            return false;
        }
        private void FastBalance(String trigger)
        {
            /* Useful variables */

            PlayerModel player = null;
            String simpleMode = String.Empty;
            PerModeSettings perMode = GetPerModeSettings();
            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;
            String log = String.Empty;
            int level = 6;
            int adj = 1;

            /* Sanity checks */

            if (fServerInfo == null) {
            return;
            }

            if (fGameState != GameState.Playing) {
            return;
            }

            if (IsNonBalancingMode()) {
            return;
            }

            if (trigger.Contains("Kill")) {
            level = 8;
            adj = 0;
            }

            if (fLastFastMoveTimestamp != DateTime.MinValue && now.Subtract(fLastFastMoveTimestamp).TotalSeconds < 25) {
            if (DebugLevel >= (level + adj)) DebugFast("Too soon to check for fast balance again, wait another " + (25.0 - now.Subtract(fLastFastMoveTimestamp).TotalSeconds).ToString("F1") + " seconds");
            return;
            }

            Speed balanceSpeed = GetBalanceSpeed(perMode);

            if (balanceSpeed == Speed.Stop) {
            if (DebugLevel >= (level + adj)) DebugFast("Speed is Stop, fast balance check skipped. " + trigger + " was trigger"); // DebugBalance on purpose to get repeat filtering
            return;
            }

            int totalPlayerCount = TotalPlayerCount();

            if (DebugLevel >= (level + adj)) DebugFast(trigger + "Checking if fast balance is needed, " + totalPlayerCount + " players");

            if (totalPlayerCount >= (MaximumServerSize-1)) {
            if (DebugLevel >= (level + adj)) DebugFast("Server is full, no balancing or unstacking will be attempted!");
            return;
            }

            if (totalPlayerCount >= (perMode.MaxPlayers-1)) {
            if (DebugLevel >= (level + adj)) DebugFast("Server is full by per-mode Max Players, no balancing or unstacking will be attempted!");
            return;
            }

            int floorPlayers = (perMode.EnableLowPopulationAdjustments) ? 4 : 5;
            if (totalPlayerCount < floorPlayers) {
            if (DebugLevel >= (level + adj)) DebugFast("Not enough players in server, minimum is " + floorPlayers);
            return;
            }

            if (totalPlayerCount > 0) {
            AnalyzeTeams(out diff, out ascendingSize, out descendingTickets, out biggestTeam, out smallestTeam, out winningTeam, out losingTeam);
            }

            // Adjust speed to Fast?
            if (balanceSpeed != Speed.Fast) {
            if (diff > MaxFastDiff()) {
            balanceSpeed = Speed.Fast;
            }
            }
            if (balanceSpeed != Speed.Fast || diff <= MaxFastDiff()) {
            if (diff > 1 && DebugLevel >= level) DebugFast("Fast balance not active, diff is only " + diff + ", requires more than " + MaxFastDiff());
            return;
            }

            // Prepare for player selection
            if (smallestTeam < 1) {
            DebugFast("Cannot determine smallest team: " + smallestTeam);
            return;
            }
            List<PlayerModel> big = new List<PlayerModel>();
            List<PlayerModel> tmp = GetTeam(biggestTeam);
            if (tmp == null || tmp.Count < 1 || biggestTeam < 1) {
            DebugFast("Cannot determine biggest team: " + biggestTeam);
            return;
            }
            big.AddRange(tmp);
            tmp = new List<PlayerModel>();
            foreach (PlayerModel p in big) {
            if (p == null) continue;
            if (fGameVersion != GameVersion.BF3 && p.Role >= 0 && p.Role < ROLE_NAMES.Length && p.Role != ROLE_PLAYER) {
            if (DebugLevel >= 7) DebugFast("Excluding ^b" + p.Name + "^n, role is " + ROLE_NAMES[p.Role]);
            continue;
            } else if (OnWhitelist && CheckWhitelist(p, WL_BALANCE)) { // exclude if on whitelist
            if (DebugLevel >= 7) DebugFast("Excluding ^b" + p.FullName + "^n: on Whitelist");
            continue;
            } else if (p.MovedByMBTimestamp != DateTime.MinValue) { // exclude if moved recently
            double mins = now.Subtract(p.MovedByMBTimestamp).TotalMinutes;
            if (mins < MinutesAfterBeingMoved) {
                if (DebugLevel >= 7) DebugFast("Excluding ^b" + p.Name + "^n: last move was " + mins.ToString("F0") + " minutes ago, less than required " + MinutesAfterBeingMoved.ToString("F0") + " minutes");
                continue;
            } else {
                // reset
                p.MovedByMBTimestamp = DateTime.MinValue;
            }
            }

            tmp.Add(p);
            }
            big = tmp;

            // Select player
            if (DebugLevel >= 7) ConsoleDebug("FastBalance selecting player");
            if (big.Count < 1) {
            if (DebugLevel >= level) DebugFast("All players on " + GetTeamName(biggestTeam) + " team were excluded, unable to select the " + SelectFastBalanceBy + " player");
            return;
            }
            String kstat = String.Empty;
            switch (SelectFastBalanceBy) {
            case ForceMove.Weakest: {
            switch (perMode.DetermineStrongPlayersBy) {
                case DefineStrong.RoundScore:
                    big.Sort(DescendingRoundScore);
                    kstat = "S";
                    break;
                case DefineStrong.RoundSPM:
                    big.Sort(DescendingRoundSPM);
                    kstat = "SPM";
                    break;
                case DefineStrong.RoundKills:
                    big.Sort(DescendingRoundKills);
                    kstat = "K";
                    break;
                case DefineStrong.RoundKDR:
                    big.Sort(DescendingRoundKDR);
                    kstat = "KDR";
                    break;
                case DefineStrong.PlayerRank:
                    big.Sort(DescendingPlayerRank);
                    kstat = "R";
                    break;
                case DefineStrong.RoundKPM:
                    big.Sort(DescendingRoundKPM);
                    kstat = "KPM";
                    break;
                case DefineStrong.BattlelogSPM:
                    big.Sort(DescendingSPM);
                    kstat = "bSPM";
                    break;
                case DefineStrong.BattlelogKDR:
                    big.Sort(DescendingKDR);
                    kstat = "bKDR";
                    break;
                case DefineStrong.BattlelogKPM:
                    big.Sort(DescendingKPM);
                    kstat = "bKPM";
                    break;
                default:
                    big.Sort(DescendingRoundScore);
                    break;
            }

            // Select weakest
            player = big[big.Count-1];
            DebugFast("Selected WEAKEST player ^b" + player.FullName + "^n, " + kstat + ": " + GetPlayerStat(player, perMode.DetermineStrongPlayersBy).ToString("F1"));
            break;
            }

            case ForceMove.Newest: {
            // Descending by elapsed join time
            big.Sort(delegate(PlayerModel lhs, PlayerModel rhs) {
                if (lhs == null) {
                    return ((rhs == null) ? 0 : -1);
                } else if (rhs == null) {
                    return ((lhs == null) ? 0 : 1);
                }
                double lTime = GetPlayerJoinedTimeSpan(lhs).TotalSeconds;
                double rTime = GetPlayerJoinedTimeSpan(rhs).TotalSeconds;
                if (lTime < rTime) return 1;
                if (lTime > rTime) return -1;
                return 0;
            });
            // Select newest
            player = big[big.Count-1];
            DebugFast("Selected NEWEST player ^b" + player.FullName + "^n, joined " + GetPlayerJoinedTimeSpan(player).TotalMinutes.ToString("F1") + " minutes ago");
            break;
            }

            case ForceMove.Random: {
            Random rnd = new Random();
            player = big[rnd.Next(big.Count)];
            DebugFast("Selected RANDOM player ^b" + player.FullName);
            break;
            }
            }

            /* Move for fast balance */

            if (DebugLevel >= 7) ConsoleDebug("Move for fast balance");

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

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

            MoveInfo move = new MoveInfo(player.Name, player.Tag, origTeam, origName, smallestTeam, GetTeamName(smallestTeam), 0);
            move.For = MoveType.Balance;
            // private message to player before getting killed
            move.Format(this, ChatMovedForBalance, false, true);
            move.Format(this, YellMovedForBalance, true, true);
            // regular message for after move
            move.Format(this, ChatMovedForBalance, false, false);
            move.Format(this, YellMovedForBalance, true, false);
            move.Fast = true;
            String why = "because difference is " + diff;
            log = "^4^bFAST BALANCE^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;
            fLastFastMoveTimestamp = DateTime.Now;

            KillAndMoveAsync(move);

            /*
            if (EnableLoggingOnlyMode) {
            // Simulate completion of move
            OnPlayerTeamChange(name, toTeam, 0);
            OnPlayerMovedByAdmin(name, toTeam, 0, false); // simulate reverse order
            }
            */
        }