| toblue | switch
| flash | warn | kick | kickban ");
Sender.ClientMessage(" | listids | kickid | kickbanid | addmut | delmut | logstats | forcetravel ) "$pass_if_needed);
} else {
Sender.ClientMessage(" mutate help []");
}
if (Sender.bAdmin) {
Sender.ClientMessage("AutoTeamBalance "$ "1.4.9s" $" admin-only console commands: mutate [atb] ( saveconfig | grantadmin | get | set | getprop | setprop | console | cc )");
}
}
Super.Mutate(str,Sender);
}
function SwitchTwoPlayers(PlayerPawn sender, String name1, String name2) {
local Pawn player1, player2;
local int newteam1, newteam2;
player1 = FindPlayerNamed(name1);
player2 = FindPlayerNamed(name2);
if (player1 == None) {
Sender.ClientMessage("Could not find player matching \""$name1$"\".");
return;
}
if (player2 == None) {
Sender.ClientMessage("Could not find player matching \""$name2$"\".");
return;
}
if (player1.PlayerReplicationInfo.Team == player2.PlayerReplicationInfo.Team) {
Sender.ClientMessage("Players \""$player1.getHumanName()$"\" and \""$player2.getHumanName()$"\" are on the same team!");
return;
}
newteam1 = player2.PlayerReplicationInfo.Team;
newteam2 = player1.PlayerReplicationInfo.Team;
ChangePlayerToTeam(player1,newteam1,true);
ChangePlayerToTeam(player2,newteam2,true);
BroadcastTeamStrengths();
}
function ToggleAdminOnPlayer(Pawn p) {
local PlayerPawn player;
if (p!=None && p.IsA('PlayerPawn')) {
player = PlayerPawn(p);
player.bAdmin = !player.bAdmin;
player.PlayerReplicationInfo.bAdmin = player.bAdmin;
}
}
// HandleEndGame gets called when the game time limit expires, BUT the game may go into overtime without us knowing (one of the earlier mutators, or the gametype itself, might decide this).
// So at this point I set a Timer to check in CheckFrequency seconds whether the game really has ended or not.
// DONE: if not needed for bWarnMidGameUnbalance or bForceEvenTeams, the timer is disabled after one check, then we wait for this function to get called again before it is started again.
function bool HandleEndGame() {
SetTimer(2,bWarnMidGameUnbalance || bForceEvenTeams); // only loop if we need to check team balance during overtime; if we are only looking for the real end-game, then we only need to use the timer once more
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "HandleEndGame() Set Timer() for 2 seconds. [bOverTime="$Level.Game.bOverTime$",bGameEnded="$Level.Game.bGameEnded$"]"); };
return Super.HandleEndGame();
}
// =========== Our State Model =========== //
// Checks if the game has begun.
function CheckGameStart() {
local int c,n,e;
local Pawn p;
// We can disable the timer immediately, if AutoTeamBalance is not needed for this game.
// If we are going to balance, then the timer waits until 2 seconds before the game starts.
// If we are going to update stats, we need to record the time the game actually started at, so we wait the same way.
if (!ShouldBalance(Level.Game) && !ShouldUpdateStats(Level.Game)) { // We do this early, to check at the very least that this is a teamgame, to avoid accessed none's below
DoGameStart();
return;
}
// TODO BUG: if bUpdatePlayerStatsForNonTeamGames is enabled, then on DM maps, we reach here and throw some Accessed None errors.
// But we still want the game start-time.
e = DeathMatchPlus(Level.Game).ElapsedTime;
n = DeathMatchPlus(Level.Game).NetWait;
c = DeathMatchPlus(Level.Game).countdown;
c = Min(c,n-e);
// DebugLog("c="$c$" n-e="$(n-e)$" e="$e$" n="$n$" p="$p);
// Initialize teams 1 or 2 seconds before the game starts:
if (c<2) {
DoGameStart();
} else {
if (bShuffleTeamsEarly) {
// TODO: Do a silent Rebalance in case a new player has joined.
if (!DeathMatchPlus(Level.Game).bTournament) {
ForceFullTeamsRebalance();
// MidGameRebalance(True); // Keep re-shuffling teams until game starts or they are even.
}
}
FlashPreGameLines();
}
}
function FlashPreGameLines() {
local int targetLine;
local Pawn p;
local float strength;
// Override the line which says what team each player is on (since teams have not yet been decided!):
// Line 3 usually displays "You are on the Red/Blue team" before the game starts.
// But since we won't balance teams until 2 seconds before game start, we want to overwrite line 3.
// We also overwrite line 4, which usually displays "Use Options -> Player Setup to change teams".
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
// The check for UTServer Avoids logging repeated calls to UTServerAdminSpectator before anyone has joined the server.
if (p.IsA('PlayerPawn') && !p.IsA('Spectator') && InStr(String(p.class),"UTServer")==-1) {
/*
// Only override the line, iff that line is currently displaying the player's team prematurely. (Avoid conflicting with XOL's pre-game hiscore display.)
// Does not work!
if (StrContains(PlayerPawn(p).ProgressMessage[3],"You") ||
StrContains(PlayerPawn(p).ProgressMessage[2],"You") ||
StrContains(PlayerPawn(p).ProgressMessage[1],"You are on ") ||
StrContains(PlayerPawn(p).ProgressMessage[5],"You") ||
StrContains(PlayerPawn(p).ProgressMessage[4],"You")
) {
*/
// We want to override the line which usually says which team you are "on".
// But different game types use a different line.
// So far I have only checked CTF and Assault.
targetLine = 3;
if (Level.Game.Class.IsA('CTFGame'))
targetLine = 3;
if (Level.Game.Class.IsA('Assault'))
targetLine = 2;
if (Level.NetMode==NM_Standalone)
targetLine = 2; // At lease true for CTF
// We do this even if not needed, to force lookups when staggering at the start of the map
strength = GetRecordedPlayerStrength(p);
// We don't spam messages if we are not going to balance teams later. (They might not get cleared!)
if (ShouldBalance(Level.Game)) {
// We don't flash in tournament mode, because it flashes all the way through warmup!
if (!DeathMatchPlus(Level.Game).bTournament) {
if (bFlashCookies) {
if (bReportStrengthAsCookies)
FlashMessageToPlayer(p, p.getHumanName() $", you have "$ Int(strength) $" cookies.",strengthColor,targetLine);
else
FlashMessageToPlayer(p, p.getHumanName() $" you have strength "$ Int(strength) $"",strengthColor,targetLine);
} else {
FlashMessageToPlayer(p,"Teams not yet assigned.",colorWhite,targetLine); // colMagenta
// FlashMessageToPlayer(p,"Assigning teams in "$Max(c-1,n-e-1),colorMagenta,3);
}
}
}
}
}
}
function DoGameStart() {
local Pawn p;
local Color msgColor;
timeGameStarted = Level.TimeSeconds+1.5; // (since we are called on average 1.5 seconds before starting countdown ends)
if (ShouldBalance(Level.Game)) {
//// We could also do this once or twice *after* the ForceFullTeamsRebalance(), to make teams really even by strength (not pickup style).
if (!bShuffleTeamsEarly) {
ForceFullTeamsRebalance();
}
// (This must come after the team switching, otherwise the default start-game "xxx is on Red" will overwrite this text.)
// TODO CONSIDER BUG: isn't it more important that the player sees which team they were moved to?!
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') && !p.IsA('Spectator')) {
// PlayerPawn(p).ClearProgressMessages(); // Clear the pre-game messages before showing new team and cookies.
switch (p.PlayerReplicationInfo.Team) {
case 0: msgColor = colorRed; break;
case 1: msgColor = colorBlue; break;
case 2: msgColor = colorGreen; break;
case 3: msgColor = colorYellow; break;
default: msgColor = colorWhite; break;
}
// PlayerPawn(p).ClearProgressMessages();
// But on XOL, when the game does start, line 3 is used to display Highest # covers. So on XOL, we use line 5.
// TODO: In standalone, this needs to be -1 for CTF
// #undef LINENR_FOR_FLASH
// #define LINENR_FOR_FLASH -1
// CONSIDER: PlayerPawn(p).ClearProgressMessages();
// TODO: For Assault, we need to move 1 line up.
FlashMessageToPlayer(p,"You are on the "$Caps(getTeamName(p.PlayerReplicationInfo.Team))$" team.",msgColor,3);
}
}
// BroadcastMessage("",False);
// if (bBroadcastHelloGoodbye) { BroadcastMessageAndLog("Red team strength is "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$"."); }
}
gameStartDone=True; // Should ensure CheckGameStart() is never called again.
// Disable('Tick');
// We disable the timer, if it is not needed to check mid-game teambalance.
// HandleEndGame() will set it again, if it is needed for CheckGameEnd().
if (bWarnMidGameUnbalance || bForceEvenTeams) {
SetTimer(CheckFrequency,True);
} else {
SetTimer(0,False);
}
}
// Deals with mid-game team imbalance, only called if bForceEvenTeams and/or bWarnMidGameUnbalance are set.
function CheckMidGameBalance() {
local int redTeamCount,blueTeamCount;
local int redTeamStrength,blueTeamStrength;
local int weakerTeam;
local String problem; // human-readable explanation of the team unbalance
local Pawn p;
local int i;
weakerTeam = -1;
redTeamCount = GetTeamSize(0);
blueTeamCount = GetTeamSize(1);
// Is one of the teams down 2 or more players?
if (redTeamCount>=blueTeamCount+2) {
weakerTeam = 1; problem = " "$redTeamCount$"v"$blueTeamCount$".";
}
if (redTeamCount<=blueTeamCount-2) {
weakerTeam = 0; problem = " "$redTeamCount$"v"$blueTeamCount$".";
}
// If so, and bForceEvenTeams is set, then take action!
if (bForceEvenTeams && weakerTeam != -1) {
MidGameRebalance(True);
return;
// DONE: bForceEvenTeams does *not* take action if the teams differ by less than 2 players. But maybe it should, if they are really unfair by strength! -- Nee leave that for bWarnMidGameUnbalance
}
// Do we want to warn players of any imbalance?
if (bWarnMidGameUnbalance) {
if (weakerTeam == -1 /*&& redTeamCount+blueTeamCount>=3*/) { // no point checking this on a 1v1 ;) - true but i want to check during development; and baiter's bug was weird, hopefully it shouldn't appear too often.
if (redTeamCount == blueTeamCount && bNeverRebalanceWhenTeamsAreEven) { // TODO: Could instead be !bWarnMidGameStrengthImbalance
return;
}
// So teams differ by <2 players. Now calculate which team is weaker, and check if that team has fewer players:
if (bCheckStrengthBalance) {
redTeamStrength = GetTeamStrength(0);
blueTeamStrength = GetTeamStrength(1);
if (redTeamCount>=blueTeamCount && redTeamStrength>blueTeamStrength+StrengthThreshold) {
weakerTeam = 1; problem = " Strength "$redTeamStrength$" v "$blueTeamStrength$".";
}
if (redTeamCount<=blueTeamCount && blueTeamStrength>redTeamStrength+StrengthThreshold) {
weakerTeam = 0; problem = " Strength "$redTeamStrength$" v "$blueTeamStrength$".";
}
}
}
// NormalLog("CheckMidGameBalance("$redTeamCount$"v"$blueTeamCount$"): checking teams => weaker="$weakerTeam$" problem="$problem);
if (weakerTeam == -1) {
return;
}
// OK we have an imbalance.
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "CheckMidGameBalance("$redTeamCount$"v"$blueTeamCount$"): doing warning "$redTeamStrength$"v"$blueTeamStrength$" => weaker="$weakerTeam$" problem="$problem); };
if (!bShowReason)
problem = "";
// TODO: if (bForceEvenTeams) { MidGameRebalance(True); return; }
if (bShowProposedSwitch && bOnlyFlashInvolvedPlayers) {
// TODO: Skip the flashing below, but allow the one in MidGameRebalance().
}
// Send all players the team imbalance warning:
if (bLetPlayersRebalance && bShowProposedSwitch) {
// OK now we suggest who to move:
MidGameRebalance(False); // Note: this will clear the progress messages, which is why we do it first.
// The suggestion to fix teams requires justification:
if (bShowReason && problem != "") {
if (bFlashRebalanceRequest) {
FlashToAllPlayers("Teams look uneven!"$problem,warnColor,FlashLine);
} else {
BroadcastMessageAndLog("Teams look uneven!"$problem);
}
}
} else {
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') && !p.IsA('Spectator')) {
// Players on different teams get slightly different messages:
if (p.PlayerReplicationInfo.Team == weakerTeam) {
// Weaker team:
if (bLetPlayersRebalance) {
if (bFlashOnWarning) {
PlayerPawn(p).ClearProgressMessages();
FlashMessageToPlayer(p,"Teams look uneven!"$problem$" Type !teams to fix them",warnColor,FlashLine);
} else {
p.ClientMessage("Teams look uneven!"$problem$" Type !teams to fix them",'Event',False);
}
}
} else {
// Stronger team:
if (bFlashOnWarning) {
PlayerPawn(p).ClearProgressMessages();
FlashMessageToPlayer(p,"Teams look uneven!"$problem$" Type "$ConditionalString(bLetPlayersRebalance,"!teams or ","")$"!"$Locs(getTeamName(weakerTeam))$"",warnColor,FlashLine);
} else {
p.ClientMessage("Teams look uneven!"$problem$" Type "$ConditionalString(bLetPlayersRebalance,"!teams or ","")$"!"$Locs(getTeamName(weakerTeam))$"",'Event',False);
}
// We may "punish" the stronger team, by shaking their view, or sending them a buzzing sound:
if (bShakeOnWarning) {
p.ShakeView(1.0,2000.0,2000.0);
}
if (bBuzzOnWarning) {
p.PlaySound(sound'FlyBuzz', SLOT_Interface, 2.5, False, 32, 16); // an annoying buzzing fly sound
}
}
}
}
}
}
}
// Before we flash new progress messages, we need to clear what was there before.
// On XOL this is the hiscore records.
// But on other servers in general, you will see the message
// "The match has begun".
function ClearAllProgressMessages() {
local Pawn p;
// local int i;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') && !p.IsA('Spectator')) {
PlayerPawn(p).ClearProgressMessages();
}
}
// for (i=0;i<8;i++) {
// FlashToAllPlayers(" ",colorWhite,i);
// }
}
function String ConditionalString(bool b, String yes, String no) {
if (b) { return yes; } else { return no; }
}
function name ConditionalName(bool b, name yes, name no) {
if (b) { return yes; } else { return no; }
}
function CheckGameEnd() {
if (Level.Game.bGameEnded) {
if (gameEndDone) return;
gameEndDone = true;
// We could (but don't) turn the Timer off now
if (ShouldUpdateStats(Level.Game)) {
UpdateStatsAtEndOfGame();
}
}
}
// Do we care if it's a teamgame? Maybe they just want to change skin colour!
function bool CheckMessage(String Msg, Pawn Sender) {
if (bEnablePlayerCommands) {
if (Sender.IsA('PlayerPawn') && !Sender.IsA('Spectator') && (TeamGamePlus(Level.Game)!=None && !TeamGamePlus(Level.Game).bNoTeamChanges)) {
if (Msg ~= "!RED" || Msg ~= "!R") {
ChangePlayerToTeam(PlayerPawn(Sender),0,false);
BroadcastTeamStrengths();
}
if (Msg ~= "!BLUE" || Msg ~= "!B") {
ChangePlayerToTeam(PlayerPawn(Sender),1,false);
BroadcastTeamStrengths();
}
if (Msg ~= "!GREEN" || Msg ~= "!G") {
ChangePlayerToTeam(PlayerPawn(Sender),2,false);
BroadcastTeamStrengths();
}
if (Msg ~= "!GOLD" || Msg ~= "!YELLOW" || Msg ~= "!Y") {
ChangePlayerToTeam(PlayerPawn(Sender),3,false);
BroadcastTeamStrengths();
}
}
if (Sender.IsA('PlayerPawn') && !Sender.IsA('Spectator')) {
if (Msg ~= "!SPEC" || Msg ~= "!SPECTATE" || Msg ~= "!S") {
PlayerPawn(Sender).PreClientTravel(); // not sure if this is actually needed
PlayerPawn(Sender).ClientTravel("?OverrideClass=Botpack.CHSpectator",TRAVEL_Relative, False);
}
}
if (Sender.IsA('Spectator')) {
if (Msg ~= "!PLAY" || Msg ~= "!P") {
PlayerPawn(Sender).PreClientTravel(); // not sure if this is actually needed
PlayerPawn(Sender).ClientTravel("?OverrideClass=",TRAVEL_Relative, False);
}
}
if (Msg ~= "!VOTE" || Msg ~= "!MAPVOTE" || Msg ~= "!V") {
Level.Game.BaseMutator.Mutate("bdbmapvote votemenu",PlayerPawn(Sender));
}
if (Msg ~= "!CTFSTATS" || Msg ~= "!CTF") {
Level.Game.BaseMutator.Mutate("smartctf stats",PlayerPawn(Sender));
}
if (Msg ~= "!STATS") {
Level.Game.BaseMutator.Mutate("smartctf stats",PlayerPawn(Sender));
ShowStrengthsTo(PlayerPawn(Sender),True);
}
if (Msg ~= "!STRENGTHS") {
ShowStrengthsTo(PlayerPawn(Sender),False);
}
if ( Msg ~= "!WHO" && (bAllowUsersToListFakes || PlayerPawn(Sender).bAdmin) ) {
ListFakesTo(PlayerPawn(Sender));
}
if (Sender.IsA('PlayerPawn')) {
if (Msg ~= "!WEBSITE" || Msg ~= "!W" || Msg ~= "!WEB" || Msg ~= "!WWW") {
if (WebsiteURL != "") {
SendPlayerToUrl(PlayerPawn(Sender),WebsiteURL);
}
}
if (Msg ~= "!FORUM") {
if (ForumURL != "") {
SendPlayerToUrl(PlayerPawn(Sender),ForumURL);
}
}
}
if (Sender.IsA('PlayerPawn')) {
if (Msg ~= "!TS" || Msg ~= "!TEAMSPEAK") {
SendPlayerToTeamspeak(PlayerPawn(Sender));
}
if (Msg ~= "!GETTS" || Msg ~= "!GetTeamSpeak") {
SendPlayerToUrl(PlayerPawn(Sender),"http://www.teamspeak.com/");
}
}
}
if (PlayerPawn(Sender)!=None && Spectator(Sender)==None && Msg ~= "TEAMS" || Msg ~= "!TEAMS") {
if (bLetPlayersRebalance && (bHelpInPugs || !DeathMatchPlus(Level.Game).bTournament)) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "MutatorTeamMessage() "$ Sender.getHumanName() $" requested rebalance with \""$ Msg $"\"."); };
RequestMidGameRebalance(PlayerPawn(Sender));
// if (FRand()<0.4) LastPlayerToJoin = PlayerPawn(Sender);
}
}
if (StrStartsWith(Caps(Msg),"!MUTATE ")) {
PlayerPawn(Sender).Mutate(StrAfter(Msg," "));
}
}
function SendPlayerToUrl(PlayerPawn Sender, String url) {
Sender.ClientMessage(">> Opening "$url);
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Sending "$Sender.getHumanName()$" to "$StrAfterFirst(url,"://")); };
//// Did not work:
// Sender.ConsoleCommand("open "$url);
//// We could test this alternative:
// Sender.ConsoleCommand("START "$url);
//// Works:
Sender.PreClientTravel();
Sender.ClientTravel(url, TRAVEL_Absolute, False);
}
function SendPlayerToTeamspeak(PlayerPawn Sender) {
local int teamNum;
local string url,nickname;
// Use the common channel by default.
url = TeamspeakChannelOther;
// But try to set a team if it's a team game.
teamNum = Sender.PlayerReplicationInfo.Team;
if (Level.Game.GameReplicationInfo.bTeamGame && teamNum>=0 && teamNum<4)
url = TeamspeakChannel[teamNum];
// If no team was set, fallback to the common channel again.
if (url == "")
url = TeamspeakChannelOther;
// Failing that, fall back to the red team, if we are not playing a war or pug.
if (url == "" && !DeathMatchPlus(Level.Game).bTournament)
url = TeamspeakChannel[0];
if (url == "") {
Sender.ClientMessage("No TeamSpeak channel has been configured for this game.");
} else {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "SendPlayerToTeamspeak("$Sender.getHumanName()$"): target url "$url); };
/* For teamspeak urls, append player name: */
if (StrContains(url,"teamspeak://") && StrContains(url,"?")) {
nickname = StrFilterBadChars(Sender.getHumanName());
/* We could also add a random number on the end, in case someone else is using the same nick. */
/* nickname = nickname $ Int(FRand()*100); */
url = StrBeforeFirst(url,"?") $ "?nickname=" $ nickname $ "?" $ StrAfterFirst(url,"?");
}
SendPlayerToUrl(Sender,url);
}
}
// Teamspeak will fail if the player's nickname contains certain chars, so we strip them here. Even better would be encoding them!
function String StrFilterBadChars(String inStr) {
local String outStr;
local int i,c;
for (i=0;i=Asc("A") && c<=Asc("Z")) || (c>=Asc("a") && c<=Asc("z")) ||
(c>=Asc("0") && c<=Asc("9")) || c==Asc("_") || c==Asc("+") ||
c==Asc("-") )
{
outStr = outStr $ Chr(c);
} else {
outStr = outStr $ "_";
}
}
return outStr;
}
// =========== Balancing Algorithms =========== //
// Also see ModifyLogin() above, for the decision of which team to send a player to when they join a running game.
// Balance the teams just before the start of a new game. No need for FlagStrength here.
// It can also be forced by a semi-admin mid-game, using "mutate forceteams".
// In this case, it doesn't check which players are holding flags.
function ForceFullTeamsRebalance() {
local Pawn p;
local int st;
local int pid;
local Pawn pl[64]; // hashmap of playerpawns, with i = PlayerID%64
local int ps[64]; // their strengths
local int moved[64]; // so 0=false 1=true :P
local int plorder[32];
local int i;
local int n;
local int mx;
local int teamnr,actualteamnr,direction,weakestStr;
local int teamstr[2];
local TeamGamePlus g; // my linux ucc make had trouble with TeamGamePlus :|
local int oldMaxTeamSize;
local bool oldbPlayersBalanceTeams, oldbNoTeamChanges;
local bool flip;
// We can't balance if it's not a teamgame
if (!Level.Game.GameReplicationInfo.bTeamGame) return;
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ForceFullTeamsRebalance() Running..."); };
if (bBroadcastHelloGoodbye) { BroadcastMessageAndLog("AutoTeamBalance is attempting to balance the teams..."); }
// rate all players, and put them in a temporary structure (pl[],ps[]):
for (p=Level.PawnList; p!=None; p=p.NextPawn)
{
if (AllowedToBalance(p))
{
st=GetPlayerStrength(p);
pid=p.PlayerReplicationInfo.PlayerID % 64;
pl[pid]=p;
ps[pid]=st;
moved[pid] = 0;
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ForceFullTeamsRebalance() Player " $ p.getHumanName() $ " on team " $ p.PlayerReplicationInfo.Team $ " has db-key " $ GetDBName(p) $ " and score " $ p.PlayerReplicationInfo.Score $ "."); };
}
}
// sort players by strength (move them out of the structure, into plorder[])
n=0;
do
{
pid=-1;
mx=0;
// find pid=i with max tg[i]
for (i=0; i<64; i++)
{
// Is this the strongest not-yet-moved player in this cycle?
if ( pl[i] != None && moved[i]==0 && (pid == -1 || ps[i]>mx) ) {
pid=i;
mx=ps[i];
}
}
// If we found one, add him as the next player in the list
if (pid != -1) {
plorder[n]=pid;
// ps[pid]=0;
moved[pid] = 1;
n++;
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ForceFullTeamsRebalance() [Ranking] "$ps[pid]$" "$ pl[pid].getHumanName() $""); };
}
} until (pid==-1);
// save team changing rules before we override them
g=TeamGamePlus(Level.Game);
oldMaxTeamSize=g.MaxTeamSize;
oldbPlayersBalanceTeams=g.bPlayersBalanceTeams;
oldbNoTeamChanges=g.bNoTeamChanges;
// deactivate team changing rules
g.MaxTeamSize=32;
g.bPlayersBalanceTeams=False;
g.bNoTeamChanges=False;
if (bClanWar)
{
// rebuild teams by clan tags
teamstr[0]=0;
teamstr[1]=0;
for (i=0; iright or right->left?
if (FRand() < 0.5)
flip = true;
else
flip = false;
// Rebuild teams by strength, assigning in order: red-blue-blue-red-red-blue-blue-...
// (On the way we also calculate total team strengths)
for (i=0; i= 24) {
if (diff<0) {
col = colorRed;
leadTeam = 0;
} else {
col = colorBlue;
leadTeam = 1;
}
// FlashToAllPlayers("Advantage "$Int(Abs(diff)),col,2);
// FlashToAllPlayers(getTeamName(leadTeam) $" has advantage "$ Int(Abs(diff)),col,2);
FlashToAllPlayers(getTeamName(leadTeam) $" leads by strength "$ Int(Abs(diff)),col,2);
// FlashToAllPlayers(getTeamName(leadTeam) $" leads by "$ Left(String(Abs(diff)/UnknownStrength),3) $" players",col,2);
// FlashToAllPlayers(getTeamName(leadTeam)" has "$ Int(Abs(diff)) $" advantage",col,2);
}
}
*/
}
function String GetTeamStrengthString() {
//@TODO! LastTeamDifference
local float difference;
local String balanceStr;
local int winningTeam;
if (TeamGamePlus(Level.Game) == None)
return "";
if (bBroadcastTeamStrengthDifference) {
difference = GetTeamStrength(1) - GetTeamStrength(0);
balanceStr = "well balanced";
if (Abs(difference)>20)
balanceStr = "reasonably balanced";
if (Abs(difference)>40)
balanceStr = "a little unbalanced";
if (Abs(difference)>70)
balanceStr = "unbalanced";
if (Abs(difference)>100)
balanceStr = "very unbalanced";
winningTeam = (1+Sgn(difference))/2;
return "Teams are "$ balanceStr $" (+"$ Int(Abs(difference)) $" to "$ getTeamName(winningTeam) $")";
} else if (bBroadcastTeamStrengths) {
return "Red team strength is "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$".";
}
}
// TODO: There's little point asking for additional "!teams" requests, if the algorithm will refuse to move any players anyway! (Well, this is DONE if bShowProposedSwitch=True.)
// TODO/DONE?: Also, it asks for additional requests, when bWarnMidGameUnbalance is flashing - it shouldn't! Well this is DONE if the flashing is caused by #players, but not if it's caused by strength imbalance.
function RequestMidGameRebalance(PlayerPawn Sender) {
local int i;
local int countRequests;
local int additionalRequiredRequests;
local Pawn p;
local string s;
// If the last request was a long time ago (>1 minute), reset the request list
if (Level.TimeSeconds > lastRebalanceRequestTime+60) {
for (i=0;i<64;i++) {
pidsRequestingRebalance[i] = 0;
}
}
// Set that this player is requesting balance
pidsRequestingRebalance[Sender.PlayerReplicationInfo.PlayerID] = 1;
// Count the number of requests at this time
countRequests = 0;
for (i=0;i<64;i++) {
if (pidsRequestingRebalance[i] != 0) {
countRequests++;
}
}
// Work out how many more requests are needed
additionalRequiredRequests = MinRequestsForRebalance - countRequests;
// But we might now change this variable, under certain conditions.
// Refuse to balance teams more than once every MinSecondsBeforeRebalance seconds:
// This also fixed the bug that (I think) if the player who said "!teams" was switched, a second call to MutatorTeamMessage was made, and MidGameRebalance was getting called again.
// TODO TEST: I may have re-introduced that bug when I moved this code around, to apply bOverrideMinRequests.
if (/*MinRequestsForRebalance<2 &&*/ lastBalanceTime + MinSecondsBeforeRebalance > Level.TimeSeconds) {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "MidGameRebalance() refusing to rebalance since lastBalanceTime="$lastBalanceTime$" is too close to current time "$Level.TimeSeconds); };
//// Don't broadcast, just mute.
// BroadcastMessageAndLog("AutoTeamBalance refuses to rebalance teams again so soon.");
// return;
additionalRequiredRequests = 99;
// But we may override this with bOverrideMinRequests...
}
if (DeathMatchPlus(Level.Game).bTournament && MinRequestsForRebalance<2) {
// We are probably doing bHelpInPugs, so let's go a little softer.
// Basically this means, during tournament mode, 1 person alone cannot force teambalance.
additionalRequiredRequests++;
} else {
// But if teams differ in size by 2 or more players, only one request to rebalance is needed:
// CONSIDER TODO: we could also require only 1 request if the stronger team has more players
if (bOverrideMinRequests && Abs(GetTeamSize(0)-GetTeamSize(1))>=2) {
additionalRequiredRequests = 0;
}
if (bOverrideMinRequests && Abs(GetTeamStrength(0) - GetTeamStrength(1)) > StrengthThreshold) {
additionalRequiredRequests = 0;
}
}
// Decide what to do
if (additionalRequiredRequests <= 0) {
MidGameRebalance(True);
lastRebalanceRequestTime = -60; // Will force a reset the next time we are called
} else {
if (additionalRequiredRequests == 99) {
BroadcastMessageAndLog("AutoTeamBalance refuses to rebalance teams again so soon.");
return;
}
if (bShowProposedSwitch) {
MidGameRebalance(False); // This will send a message
} else {
if (additionalRequiredRequests==1) { s=""; } else { s="s"; }
if (bFlashRebalanceRequest) {
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') && !p.IsA('Spectator') && !p.IsA('Bot')) {
PlayerPawn(p).ClearProgressMessages();
FlashMessageToPlayer(p,""$additionalRequiredRequests$" more player"$s$" must type !teams for rebalance.",warnColor,FlashLine);
}
}
} else {
// BroadcastRebalanceMessage("I require "$additionalRequiredRequests$" more requests before I will rebalance the teams. Say \"!teams\" if you agree.");
BroadcastRebalanceMessage(""$additionalRequiredRequests$" more player"$s$" must type !teams for rebalance.");
}
}
lastRebalanceRequestTime = Level.TimeSeconds;
}
// After a request for rebalance, whether changes were made or not, show current team strengths to all players.
// if (bBroadcastHelloGoodbye) { BroadcastMessageAndLog("Red team strength is "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$"."); }
BroadcastTeamStrengths();
}
function int Sgn(float n) {
if (n>0)
return +1;
if (n<0)
return -1;
return 0;
}
// If bDo=False, then instead of performing the change, it will instead call ProposeChange() which will message all players to suggest they type "!teams" to make the change happen.
function MidGameRebalance(bool bDo) {
local int redTeamCount,blueTeamCount;
local bool success;
if (!Level.Game.IsA('TeamGamePlus') || !Level.Game.bTeamGame)
return;
if (bDo && !bSuggesting) {
lastBalanceTime = Level.TimeSeconds;
}
if (!bDo) {
ClearAllProgressMessages(); // Only actually needed if we are about to bFlashRebalanceRequest or bFlashOnWarning.
}
redTeamCount = GetTeamSize(0);
blueTeamCount = GetTeamSize(1);
// We assume bot skills are pretty much irrelevant, and the bots will auto-switch to balance teams after we move any players around.
if (redTeamCount==0 && blueTeamCount==0)
return;
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "MidGameRebalance() "$redTeamCount$" v "$blueTeamCount$""); };
// TODO: what if redTeamCount << blueTeamCount ? e.g. it's 6v2 so we need to move two players. we could balance in a while loop if it's guaranteed to end - although the system should really be changed entirely, since it tries to balance strengths on the first switch, it will be harder to keep them balanced on the second switch.
success = True; // will become false only if MidGameTeamBalanceSwitchOnePlayer() is tried and failed.
if (redTeamCount < blueTeamCount) {
success = MidGameTeamBalanceSwitchOnePlayer(bDo,1,0);
} else if (blueTeamCount < redTeamCount) {
success = MidGameTeamBalanceSwitchOnePlayer(bDo,0,1);
}
if ((redTeamCount == blueTeamCount && !bNeverRebalanceWhenTeamsAreEven) || !success) {
success = MidGameTeamBalanceSwitchTwoPlayers(bDo);
}
//// Currently done in RequestMidGameRebalance() or not done if called automatically.
// if (bDo && success) {
// BroadcastTeamStrengths();
// }
}
// set contour
// set cntrparam levels 20
// OLD: splot [5:50][480:1600] (x**1.4) * (y**0.6)
// MMM: splot [5:50][480:1600] (x*1.4)*30 + (y*0.6)
// MMM: splot [5:50][480:1600] (x*1.8)*10 + (y*0.2)
// NEW: splot [5:50][480:1600] (x*1.0)*100 + (y*1.0)
function bool MidGameTeamBalanceSwitchOnePlayer(bool bDo, int fromTeam, int toTeam) {
local float fromTeamStrength, toTeamStrength, currentDifference, playerStrength, teamScoreStrengthDifference;
local Pawn p;
local Pawn closestPlayer; // the most ideal potential player to switch
local float newDifference; // the absolute strength difference between the two teams after the potential switch
local float timeInGame,bestScore,potentialNewDifference,thisScore;
local int playerCountDifference;
fromTeamStrength = GetTeamStrength(fromTeam);
toTeamStrength = GetTeamStrength(toTeam);
currentDifference = fromTeamStrength - toTeamStrength; // Will often be positive, but not always.
playerCountDifference = GetTeamSize(fromTeam) - GetTeamSize(toTeam);
if (currentDifference<0 && playerCountDifference<2) {
// Switching a player to the smaller but stronger team won't help!
return False;
}
teamScoreStrengthDifference = GetFlagStrengthForTeam(fromTeam) - GetFlagStrengthForTeam(toTeam);
if (Abs(currentDifference)= currentDifference && !bForceEvenTeams && CountHumanPlayers()>3) {
// We only decline to switch if #players>3 and we aren't "forcing" even teams and if teams sizes only differ by 1 player.
if (playerCountDifference>=2) {
if (bDo) {
BroadcastRebalanceMessage(""$getTeamName(toTeam)$" looks stronger than "$getTeamName(fromTeam)$". Please consider rebalancing again!");
lastBalanceTime = Level.TimeSeconds - MinSecondsBeforeRebalance; // Make immediate rebalance possible
}
// Proceed to switch.
} else {
// BroadcastRebalanceMessage("Not switching "$closestPlayer.getHumanName()$" because that would make "$getTeamName(toTeam)$" team too strong!");
// BroadcastRebalanceMessage(""$getTeamName(toTeam)$" team would be too strong with "$closestPlayer.getHumanName()$"");
NormalLog(""$getTeamName(toTeam)$" team looks too strong. Considering switching two players...");
return False;
}
}
*/
// We check that the best potential switch is better than current situation.
if (newDifference >= Abs(currentDifference) /*&& CountHumanPlayers()>3*/) {
// But if the #players differs by 2 or more.
if (Abs(playerCountDifference)>=2) {
// We will do this switch anyway, and then do a 2-player switch.
BroadcastRebalanceMessage(""$getTeamName(toTeam)$" team looks too strong. Considering switching three players...");
// Don't return False yet - do the switch and then return False.
} else {
// if (bDo) {
// BroadcastRebalanceMessage(""$getTeamName(toTeam)$" team looks too strong. Considering switching two players...");
// }
// NormalLog("MidGameTeamBalanceSwitchOnePlayer("$ bDo $") failed while "$ GetTeamSize(0) $"v"$ GetTeamSize(1) $" "$ Int(fromTeamStrength) $"v"$ Int(toTeamStrength) $" "$ GetTeamScore(0) $"-"$ GetTeamScore(1) $" diff="$ currentDifference $" bestDiff="$ newDifference $" bestP="$ closestPlayer $" bestScore="$ bestScore $"");
// LogSituation();
return False;
}
}
if (bDo) {
ChangePlayerToTeam(closestPlayer,toTeam,gameStartDone);
// BroadcastTeamStrengths();
} else {
ProposeChange(closestPlayer,None);
}
if (bSuggesting) {
SuggestedChanges = SuggestedChanges $ " ["$ Int(newDifference) $"]";
}
// return True;
// If we actually made strengths worse, but #players better, do a 2-player rebalance now:
return (newDifference <= Abs(currentDifference));
}
function bool MidGameTeamBalanceSwitchTwoPlayers(bool bDo) {
// initial:
local float redTeamStrength, blueTeamStrength, difference, teamScoreStrengthDifference;
// during loop:
local Pawn redP,blueP;
local float redPStrength, bluePStrength,redPTimeInGame,bluePTimeInGame;
local float potentialNewDifference; // the strength difference between the two teams after switching these two players
// best found:
local Pawn redPlayerToMove,bluePlayerToMove; // the best two players found so far
local float bestDifference; // the strength difference between the two teams after switching these players
local float bothTimeInGame;
local float bestScore,thisScore;
local float playerCountDifference;
redTeamStrength = GetTeamStrength(0);
blueTeamStrength = GetTeamStrength(1);
difference = blueTeamStrength - redTeamStrength; // positive implies Team 1 is stronger than Team 0
bestDifference = difference;
bestScore = (60*60) * (3+abs(difference)) * (3+abs(difference)); // 60 minutes, should be large enough.
playerCountDifference = Abs(GetTeamSize(1) - GetTeamSize(0));
teamScoreStrengthDifference = GetFlagStrengthForTeam(1) - GetFlagStrengthForTeam(0);
if (Abs(difference)=2)) {
// This is an improvement on the current situation.
// bothTimeInGame = redPTimeInGame + bluePTimeInGame;
bothTimeInGame = Max(redPTimeInGame,bluePTimeInGame);
bothTimeInGame += 240.0;
// thisScore = bothTimeInGame*(5+potentialNewDifference)*(5+potentialNewDifference);
thisScore = (bothTimeInGame*(0.0 + 2.0*FClamp(PreferenceToSwitchNewPlayers,0,1))) + ((5+potentialNewDifference)*(0.0 + 2.0*FClamp(1.0-PreferenceToSwitchNewPlayers,0,1)))*100;
if (thisScore < bestScore) {
bestScore = thisScore;
bestDifference = potentialNewDifference;
redPlayerToMove = redP;
bluePlayerToMove = blueP;
}
}
}
}
}
}
// CONSIDER: if one of the players is a bot, we should probably move him last, because bots tend to switch back to the other team, if UT.ini is configured that way. Alternatively, we could copy Daniel's temporary-ut-balance-disable code into ChangePlayerToTeam. Hmm probably nobody uses bBalanceBots anyway.
if (redPlayerToMove != None && bluePlayerToMove != None) {
if (bDo) {
ChangePlayerToTeam(redPlayerToMove,1,gameStartDone);
ChangePlayerToTeam(bluePlayerToMove,0,gameStartDone);
// BroadcastTeamStrengths();
} else {
ProposeChange(redPlayerToMove,bluePlayerToMove);
}
if (bSuggesting) {
SuggestedChanges = SuggestedChanges $ " ["$ Int(bestDifference) $"]";
}
return True;
} else {
BroadcastRebalanceMessage("AutoTeamBalance could not find two switches to improve the teams.");
// DONE: Should really log the state now, so we can check the values to debug if neccessary!
// TODO: Once we believe ATB is stable and optimal, we can trust this result and skip the logging!
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "MidGameTeamBalanceSwitchTwoPlayers("$ bDo $") failed while "$ GetTeamSize(0) $"v"$ GetTeamSize(1) $" "$ Int(redTeamStrength) $"v"$ Int(blueTeamStrength) $" "$ GetTeamScore(0) $"-"$ GetTeamScore(1) $" diff="$ difference $" bestDiff="$ bestDifference $" bestScore="$ bestScore $" redP="$ redPlayerToMove $" blueP="$ bluePlayerToMove $""); };
LogSituation();
return False;
}
}
function BroadcastRebalanceMessage(String msg) {
if (gameStartDone) {
BroadcastMessageAndLog(msg);
} // else We are performing pre-game arranging, so don't explain or complain on failure.
}
// Shows all players the request to rebalance with "!teams", and the player(s) who will be moved.
// Only called if bShowProposedSwitch=True.
// Can be caused by a player typing "!teams", or by bWarnMidGameUnbalance.
// one must be a valid player, but two can be None.
function ProposeChange(Pawn one, Pawn two) {
local Pawn p;
local String msg,action;
if (two == None) {
// msg = "Type !teams to move "$one.getHumanName();
msg = "Type !teams to move "$one.getHumanName()$" to "$getTeamName(1-one.PlayerReplicationInfo.Team);
action = one.getHumanName()$" moves to "$getTeamName(1-one.PlayerReplicationInfo.Team);
} else {
msg = "Type !teams to swap "$one.getHumanName()$" with "$two.getHumanName();
action = one.getHumanName()$" and "$two.getHumanName()$" switch";
}
// TODO NOTE: Even with this on, all players still get the Flash "Teams look uneven ..."
if (bOnlyFlashInvolvedPlayers) {
// TODO: These messages might not be accurate, if the player we are flashing to is one of those who has already requested rebalance.
if (two == None) {
FlashMessageToPlayer(one,"Please type !"$Locs(getTeamName(1-one.PlayerReplicationInfo.Team))$" to make the teams even!",warnColor,FlashLine+1);
} else {
FlashMessageToPlayer(one,"Please type !teams to switch team with "$two.getHumanName(),warnColor,FlashLine+1);
FlashMessageToPlayer(two,"Please type !teams to switch team with "$one.getHumanName(),warnColor,FlashLine+1);
}
BroadcastMessageAndLog("Teams may be better if "$action$".");
} else {
if (bFlashRebalanceRequest) {
// for (p=Level.PawnList; p!=None; p=p.NextPawn) {
// if (p.IsA('PlayerPawn') && !p.IsA('Spectator') && !p.IsA('Bot')) {
// FlashMessageToPlayer(p,msg,warnColor,FlashLine+1);
// }
// }
FlashToAllPlayers(msg,warnColor,FlashLine+1);
} else {
BroadcastMessageAndLog(msg);
}
}
}
// ======== Change game or message players: ======== //
function ChangePlayerToTeam(Pawn p, int teamnum, bool bInform) {
local Color msgColor;
local bool oldbNoTeamChanges;
if (p.IsA('Spectator')) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ChangePlayerToTeam("$p.getHumanName()$","$teamnum$"): refusing to change the team of a spectator!"); };
return;
}
if (teamnum == p.PlayerReplicationInfo.Team) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ChangePlayerToTeam("$p.getHumanName()$","$teamnum$"): doing nothing since player is already on team "$teamnum); };
return;
}
if (teamnum<0 || (TeamGamePlus(Level.Game)!=None && teamnum>=TeamGamePlus(Level.Game).MaxTeams)) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ChangePlayerToTeam("$p.getHumanName()$","$teamnum$"): WARN FAIL teamnum must be in range 0-" $ (TeamGamePlus(Level.Game).MaxTeams - 1) $ "."); };
return;
}
if (bSuggesting) {
if (SuggestedChanges != "")
SuggestedChanges = SuggestedChanges $ ", ";
SuggestedChanges = SuggestedChanges $ p.getHumanName()$" to "$getTeamName(teamnum);
return; // Do not actually switch team
}
if (p.IsA('Bot')) {
Bot(p).ConsoleCommand("taunt wave");
}
if (TeamGamePlus(Level.Game) != None) {
oldbNoTeamChanges = TeamGamePlus(Level.Game).bNoTeamChanges;
TeamGamePlus(Level.Game).bNoTeamChanges = False;
}
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ChangePlayerToTeam("$p.getHumanName()$","$teamNum$"): "$p.PlayerReplicationInfo.Team$" -> "$teamnum$""); };
Level.Game.ChangeTeam(p,teamnum); // TODO: suppress the BroadcastMessage() made by TeamGame.AddToTeam() when we are flashing team to player elsewhere anyway
if (TeamGamePlus(Level.Game) != None) {
TeamGamePlus(Level.Game).bNoTeamChanges = oldbNoTeamChanges;
}
// Kill the player, forcing them to drop flag if they have it (before this we could get a red player holding the red flag!)
p.Died(None, '', p.Location);
// Recompensate player for suicide/death points:
if (gameStartDone && !DeathMatchPlus(Level.Game).bTournament) {
// p.KillCount++; // Did not work
// Maybe this is unneccessary if we don't cause them to suicide.
p.PlayerReplicationInfo.Score += 1.0;
// We refuse to reduce Deaths to 0, so the first one will stay counted. If not, it can cause some confusion with any mutators that expect players to spawn only once with Deaths==0.
// p.PlayerReplicationInfo.Deaths -= 1;
// It may be that Deaths changes if the game has started, but not during countdown/pause stage. We wish to undo whatever is done.
// Best solution is probably to copy his Deaths before and write them again after. Score too.
}
if (bInform) {
switch (teamnum) {
case 0: msgColor = colorRed; break;
case 1: msgColor = colorBlue; break;
case 2: msgColor = colorGreen; break;
case 3: msgColor = colorYellow; break;
default: msgColor = colorWhite; break;
}
BroadcastMessage(p.getHumanName()$" has been moved to the "$getTeamName(teamnum)$" team.");
PlayerPawn(p).ClearProgressMessages();
FlashMessageToPlayer(p,"You have been moved to the "$Caps(getTeamName(teamnum))$" team!",msgColor,3); // BUG: Unfortunately this message is soon hidden by the scoreboard, which is displayed automatically when a player dies, so we also send a message to their console:
// PlayerPawn(p).ClientMessage("You have been moved to the "$Caps(getTeamName(teamnum))$" team!");
if (bShakeWhenMoved) {
p.ShakeView(2.0,2000.0,0.0);
}
if (TeamspeakChannel[teamnum]!="" && /*DeathMatchPlus(Level.Game).bTournament &&*/ bHelpInPugs) {
// todo: && someone else has used !TS in the last hour
FlashMessageToPlayer(p,"Type !TS to change teamspeak channel.",colorWhite,5);
}
}
//// I'm going to try NOT doing this, and see if now switching two players always works ok. ATB was sometimes switching two players, but one of them was not getting switched.
// if (gameStartDone) {
// FixTeamsizeBug();
// }
}
// For debugging I want some calls to BroadcastMessage() to be logged on the server, so that I can see without playing how much the players are getting spammed by broadcasts.
// Eventually, calls to BroadcastMessageAndLog could be turned back to just BroadcastMessage() calls.
// If you really really want to log, use BroadcastMessageAndAlwaysLog.
function BroadcastMessageAndLog(string Msg) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Broadcasting: "$Msg); };
BroadcastMessage(Msg);
}
function BroadcastMessageAndAlwaysLog(string Msg) {
Log("[ATB] "$Msg);
BroadcastMessage(Msg);
}
function FlashMessageToPlayer(Pawn p, string Msg, Color msgColor, optional int linenum) {
if (PlayerPawn(p)==None)
return; // Don't flash messages to bots
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Flashing message to "$p.getHumanName()$": "$Msg); };
// p.ClientMessage(Msg, 'CriticalEvent', False); // goes to HUD and console, no beep
// Coloured messages, with our own choice of colour and timeout:
if (linenum == 0)
linenum = FlashLine;
// p.ClearProgressMessages();
// p.SetProgressTime(4);
PlayerPawn(p).SetProgressTime(5);
PlayerPawn(p).SetProgressColor(msgColor,linenum);
PlayerPawn(p).SetProgressMessage(Msg,linenum);
if (gameStartDone) { // Prevent multiple (and badly overlapping) beeps during the multiple Flashes at the start of the game
p.PlaySound(sound'Beep', SLOT_Interface, 2.5, False, 32, 32); // we play our own sound
}
}
function FlashToAllPlayers(String Msg, Color msgColor, optional int linenum) {
local Pawn p;
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FlashToAllPlayers("$linenum$"): Flashing \""$Msg$"\""); };
// foreach AllActors(class'PlayerPawn',P) { // TODO: should use Level.PawnList
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') && !p.IsA('Spectator')) {
FlashMessageToPlayer(p,Msg,msgColor,linenum);
}
}
}
// ======== Library functions which do not change any state: ======== //
function bool ShouldBalance(GameInfo game) {
// Never balance in tournament mode
if (DeathMatchPlus(Level.Game).bTournament && !bHelpInPugs)
return False;
// We can't balance if it's not a teamgame
if (!Level.Game.GameReplicationInfo.bTeamGame)
return False;
if (Level.Game.IsA('CTFGame'))
return bAutoBalanceTeamsForCTF;
if (String(Level.Game.Class) == "Botpack.TeamGamePlus")
return bAutoBalanceTeamsForTDM;
if (Level.Game.IsA('Assault')) {
// Do not balance AS game if we're in the second half of the game
if (Assault(Level.Game).Part != 1)
return False;
else
return bAutoBalanceTeamsForAS;
}
// OK so it's an unknown teamgame
return bAutoBalanceTeamsForOtherTeamGames;
}
function bool ShouldUpdateStats(GameInfo game) {
if (Level.Game.IsA('CTFGame'))
return bUpdatePlayerStatsForCTF;
if (String(Level.Game.Class) == "Botpack.TeamGamePlus")
return bUpdatePlayerStatsForTDM;
if (Level.Game.Class.IsA('Assault'))
return bUpdatePlayerStatsForAS;
// OK so it's not CTF or TDM or AS, but is it another type of team game?
if (Level.Game.GameReplicationInfo.bTeamGame) // it's probably a subclass of TeamGamePlus
return bUpdatePlayerStatsForOtherTeamGames;
return bUpdatePlayerStatsForNonTeamGames;
}
function bool AllowedToBalance(Pawn b) {
if (b.IsA('Bot'))
return bBalanceBots;
else
return b.IsA('PlayerPawn') && !b.IsA('Spectator');
}
// Checks that the player is a human, or a bot when bRankBots is set. Does not check whether the human player is a spectator.
function bool AllowedToRank(Pawn b) {
if (b.IsA('Bot'))
return bRankBots;
else
return b.IsA('PlayerPawn');
}
// This is used for checking and performing mid-game teambalance. It never counts bots.
function int GetTeamSize(int team) {
local int count;
local Pawn p;
count = 0;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') && !p.IsA('Spectator') && p.PlayerReplicationInfo.Team == team) count++;
}
return count;
}
function int CountHumanPlayers() {
local Pawn p;
local int countHumanPlayers;
countHumanPlayers = 0;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.bIsPlayer && !p.IsA('Spectator') && !p.IsA('Bot') && p.IsA('PlayerPawn') && p.bIsHuman) { // maybe the last 2 are not needed
countHumanPlayers++;
}
}
return countHumanPlayers;
}
function String getTeamName(int teamNum) {
if (TeamGamePlus(Level.Game)!=None)
return TeamGamePlus(Level.Game).Teams[teamNum].TeamName;
else
return "None";
}
// Team strength is the sum of all players on that team, plus caps*FlagStrength (or other teamscore).
function float GetTeamStrength(int teamNum) {
// Add flagstrength:
return GetTeamStrengthNoFlagStrength(teamNum) + GetFlagStrengthForTeam(teamNum);
}
function float GetTeamScore(int teamNum) {
return TournamentGameReplicationInfo(Level.Game.GameReplicationInfo).Teams[teamNum].Score;
}
function float GetFlagStrengthForTeam(int teamNum) {
return GetTeamScore(teamNum) * GetFlagStrength();
}
function float GetTeamStrengthNoFlagStrength(int teamNum) {
local Pawn p;
local float strength;
strength = 0;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.bIsPlayer && !p.IsA('Spectator') && p.PlayerReplicationInfo.Team == teamNum) {
strength += GetPlayerStrength(p);
}
}
return strength;
}
// Scale FlagStrength, so it is appropriate for non-CTF gametypes:
// Some common GoalTeamScores are: CTF 7 | (DM 30) | TDM 100 | DOM 100 | Siege 20/30 | Unknown 150
function float GetFlagStrength() {
if (CountHumanPlayers() < 3)
return 0; // Hopefully fixes the bug that in a 2v0, it was refusing to move either player to the "stronger" team!
if (Level.Game.IsA('CTFGame'))
return FlagStrength;
if (String(Level.Game.Class) == "Botpack.TeamGamePlus") // TDM
return Float(FlagStrength)/14.0;
if (Level.Game.IsA('Domination'))
return Float(FlagStrength)/14.0;
if (Level.Game.IsA('Assault'))
return 0;
if (StrAfter(String(Level.Game.Class),".") == "SiegeGI")
return Float(FlagStrength)/4.0;
// Unknown gametype; assume GoalTeamScore 150
return Float(FlagStrength)/21.0;
}
// Returns the strength of a player
// If we are using proportional strength estimation (from current game and from player record) then mix the values.
// TODO: We should lean the proportion more towards current game, if known time for recorded player strength is <5 minutes.
function float GetPlayerStrength(Pawn p) {
local float timeInGame;
if (StrengthProportionFromCurrentGame >= 1.0) {
return NormaliseScore(GetScoreForPlayer(p));
}
if (StrengthProportionFromCurrentGame <= 0.0) {
return GetRecordedPlayerStrength(p);
}
if (gameStartDone) {
timeInGame = Level.TimeSeconds - p.PlayerReplicationInfo.StartTime;
if (timeInGame > 180) {
return NormaliseScore(GetScoreForPlayer(p)) * StrengthProportionFromCurrentGame + GetRecordedPlayerStrength(p) * (1.0 - StrengthProportionFromCurrentGame);
}
}
// We can't mix the values yet because the game hasn't started or the player has only just joined, so we must:
return GetRecordedPlayerStrength(p);
}
// Returns the recorded strength of a player
function float GetRecordedPlayerStrength(Pawn p) {
local int found;
if (!AllowedToRank(p) && !AllowedToBalance(p)) {
return BotStrength;
}
found = FindPlayerRecordGuaranteed(p);
if (found == -1) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Using UnknownStrength "$UnknownStrength$" for "$p.getHumanName()); };
return UnknownStrength; // unknown player or player is too weak for list (should never happen - ok with STAGGER_LOOKUPS now it can happen!)
} else {
if (avg_score[found] < 0) {
; ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "* " $ "Player "$p.getHumanName()$" had negative avg_score="$avg_score[found]$" so resetting to 0."); };;
avg_score[found] = 0;
}
// If the amount of time we have observed the player for is quite short, then their strength can be innaccurate.
// (This can cause problems e.g. with a player who played for 3 minutes and got a good score - next game his strength will be 150!)
// So if he has played for less than an hour, interpolate his strength between default server average and observed strength, according to time played.
if (hours_played[found] < 1.0) { // Note, due to equation below, this must stay at 1.0!
return hours_played[found]*avg_score[found] + (1.0-hours_played[found])*NormalisedStrength; // Consider: instead of NormalisedStrength we could use averagePlayerStrengthThisGame.
} else {
return avg_score[found]; // player's recorded strength
}
}
}
// Find player by name, or partial name
function Pawn FindPlayerNamed(String name) {
local Pawn p;
local Pawn found;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') || p.IsA('Bot')) {
if (p.getHumanName() ~= name) { // exact case insensitive match, return player
return p;
}
if (Instr(Caps(p.getHumanName()),Caps(name))>=0) { // partial match, remember it but keep searching for exact match
found = p;
}
}
}
return found; // return partial match, or None
}
// Find player by name, or partial name
function Pawn FindPlayerWithID(int id) {
local Pawn p;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (p.IsA('PlayerPawn') || p.IsA('Bot')) {
if (PlayerPawn(p).PlayerReplicationInfo.PlayerID == id) {
return p;
}
}
}
return None;
}
// ======== Player database: ======== //
// Copies from playerData[] to ip[],nick[],avg_score[],... (should be done at the start)
function CopyConfigIntoArrays() {
local int field;
local int i;
local String data;
local String args[256];
CopyConfigDone=True;
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "CopyConfigIntoArrays() "$GetDate()$" running"); };
for (i=0; i BN
// str = str $ Left(String(m.Class),1) $ Left(StrAfter(String(m.Class),"."),1);
// NEW METHOD: Select only capitalised parts of the class name: e.g. WhoPushedMe.WhoPushedMe => WPM
// Possible BUG: People *may* write mutators that are not capitalised, in which case those mutators will generate no signature.
// However, we can't change the signature now, without breaking the nicks for admins upgrading from earlier versions of ATB (although some player strengths might be retained via ip-matching)
tmpstr = StrAfter(String(m.Class),".");
for (i=0;i=Asc("A") && c<=Asc("Z")) {
str = str $ Chr(c);
}
}
m = m.NextMutator;
if (m != None) {
str = str $ "+";
}
}
}
return str;
}
function int FindPlayerRecord(Pawn p) {
return FindPlayerRecordGuaranteed(p);
}
// function int FindPlayerRecordGuaranteed(Pawn p)
//
// Will always return a valid exact record index, creating a new record if neccessary.
//
// For speed, this implementation keeps the record at position
// p.PlayerReplicationInfo.PlayerID in the database, switching with another
// record in that spot if necessary. It calls FindPlayerRecordNoFastHash() to
// do the actual lookup. This makes it possible to call FindPlayerRecord(p)
// frequently and efficiently.
function int FindPlayerRecordGuaranteed(Pawn p) {
local int pid,i;
local int found;
// i = p.PlayerReplicationInfo.PlayerID % MaxPlayerData;
pid = p.PlayerReplicationInfo.PlayerID % 64;
// BUG TODO: When bRankBots=True (or bBalanceBots=True?), all the bots have i = 1635,
// they all override that record and none of them optimise.
// Is the player's record already at i?
if (bCached[pid] > 0) {
// if (GetDBName(p) == nick[pid] && getIP(p) == ip[pid]) {
// DebugLog("FindPlayerRecord(p) FAST EXACT match for "$nick[pid]$","$ip[pid]$": ["$pid$"] ("$avg_score[pid]$","$hours_played[pid]$","$date_last_played[pid]$")");
return pid;
}
// Is there an exact or partial match for this player in the database?
found = FindPlayerRecordNoFastHash(p);
// If an exact record for the player was found, move it to index pid for the rest of this game (by swapping it with whichever record is there). This will make lookups more efficient during the rest of the game.
if (found != -1 && GetDBName(p) == nick[found] && getIP(p) == ip[found]) {
SwapPlayerRecords(pid,found);
bCached[pid] = 1;
return pid;
}
// No exact record for the player was found; we have performed a full search of the database :|
if (found > -1) {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecord() PARTIAL match for "$GetDBName(p)$" @ "$getIP(p)$": "$nick[found]); };
} else {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecord() FAILED match for "$GetDBName(p)$" @ "$getIP(p)$"."); };
}
// Let's create a new record for this player+ip, to avoid doing that again.
i = CreateNewPlayerRecord(p); // i=unknown, but the new record will be optimally indexed the next time FindPlayerRecord() is called.
if (found > -1) {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecord(p) COPY ["$i$"] <- ["$found$"]"); };
// Copy over strength from the partial-match player, but partially reset their time, to make their old strength last for max MaxHoursWhenCopyingOldRecord hours.
avg_score[i] = avg_score[found]; // Copy score from partial match record
hours_played[i] = Min(MaxHoursWhenCopyingOldRecord,hours_played[found]);
// date_last_played[i] = "copied_from_"$nick[found]$":"$ip[found]; // should get set before being written
date_last_played[i] = GetDate();
// SO: changing nick or IP will NOT reset your avg_score immediately, but after some hours of play your old record will only count for 50%. This helps to protect players who were matched incorrectly. (Different members of a family playing from the same IP, or different players using the same nick.)
// Optionally log/broadcast the fakenicker, now only if IP was matched but nick is different.
if (!(GetDBName(p) ~= nick[i])) {
if (bLogFakenickers) { ; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Fakenicker "$p.getHumanName()$" was previously "$nick[found]$" (ip "$ip[found]$")");; }
if (bBroadcastFakenickers) { BroadcastMessage(p.getHumanName()$" was previously "$nick[found]$" (ip "$ip[found]$")"); }
}
}
if (i != pid) {
SwapPlayerRecords(pid,i);
bCached[pid] = 1;
return pid;
}
return i; // if we didn't copy any stats over, he will have UnknownStrength, the same as when we returned -1
}
function SwapPlayerRecords(int i,int j) {
local string tmp_rkey;
local string tmp_player_nick, tmp_player_ip;
local float tmp_avg_score, tmp_hours_played;
local string tmp_date_last_played;
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "SwapPlayerRecords() Swapping records "$ j $" <-> "$ i $" ("$ nick[j] $":"$ ip[j] $"("$ Int(avg_score[j]) $") <-> "$ nick[i] $":"$ ip[i] $"("$ Int(avg_score[i]) $"))"); };
// Swap record [i] for record [j]:
tmp_rkey = rkey[i];
tmp_player_nick = nick[i];
tmp_player_ip = ip[i];
tmp_avg_score = avg_score[i];
tmp_hours_played = hours_played[i];
tmp_date_last_played = date_last_played[i];
rkey[i] = rkey[j];
nick[i] = nick[j];
ip[i] = ip[j];
avg_score[i] = avg_score[j];
hours_played[i] = hours_played[j];
date_last_played[i] = date_last_played[j];
rkey[j] = tmp_rkey;
nick[j] = tmp_player_nick;
ip[j] = tmp_player_ip;
avg_score[j] = tmp_avg_score;
hours_played[j] = tmp_hours_played;
date_last_played[j] = tmp_date_last_played;
}
// If an exact match for the player exists, return the index
// If not, return the index of a record with matching nick, or (preferably) matching ip
// If not, return -1
function int FindPlayerRecordNoFastHash(Pawn p) {
local int found;
local int i;
local string player_nick;
local string player_ip;
local bool bNickMatches;
local bool bIPMatches;
local float bestDate;
player_nick = GetDBName(p);
player_ip = getIP(p);
// If there are multiple partially matching records, take the most recent one.
// This should give a more up-to-date strength, and importantly ensures old older partial records will be thrown away rather than refreshed.
// #define BetterThanCurrent NumFromDateString(date_last_played[i]) > NumFromDateString(date_last_played[found])
found = -1;
for (i=0;i= 0) { ; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecordNoFastHash(p) DUPLICATE IP match for "$player_nick$","$player_ip$": ["$found$"] "$nick[i]$" ("$avg_score[found]$","$hours_played[found]$","$date_last_played[found]$")");; }
if (found == -1 || (NumFromDateString(date_last_played[i]) > bestDate)) {
found = i; // matching ip
bestDate = NumFromDateString(date_last_played[found]);
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecordNoFastHash(p) IP match for "$player_nick$","$player_ip$": ["$found$"] "$nick[i]$" ("$avg_score[found]$","$hours_played[found]$","$date_last_played[found]$")"); };
}
} else if (bNickMatches /* && found == -1 */ ) { // the part commented out was to prefer matching_ip+different_nick over matching_nick+different_ip
if (False && found >= 0) { ; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecordNoFastHash(p) DUPLICATE NICK match for "$player_nick$","$player_ip$": ["$found$"] "$nick[i]$" ("$avg_score[found]$","$hours_played[found]$","$date_last_played[found]$")");; }
if (found == -1 || (NumFromDateString(date_last_played[i]) > bestDate)) {
found = i;
bestDate = NumFromDateString(date_last_played[found]);
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecordNoFastHash(p) NICK match for "$player_nick$","$player_ip$": ["$found$"] "$ip[found]$" ("$avg_score[found]$","$hours_played[found]$"),"$date_last_played[found]$""); };
}
}
// CONSIDER: if an uneven match, choose a match with more experience (hours_played)
// CONSIDER: even better, average the strengths of all partial-matches (maybe the same nick many times on different IPs, or the same IP with many different nicks), weighted by hours_played
// CONSIDER (elsewhere): if we have little experience (<10mins) of a player, return UnknownStrength anyway?
}
if (found == -1) { ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindPlayerRecordNoFastHash("$player_nick$","$player_ip$") failed to return a record."); }; }
return found;
}
// Creates a new record in the DB for the player provided, returning its index. If p == None, then returns the index of an empty record. Should not be called without first checking whether the player in already in the DB!
function int CreateNewPlayerRecord(Pawn p) {
local int pos;
local int returned;
pos = -1;
// #ifdef CLEANUP14
// KEEP_EARLY_RECORDS_EMPTY has happened!
// Bah who cares, let's always scan for an empty one!
pos = FindEmptyPlayerRecordFast();
// #endif
if (pos<0 || pos >= MaxPlayerData) { // all records were full
// DONE: find the record with lowest hours_played and replace that one
// DONE: better, find the oldest record and replace it (we need last_date_played for that)
// TODO: first seek "oldest player record with min play-time", but if it fails, find "oldest player record"
//// This is what we should do (best yet guaranteed)
pos = FindOldPlayerRecordFastDuringGame();
}
if (bLogDeletedRecords && !( nick[pos]=="" && ip[pos]=="" )) {
; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "CreateNewPlayerRecord() true" /*"$ gameStartDone*/ $" DEL ["$pos$"] "$ nick[pos] $" "$ ip[pos] $" "$ avg_score[pos] $" "$ hours_played[pos] $" "$ date_last_played[pos] $" (score "$ FindOldestPlayerRecordMeasure(pos) $")");;
}
if (pos<64)
bCached[pos] = 0; // DEBUGGING: if his bCached had been 1, this might be a live overwrite :f
// Check for PRECLEAR_SOME_RECORDS which we believe to be dangerous! :P
if (p == None) {
ClearRecord(pos);
} else {
// Copy the pawn's vital data into his record before returning.
InitialiseRecord(pos,p);
}
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "CreateNewPlayerRecord() NEW ["$pos$"] "$ nick[pos] $" "$ ip[pos] $" "$ avg_score[pos] $" "$ hours_played[pos] $" "$ date_last_played[pos] $" (score "$ FindOldestPlayerRecordMeasure(pos) $")"); };
// if (bBroadcastCookies) { BroadcastMessageAndLog("Welcome "$ nick[pos] $"! You have "$ avg_score[pos] $" cookies."); }
return pos;
}
function InitialiseRecord(int i, Pawn p) {
ip[i] = getIP(p);
nick[i] = GetDBName(p);
avg_score[i] = UnknownStrength;
hours_played[i] = 0; // UnknownMinutes/60; // CONSIDER: using some UnknownMinutes might be better, for players who play only for a short time and get an unrepresentative strength for the next game - with UnknownMinutes their strength will be closer to the average, hence balancing will concentrate more on players we know about.
// date_last_played[i] = "fresh_record";
date_last_played[i] = GetDate();
}
function ClearRecord(int i) {
if (bLogDeletedRecords) {
if (nick[i]!="" || ip[i]!="" || avg_score[i]!=0 || hours_played[i]!=0 || date_last_played[i]!="") {
; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "CLEAR ["$i$"] "$ rkey[i] $" "$ nick[i] $" "$ ip[i] $" "$ avg_score[i] $" "$ hours_played[i] $" "$ date_last_played[i] $" (score "$ FindOldestPlayerRecordMeasure(i) $")");;
}
}
rkey[i] = "";
ip[i] = "";
nick[i] = "";
avg_score[i] = 0;
hours_played[i] = 0;
date_last_played[i] = "";
}
function int CreateNewPlayerRecordInnerBatch(int posStart) {
local int pos;
// Find an empty slot:
for (pos=posStart;pos bestAge && hours_played[i] < bestHours)) {
// if (j == 0 || ((0.1+hours_played[i])/(1.0+age) < (0.1+bestHours)/(1.0+bestAge))) {
newScore = (0.1+hours_played[i])/(1.0+age); // large scores are good; records with small scores can be recycled
if (j == 0 || newScore < bestScore) {
bestI = i;
// bestAge = age;
// bestHours = hours_played[i];
bestScore = newScore;
}
}
return bestI;
}
// Finds an old player record which we can replace.
function int FindOldestPlayerRecordSlow() {
local int i,found;
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FindOldestPlayerRecordSlow() Looking for an old record to replace..."); };
// currentDateDays = DaysFromDateString(GetDate()); // Now doing this earlier, in PostBeginPlay().
found = 0;
for (i=1;i age "$ age $" / "$ (hours_played[i]+1.0) $" hours = score "$ -age/(hours_played[i]+1.0) ); }
return -age/(hours_played[i]+1.0); // 1.0 avoids division by 0, and ensures a reasonable score for new records, so they aren't immediately re-recycled.
// Alternatively:
// return DaysFromDateString(date_last_played[i]) * hours_played[i];
}
// Provides ordering of dates, but not accurate spreading.
function float NumFromDateString(String str) {
// str = StrReplace(str,"-","");
// str = StrReplace(str,":","");
// str = StrReplace(str,"/","");
str = StrFilterNum(str);
if (FRand()<0.001) { ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "NumFromDateString() "$str$" -> "$Float(str)); }; }
return Float(str);
// NOTE: float is not all that accurate; it cannot see the time-of-day:
// 200701010000 -> 200701018112.000000
}
// Returns days since 1st/Jan/1970 from a string like: 2008/01/19-13:17.08
function float DaysFromDateString(String datestr) {
local int year,month,day,hour,minute,second;
local float days;
local String str;
str = StrFilterNum(datestr);
if (str == "")
return 0; // No date => 1st/Jan/1970
year = Int(Mid(str, 0,4)) - 1977;
month = Int(Mid(str, 4,2)) - 1;
day = Int(Mid(str, 6,2)) - 1;
days = day + 365.25*month/12 + 365.25*year;
// This is called so many times, for efficiency, we skip hours:minutes:seconds, since they are not really relevant.
hour = Int(Mid(str, 8,2));
minute = Int(Mid(str,10,2));
second = Int(Mid(str,12,2));
days = days + hour/24 + minute/24/60 + second/24/60/60;
// #ifdef DebugLog
// if (bDebugLogging && FRand()<0.002) { DebugLog("DaysFromDateString() "$str$" -> "$year$"/"$month$"/"$day$"-"$hour$":"$minute$":"$second$" -> "$days); }
// #endif
return days;
}
/*
// Int is not large enough; I get: NumFromDateString() 200701010000 -> -1162452912
function int NumFromDateString(String str) {
// str = StrReplace(str,"-","");
// str = StrReplace(str,":","");
// str = StrReplace(str,"/","");
str = StrFilterNum(str);
if (FRand()<0.01) { DebugLog("NumFromDateString() "$str$" -> "$Int(str)); }
return Int(str);
// NOTE: float is not all that accurate; it cannot see the time-of-day:
// 200701010000 -> 200701018112.000000
}
*/
// =========== Updating Stats on player database: =========== //
function UpdateStatsAtEndOfGame() {
local Pawn p;
local int i;
// We know this is going to lag, and we don't care because it's the end of the game. But it prev.nts other things from getting logged as lag, when really we know it is this. :)
// Do not update stats for games with -1 && bLogExtraStats) { ; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "LogEndStats: "$p.PlayerReplicationInfo.Team$" "$p.getHumanName()$" "$getIP(p)$" "$p.PlayerReplicationInfo.Ping$" "$p.PlayerReplicationInfo.PacketLoss$" "$avg_score[i]$" "$hours_played[i]$" "$date_last_played[i]$" "$p.PlayerReplicationInfo.Score$" "$p.KillCount$" "$p.PlayerReplicationInfo.Deaths$" "$p.ItemCount$" "$p.Spree$" "$p.SecretCount$" "$(Level.TimeSeconds - p.PlayerReplicationInfo.StartTime)$"");; }
}
}
LastUpdate = GetDate() $ " on " $ StrBefore(""$Level.Game,".");
CopyArraysIntoConfig();
SaveConfig();
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "UpdateStatsAtEndOfGame() done"); };
// Cancel the log_lag. We know this will lag and we don't care.
}
function TeamInfo GetWinningTeam() {
local int i;
local Pawn p;
local TeamGamePlus thisTeamGame;
local TeamInfo WinningTeam;
// We can't find a winning team if it's not a teamgame!
if (!Level.Game.GameReplicationInfo.bTeamGame) return None;
thisTeamGame = TeamGamePlus(Level.Game);
// Which team won?
// Copied from CTFGame.SetEndCams(), and looks functionally identical to the method in TeamGamePlus.
for ( i=0; i WinningTeam.Score) )
WinningTeam = thisTeamGame.Teams[i];
// Check for tie:
for ( i=0; i to >= so if you tie with another player, you lose out!
if ( (ScaleToFullTime(p)*p.PlayerReplicationInfo.Score) >= (ScaleToFullTime(other)*other.PlayerReplicationInfo.Score) ) {
playersAbove++;
} else {
playersBelow++;
}
}
}
return 100 * playersBelow / (playersBelow + playersAbove);
}
// Returns the score the player will be awarded for this game, depending on the scoring method, and scaled up to full game time. Note that score normalisation is done elsewhere.
function float GetScoreForPlayer(Pawn p) {
local float award_score;
if (ScoringMethod == 0) {
award_score = p.PlayerReplicationInfo.Score * ScaleToFullTime(p);
} else if (ScoringMethod == 1) {
award_score = p.KillCount * ScaleToFullTime(p);
} else if (ScoringMethod == 2) {
award_score = ScaleToFullTime(p) * (p.KillCount + p.PlayerReplicationInfo.Score) / 2.0;
} else if (ScoringMethod == 3) {
award_score = GetRankingPoints(p); // Note that for this method, scaling score to full time is done *inside* GetRankingPoints()
} else if (ScoringMethod >= 4) {
award_score = ScaleToFullTime(p) * (3*p.KillCount + p.PlayerReplicationInfo.Score) / 4.0;
}
// Siege can give dodgy scores. Sometimes HUGE negative numbers, or leech
// games produce unrepresentatively high numbers.
if (award_score < -1000000) {
award_score = ScoreThresholdHigh;
}
if (award_scoreScoreThresholdHigh) {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Adjusting "$ p.getHumanName() $"'s extreme score "$award_score); };
award_score = FClamp(award_score,ScoreThresholdLow,ScoreThresholdHigh);
}
return award_score;
}
function int UpdateStatsForPlayer(Pawn p) {
local int i,j;
local float current_score;
local float old_hours_played;
local float new_hours_played;
local float hours_played_this_game;
local int previousPolls;
local int gameDuration;
local int timeInGame;
local float weightScore;
local float previous_average;
i = FindPlayerRecord(p); // guaranteed to return a record.
if (i == -1) {
; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "UpdateStatsForPlayer() FAILED to find a record for "$ GetDBName(p) $" "$ getIP(p)); }; // probably we don't have his idc
return -1;
}
gameDuration = Level.TimeSeconds - timeGameStarted;
timeInGame = Level.TimeSeconds - p.PlayerReplicationInfo.StartTime;
if (timeInGame>gameDuration)
timeInGame = gameDuration;
if (timeInGame < 60) { // The player has been in the game for less than 1 minute.
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "UpdateStatsForPlayer("$p$") Not updating this player since his timeInGame "$timeInGame$" < 60s."); };
return i;
}
hours_played_this_game = Float(timeInGame)/60.0/60.0;
current_score = GetScoreForPlayer(p);
if (!DeathMatchPlus(Level.Game).bTournament && WinningTeamBonus!=0
&& timeInGame>180 && IsOnWinningTeam(p)
) {
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "UpdateStatsForPlayer() giving bonus "$ WinningTeamBonus $" to "$p.getHumanName()$"."); };
// p.PlayerReplicationInfo.Score += WinningTeamBonus;
// p.ClientMessage("You got "$WinningTeamBonus$" bonus points for finishing on the winning team.",'Pickup',False);
current_score += WinningTeamBonus;
}
// Normalisation, or not:
// ScoringMethod 3 requires no normalisation.
if (ScoringMethod != 3) {
// GetScoreForPlayer() has already scaled players scores up to the full length of this game.
if (bNormaliseScores) {
current_score = NormaliseScore(current_score); // to get an average score of 50 (different now that we use bRelativeNormalisation)
} else {
// If we are not normalising the scores, then we have something like the end-game scores.
// But if this was a short game, scores will probably be lower, so we
// scale the scores up to what they might have been if the game had gone the full (assumed) 20 minutes.
current_score = current_score * 20.0/60.0 / (Level.TimeSeconds-timeGameStarted);
}
// In fact we could just scale to 20 minutes in ScaleToFullTime(), and avoid doing it here, since normalisation doesn't care about that scalar.
}
old_hours_played = hours_played[i];
if (old_hours_played > HoursBeforeRecyclingStrength) {
old_hours_played = HoursBeforeRecyclingStrength;
}
new_hours_played = old_hours_played + hours_played_this_game;
previous_average = avg_score[i];
; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "UpdateStatsForPlayer(p) ["$i$"] "$p.getHumanName()$" avg_score = ( ("$avg_score[i]$" * "$old_hours_played$") + "$current_score$"*"$hours_played_this_game$") / "$(new_hours_played)); };
avg_score[i] = ( (avg_score[i] * old_hours_played) + current_score*hours_played_this_game) / new_hours_played;
hours_played[i] += hours_played_this_game;
date_last_played[i] = GetDate();
if (hours_played[i] > hours_played_this_game+0.5) { // Too spammy if we have little data on a player.
if (avg_score[i]>previous_average+2) {
if (bReportStrengthAsCookies) {
if (bBroadcastCookies) { BroadcastMessageAndLog(""$ p.getHumanName() $" has earned "$ Int(avg_score[i]-previous_average) $" cookies!"); }
if (bFlashCookies) { FlashMessageToPlayer(p,"You earned "$ Int(avg_score[i]-previous_average) $" cookies this game.",strengthColor,3); } // BUG: unfortunately hidden by scoreboard, but still appears in console
} else {
if (bBroadcastCookies) { BroadcastMessageAndLog(""$ p.getHumanName() $" has gained "$ Int(avg_score[i]-previous_average) $" points of strength!"); }
if (bFlashCookies) { FlashMessageToPlayer(p,"Your strength increased by "$ Int(avg_score[i]-previous_average) $" points this game.",strengthColor,3); } // BUG: unfortunately hidden by scoreboard, but still appears in console
}
}
else if (previous_average>avg_score[i]+2) {
if (bReportStrengthAsCookies) {
if (bBroadcastCookies) { BroadcastMessageAndLog(""$ p.getHumanName() $" has lost "$ Int(previous_average-avg_score[i]) $" cookies."); }
if (bFlashCookies) { FlashMessageToPlayer(p,"You lost "$ Int(previous_average-avg_score[i]) $" cookies this game.",strengthColor,3); } // BUG: unfortunately hidden by scoreboard, but still appears in console
} else {
if (bBroadcastCookies) { BroadcastMessageAndLog(""$ p.getHumanName() $" has lost "$ Int(previous_average-avg_score[i]) $" points of strength."); }
if (bFlashCookies) { FlashMessageToPlayer(p,"Your strength decreased by "$ Int(previous_average-avg_score[i]) $" points this game.",strengthColor,3); } // BUG: unfortunately hidden by scoreboard, but still appears in console
}
}
}
return i;
}
function GetAveragesThisGame() {
local Pawn p;
local int playerCount;
if (Level.TimeSeconds <= LastCalculatedAverages + 5)
return;
playerCount = 0;
averageGameScore = 0.0;
averagePlayerStrengthThisGame = 0.0;
for (p=Level.PawnList; p!=None; p=p.NextPawn) {
if (!p.IsA('Spectator') && (AllowedToRank(p) || AllowedToBalance(p))) {
averageGameScore += GetScoreForPlayer(p);
averagePlayerStrengthThisGame += GetRecordedPlayerStrength(p);
playerCount++;
}
}
if (playerCount == 0) {
averageGameScore = 45.678;
averagePlayerStrengthThisGame = UnknownStrength;
} else {
averageGameScore = averageGameScore / Float(playerCount);
averagePlayerStrengthThisGame = averagePlayerStrengthThisGame / Float(playerCount);
}
LastCalculatedAverages = Level.TimeSeconds;
}
// Normalises a player's score so that the average output score will be NormalisedStrength (or with bRelativeNormalisation, the average strength of current players on the server).
// This is to fix the problem that some games (e.g. 2v2 w00t or PureAction or iG) have much higher scores than others, which will confuse the stats.
function float NormaliseScore(float score) {
GetAveragesThisGame();
// Avoid division-by-zero error here. You guys got average <2 frags? Screw you I'm not scaling that up to NormalisedStrength!
if (averageGameScore < 2.0) {
averageGameScore = 2.0; // CONSIDER: maybe just better not to update
} // BT games will tend to have a lot of -ve scores.
// DebugLog("NormaliseScore("$score$"): Average game score was "$averageGameScore$", average player strength was "$averagePlayerStrengthThisGame$"");
/*
if (bRelativeNormalisation) {
return score * averagePlayerStrengthThisGame / averageGameScore;
} else {
return score * NormalisedStrength / averageGameScore;
}
*/
return score * FloatWeUseForAverageGameStrength() / averageGameScore;
}
function float FloatWeUseForAverageGameStrength() {
return averagePlayerStrengthThisGame * RelativeNormalisationProportion + NormalisedStrength * (1.0 - RelativeNormalisationProportion);
}
// Takes everything before the first ":" - used when getting the IP from PlayerPawn.GetPlayerNetworkAddress(); since the client's port number changes frequently.
function string stripPort(string ip_and_port) {
if ((""$ip_and_port)=="None" || ip_and_port=="") {
// DebugLog("stripPort() ip_and_port="$ip_and_port);
return "0.0.0.0";
}
return Left(ip_and_port,InStr(ip_and_port,":"));
}
// Include my library of common UnrealScript functions:
//===============//
// //
// JLib.uc.jpp //
// //
//===============//
function int SplitString(String str, String divider, out String parts[256]) {
// local String parts[256];
// local array parts;
local int i,nextSplit;
i=0;
while (true) {
nextSplit = InStr(str,divider);
if (nextSplit >= 0) {
// parts.insert(i,1);
parts[i] = Left(str,nextSplit);
str = Mid(str,nextSplit+Len(divider));
i++;
} else {
// parts.insert(i,1);
parts[i] = str;
i++;
break;
}
}
// return parts;
return i;
}
function string GetDate() {
local string Date, Time;
Date = Level.Year$"/"$PrePad(Level.Month,"0",2)$"/"$PrePad(Level.Day,"0",2);
Time = PrePad(Level.Hour,"0",2)$":"$PrePad(Level.Minute,"0",2)$":"$PrePad(Level.Second,"0",2);
return Date$"-"$Time;
}
// NOTE: may cause an infinite loop if p=""
function string PrePad(coerce string s, string p, int i) {
while (Len(s) < i)
s = p$s;
return s;
}
function bool StrStartsWith(string s, string x) {
return (InStr(s,x) == 0);
// return (Left(s,Len(x)) ~= x);
}
function bool StrEndsWith(string s, string x) {
return (Right(s,Len(x)) ~= x);
}
function bool StrContains(String s, String x) {
return (InStr(s,x) > -1);
}
function String StrAfter(String s, String x) {
return StrAfterFirst(s,x);
}
function String StrAfterFirst(String s, String x) {
return Mid(s,Instr(s,x)+Len(x));
}
function string StrAfterLast(string s, string x) {
local int i;
i = InStr(s,x);
if (i == -1) {
return s;
}
while (i != -1) {
s = Mid(s,i+Len(x));
i = InStr(s,x);
}
return s;
}
function string StrBefore(string s, string x) {
return StrBeforeFirst(s,x);
}
function string StrBeforeFirst(string s, string x) {
local int i;
i = InStr(s,x);
if (i == -1) {
return s;
} else {
return Left(s,i);
}
}
function string StrBeforeLast(string s, string x) {
local int i;
i = InStrLast(s,x);
if (i == -1) {
return s;
} else {
return Left(s,i);
}
}
function int InStrOff(string haystack, string needle, int offset) {
local int instrRest;
instrRest = InStr(Mid(haystack,offset),needle);
if (instrRest == -1) {
return instrRest;
} else {
return offset + instrRest;
}
}
function int InStrLast(string haystack, string needle) {
local int pos;
local int posRest;
pos = InStr(haystack,needle);
if (pos == -1) {
return -1;
} else {
posRest = InStrLast(Mid(haystack,pos+Len(needle)),needle);
if (posRest == -1) {
return pos;
} else {
return pos + Len(needle) + posRest;
}
}
}
// Converts a string to lower-case.
function String Locs(String in) {
local String out;
local int i;
local int c;
out = "";
for (i=0;i=65 && c<=90) {
c = c + 32;
}
out = out $ Chr(c);
}
return out;
}
// Will get all numbers from string.
// If breakAtFirst is set, will get first number, and place the remainder of the string in rest.
// Will accept all '.'s only leading '-'s
function String StrFilterNum(String in, optional bool breakAtFirst, optional out String rest) {
local String out;
local int i;
local int c;
local bool onNum;
out = "";
onNum = false;
for (i=0;i=Asc("0") && c<=Asc("9")) || c==Asc(".") || (c==Asc("-") && !onNum) ) {
out = out $ Chr(c);
onNum = true;
} else {
if (onNum && breakAtFirst) {
// onNum = false;
// out = out $ " ";
rest = Mid(in,i);
return out;
}
}
}
rest = "";
return out;
}
// UT2k4 had Repl(in,search,replace).
function String StrReplace(String in, String search, String replace) {
return StrReplaceAll(in,search,replace);
}
function String StrReplaceAll(String in, String search, String replace) {
local String out;
local int i;
out = "";
for (i=0;i=0) {
result = Left(str,i);
str = Mid(str,i+Len(delimiter));
} else {
result = str;
str = "";
}
return result;
}
// New ATB:
// function FindBestRebalance() {
// }
U @ L M E À} DÅ ¡y D Àš -D˜ -çpppp+AutoTeamBalance+ SD† , V.PostBeginPlay() called with initialized already true; quitting. -çpppp+AutoTeamBalance+ SD† , V.PostBeginPlay() initialising ~.† ‘ ª Aº-D°-çpppp+AutoTeamBalance+ SD† , V.PostBeginPlay() disabling self on request -')-çpppp+AutoTeamBalance+ SD† , V.PostBeginPlay() added self as mutator † ‘ O¸-çpppp+AutoTeamBalance+ SD† , V.PostBeginPlay() registered self as messenger ‚w.’† ‘* -w.’† ‘ êType !teams if they become uneven. a €?'-_(Ot-D' ÄN CAò [„ ‡ z‚w C* r C ¡ ¡½ r Cº -çpppppp[AutoTeamBalance] SD† , V.AddMutator( V C) No need to add mutator self again. wU-çpppppppp[AutoTeamBalance] SD† , V.AddMutator( V C) Destroying other instance with V C.Destroy() .A C -D' C a…Ä C ÂO QB „‰ A çppV.Destroyed() I was destroyed at U†  R A ÔP C Æ‹ Ç -c -f• ‚‚‚‚„-[ -Z - -_ † ‘ a/!\ .—† ‘ -¤eÅ -a?@‚-_ „-[ -Z àS [[' Ä G ) w¦*¦ [ [ \ D´ ‚-| † ‘ a/!Q F† ‘ X DTeam ,ÿ §,² › F Dpp?Team= S DÏ +† ‘G-çppppppppp+AutoTeamBalance+ SD† , ModifyLogin( V [, \," D") F† ‘ X DTeam ,ÿ B.ž† ‘ §% eD«% B © ¬"&