private void BalanceAndUnstack(String name)
{
/* Useful variables */
PlayerModel player = null;
String simpleMode = String.Empty;
PerModeSettings perMode = null;
bool isStrong = false; // this player
int winningTeam = 0;
int losingTeam = 0;
int biggestTeam = 0;
int smallestTeam = 0;
int[] ascendingSize = null;
int[] descendingTickets = null;
String strongMsg = String.Empty;
int diff = 0;
DateTime now = DateTime.Now;
bool needsBalancing = false;
bool loggedStats = false;
bool isSQDM = IsSQDM();
bool isNonBalancingMode = IsNonBalancingMode();
String log = String.Empty;
/* Sanity checks */
if (fServerInfo == null) {
return;
}
int totalPlayerCount = TotalPlayerCount();
if (DebugLevel >= 8) DebugBalance("BalanceAndUnstack(^b" + name + "^n), " + totalPlayerCount + " players");
if (totalPlayerCount >= (MaximumServerSize-1)) {
if (DebugLevel >= 6) DebugBalance("Server is full, no balancing or unstacking will be attempted!");
IncrementTotal(); // no matching stat, reflect total deaths handled
CheckDeativateBalancer("Full");
return;
}
if (totalPlayerCount < 4) {
if (DebugLevel >= 6) DebugBalance("Server is in warmup, less than 4 players");
CheckDeativateBalancer("Warmup");
return;
}
if (totalPlayerCount > 0) {
AnalyzeTeams(out diff, out ascendingSize, out descendingTickets, out biggestTeam, out smallestTeam, out winningTeam, out losingTeam);
} else {
CheckDeativateBalancer("Empty");
return;
}
if (EnableAdminKillForFastBalance && !isNonBalancingMode && diff > MaxFastDiff()) {
DebugBalance("Fast balance is enabled and active, skipping normal balancing and unstacking");
CheckDeativateBalancer("Fast balance is active");
return;
}
/* Pre-conditions */
player = GetPlayer(name);
if (player == null) {
CheckDeativateBalancer("Unknown player " + name);
return;
}
if (!fModeToSimple.TryGetValue(fServerInfo.GameMode, out simpleMode)) {
DebugBalance("Unknown game mode: " + fServerInfo.GameMode);
simpleMode = fServerInfo.GameMode;
}
if (String.IsNullOrEmpty(simpleMode)) {
DebugBalance("Simple mode is null: " + fServerInfo.GameMode);
CheckDeativateBalancer("Unknown mode");
return;
}
if (!fPerMode.TryGetValue(simpleMode, out perMode)) {
DebugBalance("No per-mode settings for " + simpleMode + ", using defaults");
perMode = new PerModeSettings();
}
if (perMode == null) {
DebugBalance("Per-mode settings null for " + simpleMode + ", using defaults");
perMode = new PerModeSettings();
}
if (fGameVersion == GameVersion.BFH && isNonBalancingMode) {
DebugWrite("^5(AUTO)^9 Server is in ^b" + simpleMode + "^n mode, which should not be balanced! Deactivating balancer!", 4);
CheckDeativateBalancer("BFH Competitive Mode");
return;
}
/* Per-mode and player info */
String extractedTag = ExtractTag(player);
Speed balanceSpeed = GetBalanceSpeed(perMode);
double unstackTicketRatio = GetUnstackTicketRatio(perMode);
int lastMoveFrom = player.LastMoveFrom;
if (totalPlayerCount >= (perMode.MaxPlayers-1)) {
if (DebugLevel >= 6) DebugBalance("Server is full by per-mode Max Players, no balancing or unstacking will be attempted!");
IncrementTotal(); // no matching stat, reflect total deaths handled
CheckDeativateBalancer("Full per-mode");
return;
}
int floorPlayers = (perMode.EnableLowPopulationAdjustments) ? 4 : 6;
if (totalPlayerCount < floorPlayers) {
if (DebugLevel >= 6) DebugBalance("Not enough players in server, minimum is " + floorPlayers);
IncrementTotal(); // no matching stat, reflect total deaths handled
CheckDeativateBalancer("Not enough players");
return;
}
/* Check dispersals */
bool mustMove = false;
bool lenient = false;
int maxDispersalMoves = 2;
bool isDisperseByRank = IsRankDispersal(player);
bool isDisperseByList = IsInDispersalList(player, false);
/* DCE */
bool isDisperseByClanPop = false;
if (!isDisperseByList) {
isDisperseByClanPop = IsClanDispersal(player, false);
}
if (isDisperseByList) {
lenient = !perMode.EnableStrictDispersal; // the opposite of strict is lenient
String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
ConsoleDebug("ON MUST MOVE LIST ^b" + player.FullName + "^n T:" + player.Team + ", disperse evenly enabled, " + dispersalMode);
mustMove = true;
maxDispersalMoves = (lenient) ? 1 : 2;
} else if (isDisperseByClanPop) {
lenient = !perMode.EnableStrictDispersal; // the opposite of strict is lenient
String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
ConsoleDebug("ON MUST MOVE LIST ^b" + player.FullName + "^n T:" + player.Team + ", disperse clan tags evenly enabled, " + dispersalMode);
mustMove = true;
maxDispersalMoves = (lenient) ? 1 : 2;
} else if (isDisperseByRank) {
lenient = LenientRankDispersal || !perMode.EnableStrictDispersal;
String dispersalMode = (lenient) ? "LENIENT MODE" : "STRICT MODE";
ConsoleDebug("ON MUST MOVE LIST ^b" + name + "^n T:" + player.Team + ", Rank " + player.Rank + " >= " + perMode.DisperseEvenlyByRank + ", " + dispersalMode);
mustMove = true;
maxDispersalMoves = (lenient) ? 1 : 2;
}
/* Check if balancing is needed */
if (diff > MaxDiff()) {
needsBalancing = true; // needs balancing set to true, unless speed is Unstack only
if (balanceSpeed == Speed.Unstack) {
DebugBalance("Needs balancing, but balance speed is set to Unstack, so no balancing will be done");
needsBalancing = false;
}
}
/* Per-mode settings */
// Adjust for duration of balance active
if (needsBalancing && fBalanceIsActive && balanceSpeed == Speed.Adaptive && fLastBalancedTimestamp != DateTime.MinValue) {
double secs = now.Subtract(fLastBalancedTimestamp).TotalSeconds;
if (secs > SecondsUntilAdaptiveSpeedBecomesFast) {
DebugBalance("^8^bBalancing taking too long (" + secs.ToString("F0") + " secs)!^n^0 Forcing to Fast balance speed.");
balanceSpeed = Speed.Fast;
}
}
// Adjust speed to Fast if teams differ by 4 or more
if (needsBalancing && balanceSpeed != Speed.Fast && balanceSpeed != Speed.Stop && !isSQDM && diff >= 4) {
DebugBalance("^8^bTeam count difference is 4 or more (" + diff + ")!^n^0 Forcing to Fast balance speed.");
balanceSpeed = Speed.Fast;
}
String orSlow = (balanceSpeed == Speed.Slow) ? " or speed is Slow" : String.Empty;
// Do not disperse mustMove players if speed is Stop or Slow or Phase is Late or Popluation is Low and Enable Low Population Adjustments is True
if (mustMove && balanceSpeed == Speed.Stop) {
DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Balance Speed = Stop");
mustMove = false;
} else if (mustMove && balanceSpeed == Speed.Slow) {
DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Balance Speed = Slow");
mustMove = false;
} else if (mustMove && GetPhase(perMode, false) == Phase.Late) {
DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Phase = Late");
mustMove = false;
} else if (mustMove && perMode.EnableLowPopulationAdjustments && GetPopulation(perMode, false) == Population.Low) {
DebugBalance("Removing MUST MOVE status from dispersal player ^b" + player.FullName + "^n T:" + player.Team + ", due to Population = Low");
mustMove = false;
}
/* Activation check */
if (balanceSpeed != Speed.Stop && needsBalancing) {
if (!fBalanceIsActive) {
DebugBalance("^2^bActivating autobalance!");
fLastBalancedTimestamp = now;
}
fBalanceIsActive = true;
} else {
CheckDeativateBalancer("Deactiving autobalance");
}
// Wait for unassigned
if (!mustMove && needsBalancing && balanceSpeed != Speed.Fast && (diff > MaxDiff()) && fUnassigned.Count >= (diff - MaxDiff())) {
DebugBalance("Wait for " + fUnassigned.Count + " unassigned players to be assigned before moving active players");
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
/* Early exemptions - avoid doing exclusion computation if unnecessary */
// Exempt if this player already been moved for balance or unstacking
if ((!mustMove && GetMovesThisRound(player) >= 1) || (mustMove && GetMovesThisRound(player) >= maxDispersalMoves)) {
DebugBalance("Exempting ^b" + name + "^n, already moved this round");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// Exempt if role isn't ordinary player - mustMove always false for this case
if (player.Role != ROLE_PLAYER) {
String rn = "UNKNOWN";
if (player.Role >= 0 && player.Role < ROLE_NAMES.Length) rn = ROLE_NAMES[player.Role];
DebugBalance("Exempting ^b" + name + "^n, role is " + rn + " for team " + GetTeamName(player.Team));
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
/* Exclusions */
// Exclude if on Whitelist or Reserved Slots if enabled
if (OnWhitelist || (needsBalancing && balanceSpeed == Speed.Slow)) {
if (CheckWhitelist(player, WL_BALANCE)) {
DebugBalance("Excluding ^b" + player.FullName + "^n: whitelisted" + orSlow);
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
}
// Sort player's team by the strong method
List<PlayerModel> fromList = GetTeam(player.Team);
if (fromList == null) {
DebugBalance("Unknown team " + player.Team + " for player ^b" + player.Name);
return;
}
switch (perMode.DetermineStrongPlayersBy) {
case DefineStrong.RoundScore:
fromList.Sort(DescendingRoundScore);
strongMsg = "Determing strong by: Round Score";
break;
case DefineStrong.RoundSPM:
fromList.Sort(DescendingRoundSPM);
strongMsg = "Determing strong by: Round SPM";
break;
case DefineStrong.RoundKills:
fromList.Sort(DescendingRoundKills);
strongMsg = "Determing strong by: Round Kills";
break;
case DefineStrong.RoundKDR:
fromList.Sort(DescendingRoundKDR);
strongMsg = "Determing strong by: Round KDR";
break;
case DefineStrong.PlayerRank:
fromList.Sort(DescendingPlayerRank);
strongMsg = "Determing strong by: Player Rank";
break;
case DefineStrong.RoundKPM:
fromList.Sort(DescendingRoundKPM);
strongMsg = "Determing strong by: Round KPM";
break;
case DefineStrong.BattlelogSPM:
fromList.Sort(DescendingSPM);
strongMsg = "Determing strong by: Battlelog SPM";
break;
case DefineStrong.BattlelogKDR:
fromList.Sort(DescendingKDR);
strongMsg = "Determing strong by: Battlelog KDR";
break;
case DefineStrong.BattlelogKPM:
fromList.Sort(DescendingKPM);
strongMsg = "Determing strong by: Battlelog KPM";
break;
default:
fromList.Sort(DescendingRoundScore);
strongMsg = "Determing strong by: Round Score";
break;
}
double above = ((fromList.Count * perMode.PercentOfTopOfTeamIsStrong) / 100.0) + 0.5;
int strongest = Math.Max(0, Convert.ToInt32(above));
int playerIndex = 0;
int minPlayers = (isSQDM) ? 5 : fromList.Count; // for SQDM, apply top/strong/weak only if team has 5 or more players
// Exclude if TopScorers enabled and a top scorer on the team
int topPlayersPerTeam = 0;
if (balanceSpeed != Speed.Fast && (TopScorers || balanceSpeed == Speed.Slow)) {
if (isSQDM) {
int maxCount = fromList.Count;
if (maxCount < 5) {
topPlayersPerTeam = 0;
} else if (maxCount <= 8) {
topPlayersPerTeam = 1;
} else if (totalPlayerCount <= 16) {
topPlayersPerTeam = 2;
} else {
topPlayersPerTeam = 3;
}
} else {
if (totalPlayerCount <= 22) {
topPlayersPerTeam = 1;
} else if (totalPlayerCount >= 42) {
topPlayersPerTeam = 3;
} else {
topPlayersPerTeam = 2;
}
}
}
// Loop is unconditional even when topPlayersPerTeam is zero, due to assigning playerIndex
for (int i = 0; i < fromList.Count; ++i) {
if (fromList[i].Name == player.Name) {
if (!mustMove
&& needsBalancing
&& balanceSpeed != Speed.Fast
&& fromList.Count >= minPlayers
&& topPlayersPerTeam != 0
&& i < topPlayersPerTeam) {
String why = (balanceSpeed == Speed.Slow) ? "Speed is slow, excluding top scorers" : "Top Scorers enabled";
if (!loggedStats) {
DebugBalance(GetPlayerStatsString(name));
loggedStats = true;
}
DebugBalance("Excluding ^b" + player.FullName + "^n: " + why + " and this player is #" + (i+1) + " on team " + GetTeamName(player.Team));
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
} else {
playerIndex = i;
break;
}
}
}
isStrong = (playerIndex < strongest);
// Exclude if too soon since last move
if ((!mustMove || lenient) && player.MovedByMBTimestamp != DateTime.MinValue) {
double mins = now.Subtract(player.MovedByMBTimestamp).TotalMinutes;
if (mins < MinutesAfterBeingMoved) {
DebugBalance("Excluding ^b" + player.Name + "^n: last move was " + mins.ToString("F0") + " minutes ago, less than required " + MinutesAfterBeingMoved.ToString("F0") + " minutes");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
} else {
// reset
player.MovedByMBTimestamp = DateTime.MinValue;
}
}
// Exclude if player joined less than MinutesAfterJoining
double joinedMinutesAgo = GetPlayerJoinedTimeSpan(player).TotalMinutes;
double enabledForMinutes = now.Subtract(fEnabledTimestamp).TotalMinutes;
if ((!mustMove || lenient)
&& needsBalancing
&& (enabledForMinutes > MinutesAfterJoining)
&& balanceSpeed != Speed.Fast
&& (joinedMinutesAgo < MinutesAfterJoining)) {
if (!loggedStats) {
DebugBalance(GetPlayerStatsString(name));
loggedStats = true;
}
DebugBalance("Excluding ^b" + player.FullName + "^n: joined less than " + MinutesAfterJoining.ToString("F1") + " minutes ago (" + joinedMinutesAgo.ToString("F1") + ")");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
// Special exemption if tag not verified and fetches pending in the queue and joined less than 15 minutes ago
if (!player.TagVerified && PriorityQueueCount() > 0 && joinedMinutesAgo < 15) {
if (DebugLevel >= 7) DebugBalance("Skipping ^b" + player.Name + "^n, clan tag not verified yet");
// Don't count this as an exemption
// Don't increment the total
return;
}
// Exclude if in squad with same tags
if ((!mustMove || lenient) && SameClanTagsInSquad && !isDisperseByClanPop) {
int cmt = CountMatchingTags(player, Scope.SameSquad);
if (cmt >= 2) {
String et = ExtractTag(player);
DebugBalance("Excluding ^b" + name + "^n, " + cmt + " players in squad with tag [" + et + "]");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
}
// Exclude if in team with same tags
if ((!mustMove || lenient) && SameClanTagsInTeam && !isDisperseByClanPop) {
int cmt = CountMatchingTags(player, Scope.SameTeam);
if (cmt >= 5 && !isDisperseByClanPop) {
String et = ExtractTag(player);
DebugBalance("Excluding ^b" + name + "^n, " + cmt + " players in team with tag [" + et + "]");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
}
// Exclude if on friends list
if ((!mustMove || lenient) && OnFriendsList) {
int cmf = CountMatchingFriends(player, Scope.SameSquad);
if (cmf >= 2) {
DebugBalance("Excluding ^b" + player.FullName + "^n, " + cmf + " players in squad are friends (friendex = " + player.Friendex + ")");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
if (ApplyFriendsListToTeam) {
cmf = CountMatchingFriends(player, Scope.SameTeam);
if (cmf >= 5) {
DebugBalance("Excluding ^b" + player.FullName + "^n, " + cmf + " players in team are friends (friendex = " + player.Friendex + ")");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
}
}
/* - moved earlier, left here in case need to restore:
// Exempt if this player already been moved for balance or unstacking
if ((!mustMove && GetMoves(player) >= 1) || (mustMove && GetMoves(player) >= maxDispersalMoves)) {
DebugBalance("Exempting ^b" + name + "^n, already moved this round");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
*/
/* Balance */
int toTeamDiff = 0;
int toTeam = ToTeam(name, player.Team, false, out toTeamDiff, ref mustMove); // take into account dispersal by Rank, etc.
if (toTeam == 0 || toTeam == player.Team) {
if (needsBalancing || mustMove) {
if (DebugLevel >= 7) DebugBalance("Exempting ^b" + name + "^n, target team selected is same or zero");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
}
int numTeams = 2; //(isSQDM) ? 4 : 2; // TBD, what is max squad size for SQDM?
int maxTeamSlots = (MaximumServerSize/numTeams);
int maxTeamPerMode = (perMode.MaxPlayers/numTeams);
List<PlayerModel> lt = GetTeam(toTeam);
int toTeamSize = (lt == null) ? 0 : lt.Count;
if (toTeamSize == maxTeamSlots || toTeamSize == maxTeamPerMode) {
if (DebugLevel >= 8) DebugBalance("Exempting ^b" + name + "^n, target team is full " + toTeamSize);
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
if (mustMove) DebugBalance("^4MUST MOVE^0 ^b" + name + "^n from " + GetTeamName(player.Team) + " to " + GetTeamName(toTeam));
if ((!mustMove || lenient) && needsBalancing && toTeamDiff <= MaxDiff()) {
DebugBalance("Exempting ^b" + name + "^n, difference between " + GetTeamName(player.Team) + " team and " + GetTeamName(toTeam) + " team is only " + toTeamDiff);
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
/* Moved ticket ratios up here for Rout Percentage exemption */
double ratio = 1;
double t1Tickets = 0;
double t2Tickets = 0;
if (IsCTF() || IsCarrierAssault() || IsObliteration()) {
// Use team points, not tickets
double usPoints = GetTeamPoints(1);
double ruPoints = GetTeamPoints(2);
if (usPoints <= 0) usPoints = 1;
if (ruPoints <= 0) ruPoints = 1;
ratio = (usPoints > ruPoints) ? (usPoints/ruPoints) : (ruPoints/usPoints);
} else {
// Otherwise use ticket ratio
if (fTickets[losingTeam] >= 1) {
if (IsRush()) {
// normalize Rush ticket ratio
double attackers = fTickets[1];
double defenders = fMaxTickets - (fRushMaxTickets - fTickets[2]);
defenders = Math.Max(defenders, attackers/2);
ratio = (attackers > defenders) ? (attackers/Math.Max(1, defenders)) : (defenders/Math.Max(1, attackers));
t1Tickets = attackers;
t2Tickets = defenders;
} else {
t1Tickets = Convert.ToDouble(fTickets[winningTeam]);
t2Tickets = Convert.ToDouble(fTickets[losingTeam]);
ratio = t1Tickets / Math.Max(1, t2Tickets);
}
}
}
if ((fBalanceIsActive || mustMove) && toTeam != 0 && balanceSpeed != Speed.Stop) {
String ts = null;
if (isSQDM) {
ts = fTeam1.Count + "(A) vs " + fTeam2.Count + "(B) vs " + fTeam3.Count + "(C) vs " + fTeam4.Count + "(D)";
} else {
ts = fTeam1.Count + "(" + GetTeamName(1) + ") vs " + fTeam2.Count + "(" + GetTeamName(2) + ")";
}
if (mustMove) {
DebugBalance("Autobalancing because ^b" + name + "^n must be moved");
} else {
DebugBalance("Autobalancing because difference of " + diff + " is greater than " + MaxDiff() + ", [" + ts + "]");
}
double abTime = now.Subtract(fLastBalancedTimestamp).TotalSeconds;
if (abTime > 0) {
DebugBalance("^2^bAutobalance has been active for " + abTime.ToString("F1") + " seconds!");
}
if (!loggedStats) {
DebugBalance(GetPlayerStatsString(name) + ((isStrong) ? " STRONG" : " WEAK"));
loggedStats = true;
}
/* Exemptions */
// Handle Rout exemptions
double ratioPercentage = ratio * 100;
if (perMode.RoutPercentage > 100 && ratioPercentage >= perMode.RoutPercentage) {
DebugBalance("Rout detected, winning/losing ratio of " + ratioPercentage.ToString("F0") + " is greater than " + perMode.RoutPercentage.ToString("F0"));
if (isStrong) {
String si = "(" + playerIndex + " of " + strongest + ")";
DebugBalance("Exempting ^b" + name + "^n^9 " + si + ", strong players are not moved during a rout");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
} else if (mustMove && lenient) {
DebugBalance("Exempting ^b" + name + "^n^9, dispersal players are not moved during a rout when dispersal is lenient");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
}
// Already on the smallest team
if ((!mustMove || lenient) && player.Team == smallestTeam) {
DebugBalance("Exempting ^b" + name + "^n, already on the smallest team");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// SQDM, not on the biggest team
if (isSQDM && !mustMove && balanceSpeed != Speed.Fast && player.Team != biggestTeam) {
// Make sure player's team isn't the same size as biggest
List<PlayerModel> aTeam = GetTeam(player.Team);
List<PlayerModel> bigTeam = GetTeam(biggestTeam);
if (aTeam == null || bigTeam == null || (aTeam != null && bigTeam != null && aTeam.Count < bigTeam.Count)) {
DebugBalance("Exempting ^b" + name + "^n, not on the biggest team");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
}
// Exempt if only moving weak players and is strong
if (!mustMove && perMode.OnlyMoveWeakPlayers && isStrong) {
DebugBalance("Exempting strong ^b" + name + "^n, Only Move Weak Players set to True for " + simpleMode);
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// Strong/Weak exemptions
if (!mustMove && balanceSpeed != Speed.Fast && fromList.Count >= minPlayers) {
if (DebugLevel > 5) DebugBalance(strongMsg);
// don't move weak player to losing team, unless we are only moving weak players
if (!isStrong && toTeam == losingTeam && !perMode.OnlyMoveWeakPlayers) {
DebugBalance("Exempting ^b" + name + "^n, don't move weak player to losing team (#" + (playerIndex+1) + " of " + fromList.Count + ", top " + (strongest) + ")");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// don't move strong player to winning team
if (isStrong && toTeam == winningTeam) {
DebugBalance("Exempting ^b" + name + "^n, don't move strong player to winning team (#" + (playerIndex+1) + " of " + fromList.Count + ", median " + (strongest) + ")");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// Don't move to same team
if (player.Team == toTeam) {
if (DebugLevel >= 7) DebugBalance("Exempting ^b" + name + "^n, don't move player to his own team!");
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
}
/* Move for balance */
int origTeam = player.Team;
String origName = GetTeamName(player.Team);
if (lastMoveFrom != 0) {
origTeam = lastMoveFrom;
origName = GetTeamName(origTeam);
}
MoveInfo move = new MoveInfo(name, player.Tag, origTeam, origName, toTeam, GetTeamName(toTeam), YellDurationSeconds);
move.For = MoveType.Balance;
move.Format(this, ChatMovedForBalance, false, false);
move.Format(this, YellMovedForBalance, true, false);
String why = (mustMove) ? "to disperse evenly" : ("because difference is " + diff);
log = "^4^bBALANCE^n^0 moving ^b" + player.FullName + "^n from " + move.SourceName + " team to " + move.DestinationName + " team " + why;
log = (EnableLoggingOnlyMode) ? "^9(SIMULATING)^0 " + log : log;
DebugWrite(log, 3);
DebugWrite("^9" + move, 8);
player.LastMoveFrom = player.Team;
StartMoveImmediate(move, false);
if (EnableLoggingOnlyMode) {
// Simulate completion of move
OnPlayerTeamChange(name, toTeam, 0);
OnPlayerMovedByAdmin(name, toTeam, 0, false); // simulate reverse order
}
// no increment total, handled later when move is processed
return;
}
if (!fBalanceIsActive) {
fLastBalancedTimestamp = now;
if (DebugLevel >= 8) ConsoleDebug("fLastBalancedTimestamp = " + fLastBalancedTimestamp.ToString("HH:mm:ss"));
}
/* Unstack */
// Not enabled or not full round
if (!EnableUnstacking) {
if (DebugLevel >= 8) DebugBalance("Unstack is disabled, Enable Unstacking is set to False");
IncrementTotal();
return;
} else if (!fIsFullRound) {
if (DebugLevel >= 7) DebugBalance("Unstack is disabled, not a full round");
IncrementTotal();
return;
}
// Sanity checks
if (winningTeam <= 0 || winningTeam >= fTickets.Length || losingTeam <= 0 || losingTeam >= fTickets.Length || balanceSpeed == Speed.Stop) {
if (DebugLevel >= 5) DebugBalance("Skipping unstack for player that was killed ^b" + name +"^n: winning = " + winningTeam + ", losingTeam = " + losingTeam + ", speed = " + balanceSpeed);
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
// Server is full, can't swap
if (totalPlayerCount > (MaximumServerSize-2) || totalPlayerCount > (perMode.MaxPlayers-2)) {
// TBD - kick idle players?
if (DebugLevel >= 7) DebugBalance("No room to swap players for unstacking");
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
// Disabled per-mode
if (perMode.CheckTeamStackingAfterFirstMinutes == 0) {
if (DebugLevel >= 5) DebugBalance("Unstacking has been disabled, Check Team Stacking After First Minutes set to zero");
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
double tirMins = GetTimeInRoundMinutes();
// Too soon to unstack
if (tirMins < perMode.CheckTeamStackingAfterFirstMinutes) {
DebugBalance("Too early to check for unstacking, skipping ^b" + name + "^n");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// Maximum swaps already done
if ((fUnstackedRound/2) >= perMode.MaxUnstackingSwapsPerRound) {
if (DebugLevel >= 6) DebugBalance("Maximum swaps have already occurred this round (" + (fUnstackedRound/2) + ")");
fUnstackState = UnstackState.Off;
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
// Whitelisted
if (OnWhitelist) {
if (CheckWhitelist(player, WL_UNSTACK)) {
DebugBalance("Excluding from unstacking due to being whitelisted, ^b" + name + "^n");
fExcludedRound = fExcludedRound + 1;
IncrementTotal();
return;
}
}
/* - moved earlier, left here in case need to restore:
double ratio = 1;
double t1Tickets = 0;
double t2Tickets = 0;
if (IsCTF() || IsCarrierAssault()) {
// Use team points, not tickets
double usPoints = GetTeamPoints(1);
double ruPoints = GetTeamPoints(2);
if (usPoints <= 0) usPoints = 1;
if (ruPoints <= 0) ruPoints = 1;
ratio = (usPoints > ruPoints) ? (usPoints/ruPoints) : (ruPoints/usPoints);
} else {
// Otherwise use ticket ratio
if (fTickets[losingTeam] >= 1) {
if (IsRush()) {
// normalize Rush ticket ratio
double attackers = fTickets[1];
double defenders = fMaxTickets - (fRushMaxTickets - fTickets[2]);
defenders = Math.Max(defenders, attackers/2);
ratio = (attackers > defenders) ? (attackers/Math.Max(1, defenders)) : (defenders/Math.Max(1, attackers));
t1Tickets = attackers;
t2Tickets = defenders;
} else {
t1Tickets = Convert.ToDouble(fTickets[winningTeam]);
t2Tickets = Convert.ToDouble(fTickets[losingTeam]);
ratio = t1Tickets / Math.Max(1, t2Tickets);
}
}
}
*/
// Ticket difference greater than per-mode maximum for unstacking
int ticketGap = Convert.ToInt32(Math.Abs(t1Tickets - t2Tickets));
if (perMode.MaxUnstackingTicketDifference > 0 && ticketGap > perMode.MaxUnstackingTicketDifference) {
DebugBalance("Ticket difference of " + ticketGap + " exceeds Max Unstacking Ticket Difference of " + perMode.MaxUnstackingTicketDifference + ", skipping ^b" + name + "^n");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
String um = "Current ratio " + (ratio*100.0).ToString("F0") + " vs. unstack ratio of " + (unstackTicketRatio*100.0).ToString("F0");
// Using player stats instead of ticket ratio
if (perMode.EnableUnstackingByPlayerStats) {
double a1 = GetAveragePlayerStats(1, perMode.DetermineStrongPlayersBy);
double a2 = GetAveragePlayerStats(2, perMode.DetermineStrongPlayersBy);
ratio = (a1 > a2) ? (a1/Math.Max(0.01, a2)) : (a2/Math.Max(0.01, a1));
ratio = Math.Min(ratio, 50.0); // cap at 50x
// Don't unstack if the team with the lowest average stats is the winning team
// We don't want to send strong players to the team with the highest score!
if ((a1 < a2 && winningTeam == 1)
|| (a2 < a1 && winningTeam == 2)) {
if (DebugLevel >= 7) DebugBalance("Team with lowest avg. stats is the winning team, do not unstack: " + a1.ToString("F1") + " vs " + a2.ToString("F1") + ", winning team is " + GetTeamName(winningTeam));
IncrementTotal();
return;
}
String cmp = (a1 > a2) ? (a1.ToString("F1") + "/" + a2.ToString("F1")) : (a2.ToString("F1") + "/" + a1.ToString("F1"));
um = "Average " + perMode.DetermineStrongPlayersBy + " stats ratio is " + (ratio*100.0).ToString("F0") + " (" + cmp + ") vs. unstack ratio of " + (unstackTicketRatio*100.0).ToString("F0");
}
// Using ticket loss instead of ticket ratio?
if (perMode.EnableTicketLossRatio && false) { // disable for this release
double a1 = GetAverageTicketLossRate(1, false);
double a2 = GetAverageTicketLossRate(2, false);
ratio = (a1 > a2) ? (a1/Math.Max(1, a2)) : (a2/Math.Max(1, a1));
ratio = Math.Min(ratio, 50.0); // cap at 50x
um = "Ticket loss ratio is " + (ratio*100.0).ToString("F0") + " vs. unstack ratio of " + (unstackTicketRatio*100.0).ToString("F0");
// Don't unstack if the team with the highest loss rate is the winning team
// We don't want to send strong players to the team with the highest score!
if ((a1 > a2 && winningTeam == 1)
|| (a2 > a1 && winningTeam == 2)) {
if (DebugLevel >= 7) DebugBalance("Team with highest ticket loss rate is the winning team, do not unstack: " + a1.ToString("F1") + " vs " + a2.ToString("F1") + ", winning team is " + GetTeamName(winningTeam));
IncrementTotal();
return;
}
}
if (unstackTicketRatio == 0 || ratio < unstackTicketRatio) {
bool ticketRatioOk = true;
bool scoreRatioOk = true;
int maxStages = 4;
bool isRush = IsRush();
if (fServerInfo != null && isRush) maxStages = GetRushMaxStages(fServerInfo.Map);
if (isRush && perMode.EnableAdvancedRushUnstacking && fRushStage > 0 && fRushStage < maxStages) {
// Check team points as well as tickets
double usPoints = GetTeamPoints(1);
double ruPoints = GetTeamPoints(2);
if (usPoints <= 0) usPoints = 1;
if (ruPoints <= 0) ruPoints = 1;
ratio = (usPoints > ruPoints) ? (usPoints/ruPoints) : (ruPoints/usPoints);
if (DebugLevel >= 6) DebugBalance("Checking Advanced Rush Unstacking (by score): stage = " + fRushStage);
scoreRatioOk = (unstackTicketRatio == 0 || ratio < unstackTicketRatio);
if (!scoreRatioOk) {
um = "(Advanced) score ratio is " + (ratio * 100.0).ToString("F0") + "% (" + usPoints.ToString("F0") + "/" + ruPoints.ToString("F0") + ") vs " + (unstackTicketRatio * 100.0).ToString("F0");
}
}
if (ticketRatioOk && scoreRatioOk) {
if (DebugLevel >= 6) DebugBalance("No unstacking needed: " + um);
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
}
}
// Handle Rout exemptions
if (perMode.RoutPercentage > 100 && ratio >= perMode.RoutPercentage) {
DebugBalance("No unstacking during a rout, skipping ^b" + name + "^n");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
/*
Cases:
1) Never unstacked before, timer is 0 and group count is 0
2) Within a group, timer is 0 and group count is > 0 but < max
3) Between groups, timer is > 0 and group count is 0
*/
double nsis = NextSwapGroupInSeconds(perMode); // returns 0 for case 1 and case 2
if (nsis > 0) {
if (DebugLevel >= 6) DebugBalance("Too soon to do another unstack swap group, wait another " + nsis.ToString("F1") + " seconds!");
IncrementTotal(); // no matching stat, reflect total deaths handled
return;
} else {
fFullUnstackSwapTimestamp = DateTime.MinValue; // turn off timer
}
// Are the minimum number of players present to decide strong vs weak?
if (!mustMove && balanceSpeed != Speed.Fast && fromList.Count < minPlayers) {
DebugBalance("Not enough players in team to determine strong vs weak, skipping ^b" + name + "^n, ");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
// Otherwise, unstack!
DebugBalance("^6Unstacking!^0 " + um);
if (DebugLevel >= 6) {
if (isStrong) {
DebugBalance("Player ^b" + player.Name + "^n is strong: #" + (playerIndex+1) + " of " + fromList.Count + ", above #" + strongest + " at " + perMode.PercentOfTopOfTeamIsStrong.ToString("F0") + "%");
} else {
DebugBalance("Player ^b" + player.Name + "^n is weak: #" + (playerIndex+1) + " of " + fromList.Count + ", equal or below #" + strongest + " at " + perMode.PercentOfTopOfTeamIsStrong.ToString("F0") + "%");
}
}
if (!loggedStats) {
DebugBalance(GetPlayerStatsString(name));
loggedStats = true;
}
MoveInfo moveUnstack = null;
int origUnTeam = player.Team;
String origUnName = GetTeamName(player.Team);
String strength = "strong";
if (lastMoveFrom != 0) {
origUnTeam = lastMoveFrom;
origUnName = GetTeamName(origUnTeam);
}
if (fUnstackState == UnstackState.Off) {
// First swap
DebugBalance("For ^b" + name + "^n, first swap of " + perMode.NumberOfSwapsPerGroup);
fUnstackState = UnstackState.SwappedWeak;
}
switch (fUnstackState) {
case UnstackState.SwappedWeak:
// Swap strong to losing team
if (isStrong) {
// Don't move to same team
if (player.Team == losingTeam) {
if (DebugLevel >= 6) DebugBalance("Skipping strong ^b" + name + "^n, don't move player to his own team!");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
DebugBalance("Sending strong player ^0^b" + player.FullName + "^n^9 to losing team " + GetTeamName(losingTeam));
moveUnstack = new MoveInfo(name, player.Tag, origUnTeam, origUnName, losingTeam, GetTeamName(losingTeam), YellDurationSeconds);
toTeam = losingTeam;
fUnstackState = UnstackState.SwappedStrong;
if (EnableTicketLossRateLogging) UpdateTicketLossRateLog(now, losingTeam, 0);
} else {
DebugBalance("Skipping ^b" + name + "^n, don't move weak player to losing team (#" + (playerIndex+1) + " of " + fromList.Count + ", median " + (strongest) + ")");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
break;
case UnstackState.SwappedStrong:
// Swap weak to winning team
if (!isStrong) {
// Don't move to same team
if (player.Team == winningTeam) {
if (DebugLevel >= 6) DebugBalance("Skipping weak ^b" + name + "^n, don't move player to his own team!");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
DebugBalance("Sending weak player ^0^b" + player.FullName + "^n^9 to winning team " + GetTeamName(winningTeam));
moveUnstack = new MoveInfo(name, player.Tag, origUnTeam, origUnName, winningTeam, GetTeamName(winningTeam), YellDurationSeconds);
toTeam = winningTeam;
fUnstackState = UnstackState.SwappedWeak;
strength = "weak";
FinishedFullSwap(name, perMode); // updates group count
if (EnableTicketLossRateLogging) UpdateTicketLossRateLog(now, 0, winningTeam);
} else {
DebugBalance("Skipping ^b" + name + "^n, don't move strong player to winning team (#" + (playerIndex+1) + " of " + fromList.Count + ", median " + (strongest) + ")");
fExemptRound = fExemptRound + 1;
IncrementTotal();
return;
}
break;
case UnstackState.Off:
// fall thru
default: return;
}
/* Move for unstacking */
log = "^4^bUNSTACK^n^0 moving " + strength + " ^b" + player.FullName + "^n from " + moveUnstack.SourceName + " to " + moveUnstack.DestinationName + " because: " + um;
log = (EnableLoggingOnlyMode) ? "^9(SIMULATING)^0 " + log : log;
DebugWrite(log, 3);
moveUnstack.For = MoveType.Unstack;
moveUnstack.Format(this, ChatMovedToUnstack, false, false);
moveUnstack.Format(this, YellMovedToUnstack, true, false);
DebugWrite("^9" + moveUnstack, 8);
if (player.LastMoveFrom == 0) player.LastMoveFrom = player.Team;
StartMoveImmediate(moveUnstack, false);
if (EnableLoggingOnlyMode) {
// Simulate completion of move
OnPlayerTeamChange(name, toTeam, 0);
OnPlayerMovedByAdmin(name, toTeam, 0, false); // simulate reverse order
}
// no increment total, handled by unstacking move
}