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