/* ======================== 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 void ResetRound() { ClearTeams(); for (int i = 0; i < fTickets.Length; i++) { fTickets[i] = 0; } fRoundStartTimestamp = DateTime.Now; fFullUnstackSwapTimestamp = DateTime.MinValue; lock (fAllPlayers) { foreach (String name in fAllPlayers) { try { if (!fKnownPlayers.ContainsKey(name)) { ConsoleDebug("ResetRound: " + name + " not in fKnownPlayers"); continue; } PlayerModel m = null; lock (fKnownPlayers) { m = fKnownPlayers[name]; } m.ResetRound(); } catch (Exception e) { ConsoleException(e); } } } fBalancedRound = 0; fUnstackedRound = 0; fUnswitchedRound = 0; fExcludedRound = 0; fExemptRound = 0; fFailedRound = 0; fTotalRound = 0; fReassignedRound = 0; fUnstackState = UnstackState.Off; fRushStage = 0; fRushPrevAttackerTickets = 0; fTimeOutOfJoint = 0; fRoundsEnabled = fRoundsEnabled + 1; fGrandTotalQuits = fGrandTotalQuits + fTotalQuits; fTotalQuits = 0; fGrandRageQuits = fGrandRageQuits + fRageQuits; fRageQuits = 0; fLastBalancedTimestamp = DateTime.MinValue; ResetAverageTicketLoss(); fTicketLossHistogram.Clear(); }
/* Constructor */ public MULTIbalancer() { /* Private members */ fIsEnabled = false; fFinalizerActive = false; fPluginState = PluginState.Disabled; fGameState = GameState.Unknown; fServerInfo = null; fRefreshCommand = false; fServerUptime = 0; fServerCrashed = false; fDebugScramblerBefore = new List<PlayerModel>[2]{new List<PlayerModel>(), new List<PlayerModel>()}; fDebugScramblerAfter = new List<PlayerModel>[2]{new List<PlayerModel>(), new List<PlayerModel>()}; fDebugScramblerStartRound = new List<PlayerModel>[2]{new List<PlayerModel>(), new List<PlayerModel>()}; fBalancedRound = 0; fUnstackedRound = 0; fUnswitchedRound = 0; fExcludedRound = 0; fExemptRound = 0; fFailedRound = 0; fTotalRound = 0; fBalanceIsActive = false; fRoundsEnabled = 0; fGrandTotalQuits = 0; fGrandRageQuits = 0; fTotalQuits = 0; fRageQuits = 0; fMoveThread = null; fFetchThread = null; fListPlayersThread = null; fScramblerThread = null; fTimerThread = null; fModeToSimple = new Dictionary<String,String>(); fEasyTypeDict = new Dictionary<int, Type>(); fEasyTypeDict.Add(0, typeof(int)); fEasyTypeDict.Add(1, typeof(Int16)); fEasyTypeDict.Add(2, typeof(Int32)); fEasyTypeDict.Add(3, typeof(Int64)); fEasyTypeDict.Add(4, typeof(float)); fEasyTypeDict.Add(5, typeof(long)); fEasyTypeDict.Add(6, typeof(String)); fEasyTypeDict.Add(7, typeof(string)); fEasyTypeDict.Add(8, typeof(double)); fBoolDict = new Dictionary<int, Type>(); fBoolDict.Add(0, typeof(Boolean)); fBoolDict.Add(1, typeof(bool)); fListStrDict = new Dictionary<int, Type>(); fListStrDict.Add(0, typeof(String[])); fPerMode = new Dictionary<String,PerModeSettings>(); fAllPlayers = new List<String>(); fKnownPlayers = new Dictionary<String, PlayerModel>(); fTeam1 = new List<PlayerModel>(); fTeam2 = new List<PlayerModel>(); fTeam3 = new List<PlayerModel>(); fTeam4 = new List<PlayerModel>(); fUnassigned = new List<String>(); fRoundStartTimestamp = DateTime.MinValue; fRoundOverTimestamp = DateTime.MinValue; fListPlayersTimestamp = DateTime.MinValue; fFullUnstackSwapTimestamp = DateTime.MinValue; fLastValidationTimestamp = DateTime.MinValue; fListPlayersQ = new Queue<DelayedRequest>(); fPendingTeamChange = new Dictionary<String,int>(); fMoving = new Dictionary<String, MoveInfo>(); fMoveQ = new Queue<MoveInfo>(); fReassigned = new List<String>(); fReservedSlots = new List<String>(); fTickets = new int[5]{0,0,0,0,0}; fFriendlyMaps = new Dictionary<String,String>(); fFriendlyModes = new Dictionary<String,String>(); fMaxTickets = -1; fRushMaxTickets = -1; fLastBalancedTimestamp = DateTime.MinValue; fEnabledTimestamp = DateTime.MinValue; fFinalStatus = null; fIsFullRound = false; fUnstackState = UnstackState.Off; fLastMsg = null; fRushStage = 0; fRushPrevAttackerTickets = 0; fRushAttackerStageLoss = 0; fRushAttackerStageSamples = 0; fMoveStash = new List<MoveInfo>(); fLastVersionCheckTimestamp = DateTime.MinValue; fTimeOutOfJoint = 0; fUnstackGroupCount = 0; fPriorityFetchQ = new PriorityQueue(this); fIsCacheEnabled = false; fScramblerLock = new DelayedRequest(); fWinner = 0; fUpdateThreadLock = new DelayedRequest(); fLastServerInfoTimestamp = DateTime.Now; fStageInProgress = false; fHost = String.Empty; fPort = String.Empty; fRushMap3Stages = new List<String>(new String[6]{"MP_007", "XP4_Quake", "XP5_002", "MP_012", "XP4_Rubble", "MP_Damage"}); fRushMap5Stages = new List<String>(new String[6]{"MP_013", "XP3_Valley", "MP_017", "XP5_001", "MP_Prison", "MP_Siege"}); fGroupAssignments = new int[5]{0,0,0,0,0}; fDispersalGroups = new List<String>[5]{null, new List<String>(), new List<String>(), new List<String>(), new List<String>()}; fNeedPlayerListUpdate = false; fFriends = new Dictionary<int, List<String>>(); fAllFriends = new List<String>(); fWhileScrambling = false; fExtrasLock = new DelayedRequest(); fExtraNames = new List<String>(); fGotLogin = false; fDebugScramblerSuspects = new Dictionary<String,String>(); fTimerRequestList = new List<DelayedRequest>(); fAverageTicketLoss = new Queue<double>[3]{null, new Queue<double>(), new Queue<double>()}; fTicketLossHistogram = new Histogram(); /* Settings */ /* ===== SECTION 0 - Presets ===== */ SettingsVersion = 1; Preset = PresetItems.Standard; EnableUnstacking = false; EnableSettingsWizard = false; WhichMode = "Conquest Large"; MetroIsInMapRotation = false; MaximumPlayersForMode = 64; LowestMaximumTicketsForMode = 300; HighestMaximumTicketsForMode = 400; PreferredStyleOfBalancing = PresetItems.Standard; ApplySettingsChanges = false; /* ===== SECTION 1 - Settings ===== */ DebugLevel = 2; MaximumServerSize = 64; EnableBattlelogRequests = true; MaximumRequestRate = 10; // in 20 seconds WaitTimeout = 30; // seconds WhichBattlelogStats = BattlelogStats.ClanTagOnly; MaxTeamSwitchesByStrongPlayers = 1; MaxTeamSwitchesByWeakPlayers = 2; UnlimitedTeamSwitchingDuringFirstMinutesOfRound = 5.0; Enable2SlotReserve = false; EnablerecruitCommand = false; EnableWhitelistingOfReservedSlotsList = true; Whitelist = new String[] {DEFAULT_LIST_ITEM}; fSettingWhitelist = new List<String>(Whitelist); DisperseEvenlyList = new String[] {DEFAULT_LIST_ITEM}; fSettingDisperseEvenlyList = new List<String>(DisperseEvenlyList); FriendsList = new String[] {DEFAULT_LIST_ITEM}; fSettingFriendsList = new List<String>(); SecondsUntilAdaptiveSpeedBecomesFast = 3*60; // 3 minutes default EnableInGameCommands = true; /* ===== SECTION 2 - Exclusions ===== */ OnWhitelist = true; OnFriendsList = false; ApplyFriendsListToTeam = false; TopScorers = true; SameClanTagsInSquad = true; SameClanTagsInTeam = false; SameClanTagsForRankDispersal = false; LenientRankDispersal = false; MinutesAfterJoining = 5; MinutesAfterBeingMoved = 90; // 1.5 hours JoinedEarlyPhase = true; JoinedMidPhase = true; JoinedLatePhase = false; /* ===== SECTION 3 - Round Phase & Population Settings ===== */ EarlyPhaseTicketPercentageToUnstack = new double[3] { 0,120,120}; MidPhaseTicketPercentageToUnstack = new double[3] { 0,120,120}; LatePhaseTicketPercentageToUnstack = new double[3] { 0, 0, 0}; EnableTicketLossRateLogging = false; SpellingOfSpeedNamesReminder = Speed.Click_Here_For_Speed_Names; EarlyPhaseBalanceSpeed = new Speed[3] { Speed.Fast, Speed.Adaptive, Speed.Adaptive}; MidPhaseBalanceSpeed = new Speed[3] { Speed.Fast, Speed.Adaptive, Speed.Adaptive}; LatePhaseBalanceSpeed = new Speed[3] { Speed.Stop, Speed.Stop, Speed.Stop}; /* ===== SECTION 4 - Scrambler ===== */ OnlyOnNewMaps = true; // false means scramble every round OnlyOnFinalTicketPercentage = 120; // 0 means scramble regardless of final score ScrambleBy = DefineStrong.RoundScore; KeepSquadsTogether = true; KeepClanTagsInSameTeam = true; KeepFriendsInSameTeam = false; DivideBy = DivideByChoices.None; ClanTagToDivideBy = String.Empty; DelaySeconds = 50; /* ===== SECTION 5 - Messages ===== */ QuietMode = false; // false: chat is global, true: chat is private. Yells are always private YellDurationSeconds = 10; BadBecauseMovedByBalancer = "autobalance moved you to the %toTeam% team"; BadBecauseWinningTeam = "switching to the winning team is not allowed"; BadBecauseBiggestTeam = "switching to the biggest team is not allowed"; BadBecauseRank = "this server splits Colonel 100's between teams"; BadBecauseDispersalList = "you're on the list of players to split between teams"; ChatMovedForBalance = "*** MOVED %name% for balance ..."; YellMovedForBalance = "Moved %name% for balance ..."; ChatMovedToUnstack = "*** MOVED %name% to unstack teams ..."; YellMovedToUnstack = "Moved %name% to unstack teams ..."; ChatDetectedBadTeamSwitch = "%name%, you can't switch to team %fromTeam%: %reason%, sending you back ..."; YellDetectedBadTeamSwitch = "You can't switch to the %fromTeam% team: %reason%, sending you back!"; ChatDetectedGoodTeamSwitch = "%name%, thanks for helping out the %toTeam% team!"; YellDetectedGoodTeamSwitch = "Thanks for helping out the %toTeam% team!"; ChatAfterUnswitching = "%name%, please stay on the %toTeam% team for the rest of this round"; YellAfterUnswitching = "Please stay on the %toTeam% team for the rest of this round"; /* ===== SECTION 6 - Unswitcher ===== */ EnableImmediateUnswitch = true; ForbidSwitchingAfterAutobalance = UnswitchChoice.Always; ForbidSwitchingToWinningTeam = UnswitchChoice.Always; ForbidSwitchingToBiggestTeam = UnswitchChoice.Always; ForbidSwitchingAfterDispersal = UnswitchChoice.Always; /* ===== SECTION 7 - TBD ===== */ /* ===== SECTION 8 - Per-Mode Settings ===== */ /* ===== SECTION 9 - Debug Settings ===== */ ShowInLog = INVALID_NAME_TAG_GUID; ShowCommandInLog = String.Empty; LogChat = true; EnableLoggingOnlyMode = false; EnableExternalLogging = false; ExternalLogSuffix = "_mb.log"; }