Áƒ*žE6@êÎËj°Ç¨¤§_Ü#ᯒ#+;‘ê6NonePrePad GetHumanNameClientMessageGetTeamStrength SpectatorBroadcastMessageAndLog PlayerPawnChangePlayerToTeamFlashMessageToPlayer GetTeamName GetTeamSizeFindPlayerNamedBroadcastTeamStrengthsColorGetFlagStrengthForTeamGetPlayerStrengthGetDate ShowLineTogetIP GetDBNameConsoleCommandBotAllowedToBalanceMidGameRebalanceAllowedToRankCountHumanPlayersFindOldestPlayerRecordMeasureScaleToFullTime StrContains StrAfterClearProgressMessagesFlashToAllPlayers GetTeamScoreGetFlagStrengthGetRecordedPlayerStrengthGetScoreForPlayerBroadcastMessageMutateShowStrengthsToSendPlayerToUrlBroadcastRebalanceMessageBroadcastMessageAndAlwaysLogShouldBalanceFindPlayerRecordGuaranteedNumFromDateStringDaysFromDateStringNormaliseScoreStrAfterFirst ClientTravelPreClientTravelCTFGameAssault ShowStatsToGetEffectiveSemiAdminPassClearAllProgressMessagesForceFullTeamsRebalanceGetTeamStrengthNoFlagStrengthFindPlayerRecordSwapPlayerRecords ClearRecordUpdateStatsAtEndOfGameStrBeforeFirstLocs ShakeView AddMutatorAutoTeamBalanceFixTeamsizeBug ListFakesTo LogSituationSwitchTwoPlayers DoGameStartConditionalString CheckMessageGetTeamStrengthString"MidGameTeamBalanceSwitchOnePlayerProposeChangeShouldUpdateStatsFindPlayerWithIDCopyConfigIntoArraysCopyArraysIntoConfiggetISPGetAveragesThisGame!FloatWeUseForAverageGameStrength SplitString InStrLast StrFilterNumEvent GetIntOptionKickKickBan ModifyLogin TeamGamePlusCheckNewPlayerCheckTwoNewPlayersGetSuggestedChanges ListMutsTo GetNicksForToggleAdminOnPlayerCheckGameStartFlashPreGameLinesCheckMidGameBalance CheckGameEndSendPlayerToTeamspeakStrFilterBadCharsRequestMidGameRebalanceSgn#MidGameTeamBalanceSwitchTwoPlayersFindPlayerRecordNoFastHashCreateNewPlayerRecordInitialiseRecordFindEmptyPlayerRecordFast"FindOldPlayerRecordFastDuringGame FindOldPlayerRecordMediumRandomFindOldestPlayerRecordSlow!FindOldestPlayerRecordInnerBatchCleanupDatabaseCleanupDatabaseABitMoveRecordIntoDB AgeInDaysGetWinningTeamIsOnWinningTeamGetRankingPointsUpdateStatsForPlayer stripPortStrStartsWith StrBeforeStrReplaceAllCoreEngine DestroyedTimerPostBeginPlayDied ParseOption ChangeTeam ModifyPlayerSetProgressTimeSetProgressColorSetProgressMessageMutatorTeamMessageHandleEndGameMutatorBroadcastMessageRegisterMessageMutator DominationLastManStandingbAutoBalanceTeamsForCTFbAutoBalanceTeamsForTDMbAutoBalanceTeamsForAS#bAutoBalanceTeamsForOtherTeamGamesbUpdatePlayerStatsForCTFbUpdatePlayerStatsForTDMbUpdatePlayerStatsForAS$bUpdatePlayerStatsForOtherTeamGames"bUpdatePlayerStatsForNonTeamGamesbRandomColorsInLMSTeamspeakChannelOtherCheckFrequencybLetPlayersRebalancebEnablePlayerCommandsbAutoSwitchNewPlayersAutoSwitchTimeoutMinSecondsBeforeRebalancebOverrideMinRequestsMinRequestsForRebalance"MinStrengthDifferenceForRebalancebFlashRebalanceRequestbShowProposedSwitchbWarnMidGameUnbalance bShowReasonbFlashOnWarningbReportStrengthAsCookies bLoggingbBroadcastTeamStrengths!bBroadcastTeamStrengthDifferencebAllowSemiAdminKickbAllowSemiAdminForceTravelSemiAdminPassMaxHoursWhenCopyingOldRecordHoursBeforeRecyclingStrengthMinHumansForStatsScoringMethodbNormaliseScores RelativeNormalisationProportion"StrengthProportionFromCurrentGamePreferenceToSwitchNewPlayersbScalePlayerScoreToFullTimeNormalisedStrengthUnknownStrength BotStrength FlagStrengthStrengthThresholdWinningTeamBonusScoreThresholdLowScoreThresholdHighclanTagbLetPlayersViewStrengthsbAllowUsersToListFakesbUseISPNotFullIPstrengthColor warnColorMaxPlayerData DBVersion colorWhite colorRed colorBlue colorGreen colorYellow colorCyan colorMagenta colorGray colorBlackConditionalName CreateNewPlayerRecordInnerBatch StrEndsWith StrAfterLastStrBeforeLast InStrOff StrReplaceStrPop ByteProperty IntProperty BoolPropertyFloatPropertyObjectProperty NamePropertyClassPropertyStructProperty StrProperty UnrealShareNameStruct FunctionFieldObject TextBufferOutPackageClassSoundOptionsCountListPlayerPawnActorLevelGameReplicationInfoPlayerReplicationInfo GameInfo LevelInfoMutatorTimeGetPropertyTextSetPropertyTextDynamicLoadObjectInStr ReturnValueGBS ScriptTextijECXATeamType bIsPlayer LocationGetPlayerNetworkAddressPingOtherIPDeltaResultURLMsgP SpawnClassN bIsHumanIdM bTeamGame PlayerNameScoreHasFlag PacketLoss StartTimePRIbAdminGame nextPawnStartSpreebBeep TimeSecondsYearMonthDayHourMinuteSecondNetMode PawnList bGameEnded bOverTimepos BaseMutator NextMutatorPortalStrSTfliesTeamNum ElapsedTime TeamName PlayerIDDeathsSender DirectionActionLine SecretCount KillCount ItemCountReplace ReceiverTeamsIn TeamInfo MaxTeams MaxTeamSizeSizeFoundOffsetflybuzz BestScorebNoTeamChangesGRI PlayerCount TeamIndexBeepSuccessGenericFlip Initializedk DelimiterData NickNameNickPartBotpackDeathMatchPlusNetWaitTeamChangeMessage CountDown bTournamentTournamentGameReplicationInfobPlayersBalanceTeamsdiffbBroadcastHelloGoodbyebSeparateStatsByGamemodebSeparateStatsByMutators WebsiteURL ForumURLTeamspeakChannelbForceEvenTeams bNeverRebalanceWhenTeamsAreEvenbOnlyFlashInvolvedPlayerspidsRequestingRebalancelastRebalanceRequestTimebCheckStrengthBalancebShakeOnWarningbBuzzOnWarningbShakeWhenMovedbFlashPlayerJoinsbFlashTeamStrengthsbBroadcastCookiesbFlashCookies FlashLine bHelpInPugsbLogDeletedRecords bBalanceBots bRankBotsbDebugLoggingbLogExtraStats bClanWarbLogFakenickersbBroadcastFakenickersbShuffleTeamsEarly LastUpdate playerDataCopyConfigDonerkey avg_score hours_playeddate_last_played bSuggestingSuggestedChangesbCachedcurrentDateDaysgameStartDone gameEndDonetimeGameStartedlastBalanceTimeaverageGameScoreaveragePlayerStrengthThisGameLastCalculatedAveragesLastPlayerToJoinCleanupProgressnewATB selectedTeam teamSizeteamSizeWithBotsteamStrteamnrplnamepawstrengthSwing strengthA strengthBredTeamStrengthblueTeamStrengtholdDifferencenewDifference timeInGameA timeInGameBshowIPsipstr nickListbExtraplayerGameStrengthdeltaStrengthdeltaStrengthStr redBonus blueBonusnompippnickonickargs argcount localPasspass_if_neededpp bTempBoolmutStr mutClassmutnmutname1name2player1player2 newteam1 newteam2 targetLine strength msgColor redTeamCountblueTeamCount weakerTeamproblemyesnooutStrpidplpsmovedplordermx actualteamnr weakestStroldMaxTeamSizeoldbPlayersBalanceTeamsoldbNoTeamChanges leadTeamcol difference balanceStr winningTeamcountRequestsadditionalRequiredRequestsbDo fromTeamtoTeamfromTeamStrengthtoTeamStrengthcurrentDifferenceplayerStrengthteamScoreStrengthDifferenceclosestPlayer timeInGamepotentialNewDifference thisScoreplayerCountDifferenceredPblueP redPStrengthbluePStrengthredPTimeInGamebluePTimeInGameredPlayerToMovebluePlayerToMovebestDifferencebothTimeInGameonetwobInformlinenumtmpstr tmp_rkeytmp_player_nicktmp_player_iptmp_avg_scoretmp_hours_playedtmp_date_last_played player_nick player_ip bNickMatches bIPMatches bestDate returned posStartbestIagebestAge bestHoursnow newScoreiStart lowestScore dateStringdatestrdays thisTeamGame playersAbove playersBelow award_scorecurrent_scoreold_hours_playednew_hours_playedhours_played_this_gamepreviousPolls gameDuration weightScoreprevious_average ip_and_portdividerparts nextSplitDate haystackneedle instrRestposRest breakAtFirstrestonNumsearch+@€‡@Q„ I@ €@'@ {@~€„ @mK„P J@Y{ _!€„€v„]O@jz €„~ €O€v„e€p@J@S„ @_^@h@&r„J €„x €„AM„5„~„T€‡s’JG„H€„L„f@Lp •Ii Bÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿm A– “•=$:eƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”„žtÈ„žtÈ„žtÈƒŽ”ƒŽ”ƒŽ”ƒŽ”„žtȉ{#U¿ƒŽ”ƒŽ”—-¤%nƒŽ”„žtÈƒŽ”ƒŽ”„žtÈ„žtÈ„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtȉ{#U¿ƒŽ”„žtÈƒŽ”„žtÈ„žtÈ„žtȉ{#U¿„žtȉ{#U¿„žtÈƒŽ”ƒŽ”„žtȉ{#U¿„žtȉ{#U¿™ôªª‘ƒŽ””©Ù«¢'n 6ƒŽ”’}"›#’}"›#™ôªª‘™ôªª‘’}"›#’}"›#™ôªª‘™ôªª‘’}"›#™ôªª‘ƒŽ”ƒŽ”ƒŽ”ƒŽ”„žtÈ„žtÈƒŽ”„žtȉ{#U¿„žtÈ„žtȉ{#U¿„žtȉ{#U¿„žtȉ{#U¿„žtÈ„žtȉ{#U¿„žtÈ„žtÈ„žtÈƒŽ”„žtȉ{#U¿„žtÈƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ””©Ù«ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”„žtȉ{#U¿ƒŽ”ƒŽ”ƒŽ”ƒŽ”„žtÈ„žtÈ„žtÈƒŽ””©Ù«ƒŽ””©Ù«•=$:e•=$:e„žtȇ> £YƒŽ”ƒŽ”ƒŽ”ƒŽ”ƒŽ”„žtÈ„žtÈ„žtȇ> £Y‰{#U¿„žtÈƒŽ”„žtÈ„žtÈ„žtÈ„žtÈ„žtÈ„žtÈƒŽ”„žtÈƒŽ”„žtÈƒŽ”„žtȉ{#U¿ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈƒŽ””©Ù«ž;¬º(™ôªª‘ƒŽ”ƒŽ”’}"›#™ôªª‘ƒŽ”„žtÈ„žtÈ„žtÈ„žtÈ„žtÈ„žtÈ„žtÈƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtÈ„žtÈ„žtÈ„žtÈ„žtÈƒŽ””©Ù«ƒŽ””©Ù«ƒŽ””©Ù«¥”©Ù«ƒŽ””©Ù«¢'n 6ƒŽ”—-¤%nƒŽ””©Ù«¢'n 6ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ”º—¬ž ƒŽ”ƒŽ”„žtÈ„žtÈ„žtÈ„žtÈƒŽ”„žtȇ> £Y‡> £Y‡> £Y„žtÈƒŽ”„žtÈƒŽ”„žtÈ„žtȉ{#U¿ƒŽ”„žtÈƒŽ”ƒŽ”’}"›#ƒŽ”„žtÈƒŽ”’}"›#„žtÈ„žtÈÏýúL]ƒŽ”ƒŽ”’}"›#ƒŽ”’}"›#ƒŽ”„žtÈ„žtȉ{#U¿ƒŽ””©Ù«ƒŽ”ƒŽ”’}"›#„žtÈ„žtÈƒŽ”—-¤%n„žtȉ{#U¿„žtȇ> £Y„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtȉ{#U¿„žtÈ„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtÈƒŽ”„žtÈ„žtȉ{#U¿„žtȉ{#U¿ƒŽ”„žtȉ{#U¿ƒŽ”„žtÈ„žtȉ{#U¿„žtȉ{#U¿ƒŽ”„žtȉ{#U¿„žtÈ„žtÈƒŽ”ƒŽ”„žtÈ„žtȉ{#U¿„žtȉ{#U¿ƒŽ”„žtȉ{#U¿„žtÈƒŽ”ƒŽ””©Ù«ƒŽ””©Ù«ƒŽ”ƒŽ”ƒŽ”‡> £Y‰{#U¿ƒŽ”ƒŽ”ƒŽ”ƒŽ”—-¤%nƒŽ”„žtÈ„žtÈ„žtÈ„žtȇ> £Y„žtÈƒŽ”ƒŽ”ƒŽ””©Ù«¢'n 6ƒŽ”ƒŽ”„žtÈ„žtȉ{#U¿ƒŽ”„žtÈ„žtȉ{#U¿„žtȉ{#U¿„žtÈƒŽ”„žtÈƒŽ”’}"›#’}"›#’}"›#’}"›#’}"›#’}"›#„žtȉ{#U¿ƒŽ”’}"›#ƒŽ”’}"›#ƒŽ”„žtÈƒŽ”’}"›#ƒŽ”’}"›#ƒŽ”’}"›#’}"›#’}"›#‡> £Y‰{#U¿ƒŽ””©Ù«¢'n 6ƒŽ”—-¤%n‡> £YƒŽ”‡> £Y‡> £Y‡> £YƒŽ”‡> £Y‡> £Y‡> £Y„žtÈ„žtÈƒŽ”ƒŽ”’}"›#„žtÈ„žtȇ> £Y‡> £Y„žtȇ> £Y‡> £YƒŽ””©Ù«•=$:eƒŽ””©Ù«•=$:eƒŽ””©Ù«•=$:e‡> £Y„žtÈ„žtÈƒŽ”—-¤%nƒŽ”„žtȇ> £YƒŽ””©Ù«ƒŽ”ƒŽ”„žtÈ„žtÈ„žtȇ> £Y„žtÈƒŽ”ƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿‡> £Y„žtȇ> £Y„žtÈ„žtÈ„žtÈ„žtÈƒŽ”ƒŽ”ƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtȉ{#U¿„žtÈƒŽ”„žtÈ„žtÈ„žtÈ„žtÈƒŽ””©Ù«¥”©Ù«ƒŽ””©Ù«¥”©Ù«ƒŽ”ƒŽ”ƒŽ”—-¤%n„žtÈ„žtÈ„žtÈƒŽ”ƒŽ”ƒŽ”—-¤%nƒŽ”—-¤%nƒŽ”—-¤%nƒŽ”—-¤%nƒŽ”ƒŽ””©Ù«ƒŽ””©Ù«„žtȇ> £Y‡> £Y‡> £Y‰{#U¿‡> £Y‡> £Y‡> £Y„žtȉ{#U¿„žtȉ{#U¿‡> £Y„žtÈ„žtÈ„žtȉ{#U¿„žtȉ{#U¿ƒŽ”‡> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £YƒŽ””©Ù«‡> £YƒŽ””©Ù«¢'n 6‡> £Y‡> £Y‡> £Y„žtȇ> £Y„žtÈ„žtÈ„žtȉ{#U¿‡> £Y‡> £Y„žtȇ> £Y‡> £Y‡> £Y‰{#U¿‡> £Y‡> £Y‰{#U¿‡> £Y‡> £Y‡> £Y„žtÈ„žtȇ> £Y‡> £Y‡> £Y„žtȇ> £Y‡> £Y‡> £Y„žtÈ„žtȇ> £Y‡> £Y‡> £Y„žtȇ> £Y‡> £Y‡> £Y„žtÈ„žtȇ> £Y‡> £Y‡> £Y„žtȇ> £Y‡> £Y‡> £Y„žtÈ„žtȇ> £Y‡> £Y‡> £Y„žtȇ> £Y‡> £YƒŽ”„žtÈ„žtȇ> £Y‡> £Y„žtÈA– “‡> £Y¥•=$:eƒŽ””©Ù«•=$:e‡> £YƒŽ””©Ù«•=$:e•=$:e•=$:e¥•=$:e•=$:e•=$:e•=$:eƒŽ”•=$:e‡> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £YA– “A– “‡> £YA– “‡> £Y‡> £YA– “‡> £Y‡> £Y„žtÈ„žtȇ> £Y‡> £Y„žtȇ> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £Y‡> £YƒŽ””©Ù«•=$:e•=$:e•=$:eƒŽ”‡> £YƒŽ”‡> £YƒŽ”„žtÈ„žtȉ{#U¿„žtȉ{#U¿„žtÈ„žtȇ> £YƒŽ”‡> £Y‡> £Y‡> £Y‡> £Y‡> £YƒŽ”„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtȉ{#U¿„žtȉ{#U¿„žtÈƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtȉ{#U¿„žtÈ„žtÈ„žtÈƒŽ”„žtȉ{#U¿„žtÈƒŽ”…œÔX…œÔXƒŽ”…œÔX‡> £Y‰{#U¿‡> £Y‰{#U¿‡> £Y‰{#U¿‡> £Y‰{#U¿ƒŽ”‡> £Y‰{#U¿ƒŽ”‡> £Y‰{#U¿ƒŽ”‡> £Y‡> £Y‡> £Y‰{#U¿‡> £Y‰{#U¿‡> £Y‡> £Y‰{#U¿ƒŽ”—-¤%nƒŽ”„žtȇ> £Y‰{#U¿ƒŽ”ƒŽ”‡> £Y‡> £Y‰{#U¿‡> £YƒŽ””©Ù«ƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈž;¬º(™ôªª‘ž;¬º(™ôªª‘ƒŽ”ž;¬º(™ôªª‘ž;¬º(™ôªª‘ž;¬º(™ôªª‘ž;¬º(™ôªª‘•=$:eƒŽ””©Ù«ƒŽ””©Ù«ƒŽ”ƒŽ”ƒŽ””©Ù«ƒŽ””©Ù«ž;¬º(™ôªª‘ž;¬º(™ôªª‘ƒŽ”„žtÈ„žtÈ„žtÈ„žtȉ{#U¿„žtÈ„žtÈƒŽ””©Ù«ƒŽ”ƒŽ”ƒŽ””©Ù«ƒŽ”—-¤%nƒŽ”•=$:eA– “ƒŽ”ƒŽ”A– “•=$:eƒŽ”ƒŽ”ƒŽ””©Ù«•=$:eƒŽ”ƒŽ”ƒŽ””©Ù«ƒŽ”ƒŽ”ƒŽ”’}"›#BA@–BRÓSÓTÓUÓVÓWÓXÓYÓZÓ[Ó\].-teamspeak://ekiebox.org:8767?channel=shitbra]"^Ó_Ó`Óa" b"xcÓd"e"fÓgÓhÓiÓjÓkÓlÓmÓnÓoÓpÓq]defaults_to_admin_passr$@@s$@@t"u"vÓw$?x$š™™>y$?zÓ{"2|"-}"~""x@"A"öÿÿÿB"ÐC]XOLDÓEÓFÓG*ÿ˜0 H*ÿ I"J$33³?K*ÿÿÿ L*ÿ M* ÿ N* ÿ O*ÿÿ P* ÿÿ Q*ÿ ÿ R*ÀÀÀ S* z@€•F€@B„~•A„IBX„d„"r„w„C@ cW‡Z€‡L‡b@N@il@„8Yze„„`@g@@RVgžC„W•%„U@VD}„„en™} B@M@Q@ @)g€@•@„l„ C,v@Flo„T €„B E€„u €„@•ktu’s| ju€@pq€@r€@@@G@y@{@~@`W GV€ @M|‡T€‡U@WE€n€O €[q„^„w €„W€Pj €@f€@z|@Ws@\@K@A@N@O@$@@d‡1H^H€…Y€@[€‡a€‡eO@@@X‡nT€‡#3 W@]\op^_a@cMYMaD €@v€@` €@o@Vq€@P@|@@v@F@x@BB@@|@Q@R@DRY ežhS€„€‡Gbc€„J€…X€„@@NV@l¥•j€‡g€„€@V@Z9m€\YZ_`@a €„qb@hd\€lo™v €„hSvV €@A€@B€@E€@@~@@t@@Z@@A@w@@D@E@u@w@H@C@~@G@L@@L@N@x@@@B@}@S@P@@S@T@aX @ Z K \€¥‡D€@E@_U€‡2€c@@e€‡€@i€‡*@P@P@k€@€@mor@B €@tuXL €y\{:Q €4@A]€@€@f€@g€@pc €„k €y €@„A €@D €„P@Q@R@ST@M €„O €„€„T €Z[ ia €th €abcdegg€@F €@i@@N €@l€@T €@^ €m€@b €@d @h €@j €@t@u@v@w@x@y@z@{@|@}@~@}@@@A@C@[@D@E@F@I@H@@J@@K@M@O@P@]@Q@R@T@U@y@@zW]J€‰Z€_dh€‡^€_€@y€@a€b€{€[€‡€@I€a€ G€ e €i €m €o €q €{ €F €„H €„]€f €@l €@r €„t €„@ €w€@J €y€@L €@j€@|€@P €@~€@R €@@€@X €@Z €@\ €@D€@m€@pG€@H€@f €@Kö /* WARNING! This file was auto-generated by jpp. You probably want to be editing ./AutoTeamBalance.uc.jpp instead. */ // vim: tabstop=2 shiftwidth=2 noexpandtab filetype=uc // == AutoTeamBalance ======================================================== // A UT mutator that makes fair teams at the beginning of each teamgame. // Works by recording the relative strengths of players on the server (indexed // by nick/ip/Idc). // It also attempts to put a player joining the game on the weaker team, and // can perform mid-game rebalancing when a player types "!teams". // by F0X|nogginBasher and Daniel Mastersourcerer at Kitana's Castle. // Copyright Paul Clark 2007-8, released under the LGPL. // Thanks to: Daniel, iDeFiX, unrealadmin, Matt, the author of adwvaad, and // #unrealscript at EnterTheGame. // Code snippets lifted from iDeFiX's team balancer, TeamBallancer, and the // adwvaad thread. // =========================================================================== // == Lesser GNU Public Licence ============================================== // This program is free software: you can redistribute it and/or modify it // under the terms of the Lesser GNU General Public License as published by the // Free Software Foundation, either version 3 of the License, or (at your // option) any later version. // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License // for more details. // You should have received a copy of the GNU Lesser General Public License // along with this program. If not, see . // =========================================================================== /* // Note: Server admins who use Resurrector should be aware that restoring the // player's Score without restoring his TimeInGame will break ATB's scoring // calculations. // TODO: Do not update stats for players who are "sitting". e.g. If they have // not moved and have not fired their weapon in the last 10 seconds, then // decrease their time-in-game (increase their join time) by 10 seconds. // Fine for CTF. In Siege and LMS it might punish the player a bit; so be it! // Another alternative strategy for mid-game switch: // When teams are bad, or a player requests, alert to all players: // Teams look uneven (reason) // Type T to switch with %ideal_opposite_player // Swallow all Ts so they are invisible. // When two matching players type T then swap them. Or better, after a given // amount of time choose the best 2 of those who volunteered. // If teams look even, reduce probability that 1 player will cause spam of all others. // Maybe only spam his opposite(s) when he is trying to fix the teams. // Prevent a single player from breaking through the probability before the // Link !red !blue etc. etc. into T so that it all works smoothly? // BUG: ATB and Resurector are incompatible. Since Resurector v1 does not // recover each player's timeInGame, ATB will calculate skewed player stats. // Until we fix Resurector to recover timeInGame also, I recommend not running // it alongside ATB. // TODO: When it looks like teams are bad, send a message to relevant players: // "Press F1 to switch team" // Then detect calls from those players: "mutate smartctf stats" // This saves us having to go client-side to set a keybind, and should work for // most players, maybe not all. // TODO: F1Says redScore=total_of_red_scores+TeamScore, blueScore=... ; compare // TODO: mutate debug // TODO: AutoTeamBalanceTest.jpp // TODO CONSIDER: Would it be better to catch attempted creation of new Mutator in CheckReplacement or AlwaysKeep? // TODO: Have better support for detecting when a player is having a good or bad day ;) // This may require doubling up his records, one for short term and one for long term strength. // Although after 1 map we can't deduce a lot, after 2 map of pwnage, we really should try to rearrange the teams a bit better with respect to this day's irregularities. // CONSIDER: CurrentPlayerRecordTop could be the variable version of MaxPlayerData :P Although <1.5 ATBs used MaxPlayerData. // You are advised to set MaxPlayerData to the max 4096 now, for better // operation in the future. // TODO: In fact we will make this a default. // If you want to change MaxPlayerData, do it before migration! // Once you have a 1.5 database you should not change MaxPlayerData! // Actually you can change it if you want to, but you must set DBVersion back // to 1.4, so that migration will re-run, and records will be placed in their // correct bands. // TODO: Requested by sony_scarface. Allow players back onto their original team, if they have left and rejoined. // In the case that doing this would create e.g. 6v4, then move a player to the other team to fix it. The player we move should be the one with least time in the game. Maybe the rejoining player actually has less time than that guy, or has been away a long time, in which case soddit the rejoining player won't get his team back. :P // Also we need to watch out for if the player swaps team legitimately. E.g. someone leaves making 4v6, and Qwartz goes Red, then he becomes a red player (if he stays long enough - if not he might like to go back to blue :o ). // DONE: He has trouble typing in the player's name for "mutate kick". So instead, offer him a list of player IDs, and let him select the ID to remove. // TODO: Shiva requests that we detect leech games, and collect no stats for them. // For detection, he recommends "look for >40 conts", or Max RU > 4000, at any point during game! // REQUEST: log all actions taken by semi-admins. // kick for 1 map // embedded IDC-like system // DONE: If engine has bNoTeamChanges set, then players can't switch in Player Options. In that case, we should also disable !red and !blue! // Actually it's not true - There is a bug in UT's implementation of bNoTeamChanges which means it doesn't work! // TODO: ATM ATB gives a raised priority to player who joined the game only recently. If we skew this via some variable, and set that to 1.0, we would be able to enforce "bAlways/OnlySwitchRecentJoiners". // The theory here is especially for Siege, but also for CTF, on a public server if people often leave, and leave the losing team, that team needs a helpful switch fast, or it will fall behind. That means 50% of the time a player drops, we need to rebalance. And also since Siege involves investment, it makes sense to switch the new guy regardless of his strength, it's better than not moving anyone, or moving someone who has been playing for longer. // In other words we are doing what TeamBallancer does, except doing it in 5v6 situations when blue>>red by strength and players. // maybe the variable to fiddle is the power - atm don't we square potentialDifference and keep timeInGameFactor linear? // TODO: detect idlers, after 30 seconds give them 10 seconds warning countdown, then send them to !spec :) // some checkidlers fail because they player keeps getting killed when he // spawns on his spawnpoint. avoid this. check his velocity (no!) check // his inputs? :s // Considering new database policy using hashtable: // Each player has 3 records - one for nick, one for ip, and one for nick+ip. (They can all live in same hashtable.) // All three get updated. His strength is calculated from all 3. // If his nick or IP changes, one or more of the old records can be used to make his new records. // Records automatically cleanup according to age. // When parsing, we can check duplicates and remove the older. // Do we need the third combined record? It does mean we only have to lookup one. // TODO: AutoTeamBalance chooses which 1 or 2 players to switch based on a combination of balance improvement, and time on server. // but server admins may wish to make it that low-scoring players are moved more often that high. That won't help team balance much, but might help enough to satisfy those players who are actually affecting the game! // TODO: When ATB detects someone should sit, offer them a black teleporter and a message. // TODO: When ATB detects someone should switch team, offer them a red teleporter and a message. // TODO: Immediately when a new player joins the server, find the ideal player to swap him with. If one exists that significantly improves teams, offer the playing player to type !ok and switch with the joining/spawning player. We could even mute !ok. // We could also add to one of their keybinds, and see if they press it. :o It should be a weird key, they don't wanna hit it by accident. Should it timeout? // To really smooth things, hide/overwrite the joining player's "you are on the ___ team" message until it's finally decided. // TODO: Super stats: top two players, top two players each team, two players with closest strength (ladder opportunity) // printed list of current player order clearly showing your position, for this gametype, and for all others // DONE: bHelpInPugs=False // If set, overrides bTournament, to provide !teams suggestions. // TODO: If we can't do better lookups, at least organise the db to have lost records later in the list. We can do this slowly over time, by swapping 3 records instead of swapping 2. Move the new guy 70% towards the top, and he will be sort-of cached for the next few hours. // TODO: bAutoSwitchNewPlayers doesn't take action *until* B spawns, which is good if B doesn't stay long enough to spawn, but is it messy? // We could actually inform player A from the moment B joins that they will be swapped, but wait until B spawns. // 2008-09-26 00:06 Released AutoTeamBalance.undergound149 // TODO: teamspeak needs to remove/fix weird chars in nickname // DONE: By using bCached[], the record is not re-checked if the nick changes, or if IP changes. On XOL this is a problem because the initial IP often does change to IDC after a little bit of time. OK hacking this into the IDC catch code. // This solution means the empty record of their IP will be created. An alternative would be, at the end of the game, to overwrite whatever is in the DB with the players current nick and idc. // TODO: !listfakes could become !whois // TODO: Should there be a !grey/!gray, for DM? // TODO: I think mid-game rebalance should not use FlagStrength, but mid game player joins should. // TODO: we may have a record cached, but we still have to check it each time. It would be nice to have a faster check for whether we have it cached or not. // TODO: I wanted to do a QuickSort and QuickSearch, but since we must search by IP or by Nick, I might try BuildHash and SearchHash. But really better would be a sorted index, or even a binary tree. Whatever we do, we need two, one for IP and one for nicks. // TODO: BroadcastTeamStrengths() was occasionally being called twice. Check that it is not called if it cannot do anything (or at least not called twice!). Seems OK atm (I did remove some). // TODO: Prevent players from switching team when teams are already even. (Admin optional imo.) // TODO: My recent attempt to perform 2-player-rebalance if 1-player-rebalance fails, does not work in the situation where it's 3v5 and team strengths are even. In that situation, it cannot find anything better, because it is looking at balancing strength, not players. // TODO: mutate suggest - Like bProposeSwitch but only messages Sender. // TODO: ATB may decide to switch 3 players, but it only flashes that it will switch two, and this looks wrong to players! // TESTING: If ATB is used in pug mode, it could also switch players teamspeak channel. We could invoke "MUTATE REDTS" if the TeamSpeak mutator is available, or we could try to execute a URL on the client directly. // TODO: delmut may not be working 100% ideally; I think I broke mapvote when I did "mutate addmut WeirdMuts.ForceBehindView" and then delmut on it. // Another possibility for the future: If teams become uneven, find the two most suitable players to switch, and let the BETTER of the two players decide whether they switch teams or not. Is this a nice way to do it? // TODO: Add "mutate undo" and "!undo" which can restore last few saved team states. That way you can try some things to improve the teams, but go back if needed. // Changes since 1.4: // Re-arranged database to 1.5 hashtable style. (Not actually enabled yet - see HASH15!) // When exact matching is not possible, prefers to match more recent records. This requires extra processing when multiple partial matches are found, so watch out for lag or "too many iterations" errors. // Changed the default number of player records to 2048, to reduce lag on general servers. // Reduced MinSecondsBeforeRebalance to 3 because it's annoying and only really needed to fix the bug with duplicated calls to MutatorTeamMessage(). // Added bBroadcastTeamStrengths and bFlashTeamStrengths. // Moved "Type !teams" message from Flash to the GameInfo.TeamChangeMessage. // DONE: Added ADDMUT and DELMUT. // DONE: Swap 2 or 3 players when swapping 1 or 2 won't work, e.g. 4 strong vs 5 weak. // DONE: added bCached - a faster check on whether we have already cached (looked up and swapped) a player's record // DONE: If ATB detects uneven teams, that should count as MinRequestsForRebalance-1 votes, so that only 1 request is needed to fix the teams. OK we check 3v5 and we check strength difference. // DONE: Compile time option LOG_LAG, to help debugging/development. // DONE: When deciding which player to switch mid-game, ATB now prefers players who have not been in the game for long. // DONE: Holding the flag will only prevent you from being switched if there is no better switch that can be made. // DONE: bAutoSwitchNewPlayers: Swaps latest player to join with last player to join, if it improves teams. We also now prefer to swap players shortest on the server, when performing mid-game rebalance. // DONE: mutate listfakers aka mutate listfakes aka mutate listnick // DONE: WinningTeamBonus is now given in secret. Reduced default to 5 points. // DONE: Foolish SemiAdmins can no longer switch Spectators onto a playing team. [Thanks AdminThis for the bugreport.] // DONE: I set the default StrengthProportionFromCurrentGame to 1.0, which in v1.5 will minimise mid-game lag (there will be no costly database lookups). The disadvantage is that the performance of players in previous games will not be taken into account. Admins can turn it on and decide if their server can handle the processing or not. // DONE: Server passwords can no longer be read using "mutate get" after "mutate grantadmin". // Added "!ts" commands to connect a player to their team's Teamspeak channel, if the admin wishes to configure it. // Changes in 1.4: // Fixed the bug that a spectator could type "!red" and pick up the blue flag! // Finally added a date_last_played entry for each player. Now old records can be recycled better. // As well as displaying the SmartCTF scoreboard, !stats now also shows all the player strengths. (It's a shortcut for "mutate strengths".) // Messages flashed to players now have different colours, position and timeout, and do not appear in the player's console. // Overwrites the default pre-game message which tells players which team they are on, since teams are not assigned until later! // If bShowReason is set, will explain why teams need to be rebalanced. // Now always broadcasts the reason if rebalancing was not possible, even if bBroadcastHelloGoodbye=False. // Added a new command: mutate listmuts // Also added "mutate strengths extra" - use with care, it may require too much processing with large numbers of players! // Also added "mutate stats" which shows various game stats for each player in the console (e.g. frags, deaths, item pickups). // Added RelativeNormalisationProportion with a default of 0.5, to stop player strengths from shooting off too high. // If a semi-admin accidentally types the semi-admin pass in chat instead of the console, their message is swallowed. // If team sizes differ by 2 or more players, someone will be switched, even if the smaller team had greater strength, but immediate rebalancing will be made possible, and recommended to all players. // Jan 08 // I want the server to have an average player strength of UnknownStrength (50). // This allows us to estimate the strength of unknown players, and keeps the numbers under control, so that strength (units ^^) means a similar thing on the server from day to day, rather than changing depending who is playing. // Problem: the relative normalisation was making numbers shoot off high; the average strength of all the records in my DB came to 58! This may be caused by relatively stronger players spending more time on the server than weaker players. // Solution: When the normalisation is performed, the target is not 50, or the current player average, but halfway between (configurable by RelativeNormalisationProportion). // I'm not sure that this will bring my total average back down to 50 anytime soon (although I could force that in one parse), but I think it will at least keep the average from increasing more. // Changes in 1.3 October 2007: // Added MinRequestsForRebalance. // Fixed the bug "X has lost N cookies" appearing when it shouldn't. // Scaled FlagStrength for non-CTF gametypes, so it is not disproportional. // Also reduced the default to 10, to give less of a disadvantage to the leading team. // Forced FlagStrength to 0 when #players <3, hoping to fix pizzaman's bug. // Made MaxPlayerData configurable - but do not set it above the static limit 4096. // Added bSeparateStatsByMutators, so now you can split up stats by gametype, or by mutators, or both, or neither. (NOTE: sony_scarface, or anyone who had bSeparateStatsByGamemode=True in earlier versions, should set this to True to keep your database as it was.) // Now we only broadcast "fakenickers" when nick changes, not IP. // Gives the player who was switched +1 frags and -1 deaths to make up for the suicide when he changes teams. // Now mid-game warning or multiple requests for !teams will display the proposed player(s) to move in advance. // Changed the default MaxHoursWhenCopyingOldRecord and HoursBeforeRecyclingStrength, so that cookies/strength are more sensitive, i.e. change more each game. // Some minor improvements to messages. // Stripped out a lot of comments. // DONE: we cache the averageGameScore and averagePlayerStrengthThisGame for 3 seconds, to avoid recalculating it unneccessarily. // DONE: longer lasting, better coloured flashing messages // TODO: make the bonus for winning team hidden from scoreboard, and then it might get used more often :) // TODO: when switching players, avoid players whos strength is less known (they have played less time on server) // DONE: when joining unstarted game, u get msg saying u r on Red or Blue team, but we change it later :S - overwrite or empty that message! // DONE: what should we do when it's 3v5 but red team has strong players, so team strengths are similar? // CONSIDER: integrate with AKA - no apparently AKA is no better than ATB's current system; make an IDC-like add-on instead // CONSIDER: when a player joins with a new IP, and we copy their old stats over to // a new record, we should really delete their old stats record, so that any // future copies will use the new (latest) strength. // Hmm but maybe he isn't really the same player. Better to leave the clearing up to the date-based algorithm. // TODO CHECK: maybe an admin wants disable players from switching team // entirely; if he does that, can ATB still do the switching it needs to?! // try enabling bNoTeamChanges in UnrealTournament.ini // CONSIDER: maybe we *can* update stats mid-game; if we rename timeGameStarted to lastTimeUpdated, and change that when we do an update. // Since we scale player scores to "full-time", this should also work for scaling down. // However, the current algorithm will still not count the score/frags earned since the last update, but since the player joined. We would need to do what iDeFiX's mod does, and store their each player's score/frags from the last update. // What has IDC support done to bLogFakenickers and bBroadcastFakenickers? 1) It will log if they change/remove their IDC or IP. 2) It might spam an unrecorded IP when a player joins, although we might get an IDC later, and have a record for that. The IP will eventually be reclaimed, since players end the game with an IDC, so they will be logged as a fakenicker again. ;) // CONSIDER: could add last_date_played, so that we can recycle old records suitably. // CONSIDER: make 2-player balancing optional // CONSIDER: increase mintimewhenipchanges, *especially* if first two digits remain the same // CONSIDER: when ip does change (often), delete the old record - it's no use to us // TEST: When I was testing both ServerActor *and* mutator (not actually desirable), it seemed "!teams" was not working - is this fixed now? // BUG: Do not use NetWait<3; it may cause the teambalance to occur before anyone joins the server! */ //// Preprocessing definitions: // #define XOL_SPECIFIC //// Testing - Things we are finalising for next release: // #define TESTING // #define DEBUGGING // #define SWING_PROPS //// Features which worked and we are keeping: // Semi-admin stuff: //// Note: CLEANUP14 achieves KEEP_EARLY_RECORDS_EMPTY, which reduces mid-game lag. // This is quite important to ensure old duplicates get forgotten. Although it has introduced some extra calculation. //// Disabled - For good reason. // HASH15 is not yet complete. It's fail because we are hashing by Name+IP when really we need 1 hashtable for each. //// #define HASH15 // APPLY_LASTBADPLAYER_TO_REBALANCE is not really doing it right. //// #define APPLY_LASTBADPLAYER_TO_REBALANCE // I believe cleanup before the game is better. On slow machines, CLEANUP_AFTER might possibly cause the red-icon. //// #define CLEANUP_AFTER //// Unstable - Things which we cannot release, and have been abandoned for the moment: // #define SUPERBALANCE - i think it breaks the engine's idea of who is on which team // #define COOL_CAMERA - not a project for ATB, needs to be clientside anyway // #define PRECLEAR_SOME_RECORDS - current implementation is dangerous - also doesn't work on the presence of CLEANUP14 //// RANDOMBOTGREET requires DETECT_PLAYERJOINS //// I don't know if it works or not because join detection happens before I really enter :P // #define RANDOMBOTGREET //// I never got these data-structures working. =/ // #define FAST_TREE // #define FAST_HASH //// FAST_DATE_COMPARISON may be dangerous, since records just created might be seen as worth deleting! Check this before defining FAST_DATE_COMPARISON. // #define FAST_DATE_COMPARISON //// Debugging - Things we only want during development, and will never release: // TODO: If we do take the duplicate, the older useless one should be removed. // #define LOG_TICKRATE // #define WARN_TICKRATE_CHANGE // Changelog: // 0209 inlined SHORTHELP // Just for me really: // #define ATB15 // (if debugging ^^ ) class AutoTeamBalance expands Mutator config(AutoTeamBalance); //// Various hashing attempts. // MY_HASH_FN is used by more than one! //// Config variables (documented in AutoTeamBalance.txt): var config bool bBroadcastHelloGoodbye; var config bool bAutoBalanceTeamsForCTF; var config bool bAutoBalanceTeamsForTDM; var config bool bAutoBalanceTeamsForAS; var config bool bAutoBalanceTeamsForOtherTeamGames; var config bool bUpdatePlayerStatsForCTF; var config bool bUpdatePlayerStatsForTDM; var config bool bUpdatePlayerStatsForAS; var config bool bUpdatePlayerStatsForOtherTeamGames; var config bool bUpdatePlayerStatsForNonTeamGames; var config bool bRandomColorsInLMS; var config bool bSeparateStatsByGamemode; var config bool bSeparateStatsByMutators; var config String WebsiteURL; var config String ForumURL; var config String TeamspeakChannel[4]; var config String TeamspeakChannelOther; var config bool bForceEvenTeams; var config int CheckFrequency; var config bool bNeverRebalanceWhenTeamsAreEven; var config bool bLetPlayersRebalance; var config bool bEnablePlayerCommands; var config bool bAutoSwitchNewPlayers; var config int AutoSwitchTimeout; // CONSIDER: Possible refactoring for 2.0; is bOverrideMinRequests == // bNeverRebalanceWhenTeamsAreEven or better related to it than it is now? // Some combinations of Flashing options and bShowProposedSwitch might be // incompatible and also ripe for refactoring. #define some // bReallyOverrideMinRequestNow maybe? :p // If players can rebalance, how does it happen? var config int MinSecondsBeforeRebalance; var config bool bOverrideMinRequests; // When teams are terrible by count, or by strength. Also overrides MinSecondsBeforeRebalance. var config int MinRequestsForRebalance; var config int MinStrengthDifferenceForRebalance; var config bool bFlashRebalanceRequest; var config bool bShowProposedSwitch; var config bool bOnlyFlashInvolvedPlayers; var int pidsRequestingRebalance[64]; var int lastRebalanceRequestTime; // Checking and warning? var config bool bCheckStrengthBalance; var config bool bWarnMidGameUnbalance; var config bool bShowReason; var config bool bFlashOnWarning; var config bool bShakeOnWarning; var config bool bBuzzOnWarning; var config bool bShakeWhenMoved; // Flashing? var config bool bFlashPlayerJoins; var config bool bFlashTeamStrengths; var config bool bBroadcastCookies; var config bool bFlashCookies; var config bool bReportStrengthAsCookies; var config int FlashLine; var config bool bHelpInPugs; // 2.0 - bATBDuringWarmupAndPause // Debugging? var config bool bLogging; var config bool bLogDeletedRecords; // #define bLogDeletedRecords True var config bool bBalanceBots; var config bool bRankBots; var config bool bDebugLogging; var config bool bBroadcastTeamStrengths; var config bool bBroadcastTeamStrengthDifference; var config bool bAllowSemiAdminKick; var config bool bAllowSemiAdminForceTravel; var config String SemiAdminPass; var config bool bLogExtraStats; var config float MaxHoursWhenCopyingOldRecord; var config float HoursBeforeRecyclingStrength; var config int MinHumansForStats; var config int ScoringMethod; // 0=score, 1=frags, 2=average_frags_and_score, 3=0-100_ordered_ranking, 4=75%frags,25%score var config bool bNormaliseScores; // var config bool bRelativeNormalisation; var config float RelativeNormalisationProportion; var config float StrengthProportionFromCurrentGame; // TODO: This should swing depending whether records are from last game or two weeks ago. var config float PreferenceToSwitchNewPlayers; // Misnamed. Should be ToChange. Also, a better approach might be - only switch players who are so-far down the scoreboard, or so-far down the list of recently joined players. // TODO: var config float HowFarDownAgeList; var config bool bScalePlayerScoreToFullTime; // Leave this true, more accurate this way var config int NormalisedStrength; var config int UnknownStrength; var config int BotStrength; var config int FlagStrength; var config int StrengthThreshold; var config int WinningTeamBonus; var config int ScoreThresholdLow; var config int ScoreThresholdHigh; var config bool bClanWar; var config string clanTag; // var config bool bUseOnlyInGameScoresForRebalance; var config bool bLogFakenickers; var config bool bBroadcastFakenickers; var config bool bLetPlayersViewStrengths; var config bool bAllowUsersToListFakes; var config bool bUseISPNotFullIP; var config bool bShuffleTeamsEarly; // BUG: unstable! var config string LastUpdate; var config Color strengthColor,warnColor; var config String playerData[4096]; // String-format of the player data stored in the config (ini-file), including ip/nick/avg_score/time_played data // For storing player strength data: var config int MaxPlayerData; var config float DBVersion; // Temporary state: // Internal (parsed) player data: var bool CopyConfigDone; // set to true after the arrays have been populated (so we don't do it twice) var String rkey[4096]; // Not yet stored in config file! Must be regenerated when needed. var String ip[4096]; // We could consider using instead the default struct Guid { var int A, B, C, D; }; // These 4 are copied from the config file, and used to re-generate playerData later. var String nick[4096]; var float avg_score[4096]; var float hours_played[4096]; var String date_last_played[4096]; // var int games_played[MaxPlayerDataMax]; // Runtime var bool bSuggesting; // When true means we should not switch players, we are just doing a dummy run to build up SuggestedChanges. var String SuggestedChanges; // TODO: bCached[0] is not implemented properly - is it undesirable to fix it? var int bCached[64]; // TODO CONSIDER: One change that bCached has introduced: if the player changes nick after they have been looked up, ATB will continue to use their old record. However, I don't think this is a major problem. The new record should be copied from the old record at the next game. var float currentDateDays; // Used by FindOldestPlayerRecordMeasure(). // For local state caching (not repeating when called by Tick's or Timer's): var bool initialized; // Mutator initialized flag var bool gameStartDone; // Teams initialized flag (we never initialise this to False, but I guess Unreal does that for us) var bool gameEndDone; var int timeGameStarted; var int lastBalanceTime; var float averageGameScore; var float averagePlayerStrengthThisGame; var float LastCalculatedAverages; var Color colorWhite,colorRed,colorBlue,colorGreen,colorYellow,colorCyan,colorMagenta,colorGray,colorBlack; // TODO! var PlayerPawn LastPlayerToJoin; // Originally intended as the last player to join an even (2v2) game and unbalance it. But now also may hold other players who are offering themselves for auto-switching. var int CleanupProgress; // Default values: // DefaultLog is important stuff that gets logged even if bLogging=False, but it has the same formatting as the other log levels. // #define DebugLog(X); // #define NormalLog(X); Log(X); // #define NormalLog(X); if (bLogging) { Log(X); } // ==== Hooks or overrides - functions and events called externally: ==== // // Initialize the system function PostBeginPlay() { Super.PostBeginPlay(); // Forcing some defaults for XOL: // bFlashPlayerJoins=False; // bAutoSwitchNewPlayers=True; // TODO XOL BUG: Because it happens before we get their IDC - bad! // bFlashOnWarning=true; // bFlashRebalanceRequest=true; // FlashLine=0; // WinningTeamBonus=5; // bLogDeletedRecords=True; // bWarnMidGameUnbalance=True; // Test to see if this stops ATB from being destroyed at 32 seconds. if (initialized) { ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".PostBeginPlay() called with initialized already true; quitting."); }; return; } ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".PostBeginPlay() initialising"); }; currentDateDays = DaysFromDateString(GetDate()); // For FindOldestPlayerRecordMeasure(). // If AutoTeamBalance was installed as a ServerActor, we need to register it as a mutator: // Self.AddMutator() will check that it is not already in the mutator chain. Level.Game.BaseMutator.AddMutator(Self); if (initialized) { // Another copy of ATB may have disabled us during AddMutator() by setting our initialized to True. ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".PostBeginPlay() disabling self on request"); }; gameStartDone=True; // Disable('Tick'); // TODO: Self.Destroy(); // This has been called already. return; } ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".PostBeginPlay() added self as mutator"); }; // We always want to register as a messenger, so that players may type "!red" or "!blue" Level.Game.RegisterMessageMutator(Self); ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".PostBeginPlay() registered self as messenger"); }; // if (TeamGamePlus(Level.Game) != None && bEnablePlayerCommands) { if (TeamGamePlus(Level.Game) != None && bLetPlayersRebalance) { // TeamGamePlus(Level.Game).TeamChangeMessage = "Type !red or !blue to change team."; TeamGamePlus(Level.Game).TeamChangeMessage = "Type !teams if they become uneven."; } // This is how we detect the moment just before game-start (in CheckGameStart()), to do a final team balance: SetTimer(1,True); gameEndDone = false; // Kinda redundant, since it will have been default initialised to false anyway. CopyConfigIntoArrays(); // First time the data is needed, we must convert it. CleanupDatabase(); initialized = true; } // Implementation of AddMutator which prevents double or recursive adding: function AddMutator(Mutator Other) { // DebugLog(Self$".AddMutator("$Other$") called."); if (Other != None && Other.Class == Self.Class) { if (Other == Self) { ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".AddMutator("$Other$") No need to add mutator self again."); }; } else { ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ Self$".AddMutator("$Other$") Destroying other instance with "$Other$".Destroy()"); }; AutoTeamBalance(Other).initialized = true; // tell the other copy it should not initialize Other.Destroy(); // seems to do nothing useful; the mutator continues to run through PostBeginPlay(). } } else { // DebugLog(Self$".AddMutator("$Other$") Calling Super.AddMutator()."); Super.AddMutator(Other); } } // There was a problem whereby it was getting destroyed (due to taking 3 seconds to process CheckMidGameBalance), and then the new one was perform ForceFullTeamsRebalance! I think this is fixed now, but disabling this code until I have time to watch it a bit. ;) OR ... make it an option ... OR ... somehow make it auto-fix (aka disable) when that happens. :P // #define DESTROY_RECOVERY // Disabled cos it might be causing lag on XOL, and sometimes it rebalances just after play started. :f event Destroyed() { local AutoTeamBalance newATB; local int i; Log(Self$".Destroyed() I was destroyed at "$Level.TimeSeconds); // Find out why - should create a stack-trace in the log: // if (FRand()<0.2) { // assert(false); // } Super.Destroyed(); } // Timer is initially set at 1 second to detect the moment before game-start for ForceFullTeamsRebalance(). // Then it is set to CheckFrequency seconds during play, to detect mid-game unbalance, if bWarnMidGameUnbalance or bForceEvenTeams is set. // Also (after HandleEndGame() is called), it detects the real game end, and calls UpdateStatsAtEndOfGame(). event Timer() { if (!gameStartDone) CheckGameStart(); if (gameStartDone) CheckGameEnd(); // This is where gameEndDone is determined if ((bWarnMidGameUnbalance || bForceEvenTeams) && gameStartDone && !gameEndDone && Level.Game.IsA('TeamGamePlus') && !DeathMatchPlus(Level.Game).bTournament ) { CheckMidGameBalance(); } // Reset frequency. (Was not really needed until we set it low in HandleEndGame().) if (gameStartDone) SetTimer(CheckFrequency,!gameEndDone && (bWarnMidGameUnbalance || bForceEvenTeams)); } // If a new player joins a game which has already started, this will send him to the most appropriate ("weaker") team (based on summed strength of each team, plus capbonuses). // This may cause a little lag on slow CPU servers when a new player joins, because it will search the whole database to find his record; if this is a problem, set bUseOnlyInGameScoresForRebalance. function ModifyLogin(out class SpawnClass, out string Portal, out string Options) { local int selectedTeam; local int teamSize[2]; local int teamSizeWithBots[2]; local int teamStr[2]; // each team's strength, only used if the #players on each team is equal local int teamnr; local String plname; local Pawn p; local TournamentGameReplicationInfo GRI; if (NextMutator!= None) NextMutator.ModifyLogin(SpawnClass, Portal, Options); if (bRandomColorsInLMS && Level.Game.IsA('LastManStanding')) { selectedTeam=Level.Game.GetIntOption(Options,"Team",255); teamnr=Rand(5); if (teamnr!=selectedTeam) Options="?Team=" $ teamnr $ Options; return; } if (!ShouldBalance(Level.Game)) return; ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ModifyLogin("$SpawnClass$","$Portal$",\""$Options$"\")"); }; // read this player's selected team selectedTeam=Level.Game.GetIntOption(Options,"Team",255); // Check team balance of current players in game // Calculate sum of player strengths for each team (as well as the flagbonus above) // initiate team scores GRI=TournamentGameReplicationInfo(Level.Game.GameReplicationInfo); teamStr[0]=GRI.Teams[0].Score*GetFlagStrength(); teamStr[1]=GRI.Teams[1].Score*GetFlagStrength(); teamSize[0]=0; teamSize[1]=0; teamSizeWithBots[0]=0; teamSizeWithBots[1]=0; for (p=Level.PawnList; p!=None; p=p.NextPawn) { // ignore non-player pawns if (p.bIsPlayer && !p.IsA('Spectator')) { teamnr=p.PlayerReplicationInfo.Team; if (teamnr<2) { // I changed this from Daniel's version, so that bot strengths are not considered. // Since a player is joining, one of the bots may leave, or switch team, so counting that bot's strength is inaccurate, and we don't know which bot it will be. So let's just count player strengths. if (!p.IsA('Bot')) { teamSize[teamnr]++; teamStr[teamnr] += GetPlayerStrength(p); } teamSizeWithBots[teamnr]++; } } } if (bClanWar) { // send player to his clan's team teamnr=0; plname=Level.Game.ParseOption(Options,"Name"); if (Instr(Caps(plname),Caps(clanTag))==-1) teamnr=1; } else { // Do we even know the strength of the joining player? // if both teams have the same number of players send the new player to the weaker team if (teamSize[0]==teamSize[1]) { // teamnr=0; if (teamStr[0]>teamStr[1]) teamnr=1; teamnr=0; if (teamStr[0]>=teamStr[1]+Rand(2)) teamnr=1; ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ModifyLogin() "$teamSize[0]$"v"$teamSize[1]$" and "$teamStr[0]$"v"$teamStr[1]$" so sending new player to WEAKER team "$getTeamName(teamnr)$"."); }; } else { // send player to the team with fewer players // teamnr=0; if (teamSize[0]>teamSize[1]) teamnr=1; teamnr=0; if (teamSize[0]>=teamSize[1]+Rand(2)) teamnr=1; ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ModifyLogin() "$teamSize[0]$"v"$teamSize[1]$" so sending new player to SMALLER team "$getTeamName(teamnr)$"."); }; } } // if selected team does not equal forced team then modify login if (teamnr!=selectedTeam) Options="?Team=" $ teamnr $ Options; FixTeamsizeBug(); } function FixTeamsizeBug() { local TournamentGameReplicationInfo GRI; local Pawn p; local int teamnr; local int teamSizeWithBots[2]; GRI=TournamentGameReplicationInfo(Level.Game.GameReplicationInfo); for (p=Level.PawnList; p!=None; p=p.NextPawn) { if (p.bIsPlayer && !p.IsA('Spectator')) { teamnr=p.PlayerReplicationInfo.Team; if (teamnr<2) { teamSizeWithBots[teamnr]++; } } } // Fix teamsize bug in Botpack.TeamGamePlus if (GRI.Teams[0].Size!=teamSizeWithBots[0] || GRI.Teams[1].Size!=teamSizeWithBots[1]) { ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "FixTeamsizeBug() Fixing team size (" $ GRI.Teams[0].Size $ "," $ GRI.Teams[1].Size $ ") should be (" $ teamSizeWithBots[0] $ "," $ teamSizeWithBots[1] $ ")"); }; GRI.Teams[0].Size=teamSizeWithBots[0]; GRI.Teams[1].Size=teamSizeWithBots[1]; } } // We use MutatorTeamMessage and MutatorBroadcastMessage to catch messages said by players and spectators respectively. // TODO: this only detects player joins. I fear to detect players leaving, we must read the MutatorBroadcastMessage! function ModifyPlayer(Pawn paw) { local PlayerPawn p; local float strengthSwing; ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ModifyPlayer("$paw.getHumanName()$") called."); }; // Check if this is the player's first spawn: p = PlayerPawn(paw); if (p!=None && p.PlayerReplicationInfo.Deaths == 0 && Spectator(p)==None) { if (gameStartDone && Level.TimeSeconds >= timeGameStarted+10) { ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ModifyPlayer() Player join detected: "$p.getHumanName()$" ("$getTeamName(p.PlayerReplicationInfo.Team)$")"); }; if (bFlashPlayerJoins) { ClearAllProgressMessages(); FlashToAllPlayers(p.getHumanName()$" has joined the game!",colorGreen,1); } // Do LastPlayerToJoin balancing: CheckNewPlayer(p); //// TODO XOL BUG: We don't have accurate strengths until we have their Idc record! // strengthSwing = GetTeamStrength(1)-GetTeamStrength(0); // if (Abs(strengthSwing)>50) { // This may have changed, but what it prints won't have. // BroadcastMessageAndLog( getTeamName( Int( (1+Sgn(strengthSwing))/2 ) ) $" team leads by strength "$ Abs( strengthSwing ) ); // BroadcastTeamStrengths(); // He should be on the right team by now. // } // else broadcast "and he has helped to balance the teams :)" } } Super.ModifyPlayer(paw); } function CheckNewPlayer(PlayerPawn p) { if (bAutoSwitchNewPlayers && gameStartDone && CountHumanPlayers()>2 && !DeathMatchPlus(Level.Game).bTournament) { if ( (CountHumanPlayers() % 2) > 0 ) { // This player has made the teams uneven by playercount! LastPlayerToJoin = p; // NormalLog("ModifyPlayer() Setting "$p.getHumanName()$" as LastPlayerToJoin."); // strengthSwing = GetTeamStrength(1)-GetTeamStrength(0); // if (Abs(strengthSwing)>70 && GetPlayerStrength(p)>70) { // NormalLog("ModifyPlayer() And warned him because strengthSwing "$strengthSwing$" > 70 and playerStrength > 70 !"); // p.ClientMessage("You may be switched with the next player who joins."); // } } else { // This player has made the teams even by playercount. // But teams might be better if he switched with the LastPlayerToJoin ... CheckTwoNewPlayers(LastPlayerToJoin, p); // LastPlayerToJoin = None; LastPlayerToJoin = p; } } } // A joined the game recently. B is joining now and is about to spawn. // This function will decide whether to swap the two players. function CheckTwoNewPlayers(PlayerPawn A, PlayerPawn B) { local float strengthA,strengthB,redTeamStrength,blueTeamStrength; local float oldDifference,newDifference,delta; local float timeInGameA,timeInGameB; if (A == None) return; if (A.PlayerReplicationInfo.Team == B.PlayerReplicationInfo.Team) return; if (A.PlayerReplicationInfo.HasFlag!=None) // no need to check B, he just joined return; // damn we can't switch him now he has the flag, he got away with it! // TODO: We might also be slightly disinclined to switch him if he just got belt or amp. If he loses armour/vials/weapons well tough. // We could work Resurrector style and restore his location and inventory just keep his team switched ^^ // TODO: Do not switch him if he has a warhead (or subclass). redTeamStrength = GetTeamStrength(0); blueTeamStrength = GetTeamStrength(1); oldDifference = blueTeamStrength - redTeamStrength; if (Abs(oldDifference) < 40) // teams are not unbalanced return; delta = GetPlayerStrength(A) - GetPlayerStrength(B); if (A.PlayerReplicationInfo.Team == 1) delta = -delta; newDifference = blueTeamStrength - redTeamStrength + delta*2.0; timeInGameA = Level.TimeSeconds - A.PlayerReplicationInfo.StartTime; timeInGameB = Level.TimeSeconds - B.PlayerReplicationInfo.StartTime; // Only needed since we moved CheckNewPlayer() into bWaitForIDC. if (Abs(newDifference) >= Abs(oldDifference)) // would make balance worse! return; if (Abs(oldDifference)-Abs(newDifference) < 15) // insignificant improvement in balance (less than 7.5 between these two players) return; if (timeInGameA:". We remove this. CheckMessage(Mid(Msg,InStr(Msg,":")+1), Receiver); } return Super.MutatorBroadcastMessage(Sender,Receiver,Msg,bBeep,Type); } // Catch messages from players: function bool MutatorTeamMessage(Actor Sender, Pawn Receiver, PlayerReplicationInfo PRI, coerce string Msg, name Type, optional bool bBeep) { // Swallow lines containing the semi-admin pass: if (StrContains(Caps(Msg),Caps(GetEffectiveSemiAdminPass()))) return False; if (Sender == Receiver) { // Only process the message once. ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "MutatorTeamMessage() Checking ("$Sender.getHumanName()$") "$Msg$""); }; CheckMessage(Msg, Receiver); } return Super.MutatorTeamMessage(Sender,Receiver,PRI,Msg,Type,bBeep); } function ShowStatsTo(PlayerPawn Sender,optional bool showIPs) { local int i; local Pawn p; local String ipstr; ShowLineTo(Sender,"Team | Name | IP | Ping | PktLoss | Strength | Hours | Last"); for (p=Level.PawnList; p!=None; p=p.NextPawn) { if (!p.IsA('Spectator') && AllowedToRank(p)) { i = FindPlayerRecord(p); if (showIPs) ipstr = getIP(p); else ipstr = "-"; if (i>-1) { ShowLineTo(Sender,""$p.PlayerReplicationInfo.Team$" | "$p.getHumanName()$" | "$ipstr$" | "$p.PlayerReplicationInfo.Ping$" | "$p.PlayerReplicationInfo.PacketLoss$" | "$Int(avg_score[i])$" | "$Int(hours_played[i])$" | "$date_last_played[i]$""); } } } ShowLineTo(Sender,"Name | Score | Frags | Deaths | Items | Spree | Secret | Time"); for (p=Level.PawnList; p!=None; p=p.NextPawn) { if (!p.IsA('Spectator') && AllowedToRank(p)) { ShowLineTo(Sender,""$p.getHumanName()$" | "$Int(p.PlayerReplicationInfo.Score)$" | "$p.KillCount$" | "$Int(p.PlayerReplicationInfo.Deaths)$" | "$p.ItemCount$" | "$p.Spree$" | "$p.SecretCount$" | "$Int(Level.TimeSeconds - p.PlayerReplicationInfo.StartTime)$""); } } } function ListFakesTo(PlayerPawn Sender) { local PlayerPawn p; local String nickList; foreach AllActors(class'PlayerPawn',p) { // We do list spectators, but not the UTServerAdminSpectator that some servers have. // if (InStr(String(p.class),"UTServer")<0) { // DONE: on [-u-] there must be some other spectator actors - either hide all spectators, or show only those who are player spectators (e.g. they have an IP) if (!p.bIsPlayer) { // This still catches spectators, but only those "possessed" by real players. ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "ListFakesTo() Skipping "$p.getHumanName()$" ("$p$")"); }; } else { nickList = GetNicksFor(p); if (nickList != "") Sender.ClientMessage(p.getHumanName() $" has other nicks: "$ nickList); else Sender.ClientMessage(p.getHumanName() $" has ip: "$ ip[FindPlayerRecordGuaranteed(p)]); } } } function LogSituation() { ShowStrengthsTo(None,true); ShowStatsTo(None,true); // TODO: ListFakesTo log } function ShowStrengthsTo(PlayerPawn Sender,bool bExtra) { local Pawn p; local int team; local int i; local float playerGameStrength,deltaStrength; local string deltaStrengthStr; local string redBonus,blueBonus; if (bExtra) deltaStrengthStr = "(+/-) GameStrength UsedStrength "; ShowLineTo(Sender,"[Team] Strength "$deltaStrengthStr$"| Name | Time"); GetAveragesThisGame(); for (team=0;team<2;team++) { for (p=Level.PawnList; p!=None; p=p.NextPawn) { if (AllowedToBalance(p) && p.PlayerReplicationInfo.Team == team) { i = FindPlayerRecord(p); if (i > -1) { // actually it's guaranteed to be > -1 // ShowLineTo(Sender,"["$getTeamName(p.PlayerReplicationInfo.Team)$"] "$p.getHumanName()$" has strength "$Int(avg_score[i])$" after "$Left(""$hours_played[i],5)$" hours."); if (bExtra) { playerGameStrength = NormaliseScore(GetScoreForPlayer(p)); // playerGameStrength = averagePlayerStrengthThisGame; deltaStrength = playerGameStrength - avg_score[i]; deltaStrengthStr = ""$Int(deltaStrength+0.5); if (deltaStrength>0) deltaStrengthStr = "+"$deltaStrengthStr; deltaStrengthStr = "(" $ deltaStrengthStr $ ") " $ Int(playerGameStrength) $ " " $ Int(GetPlayerStrength(p)) $ " "; } ShowLineTo(Sender,"["$getTeamName(p.PlayerReplicationInfo.Team)$"] "$Int(avg_score[i])$" "$deltaStrengthStr$"| "$p.getHumanName()$" | "$Left(""$hours_played[i],4)$" hours"); } } } } if (GetFlagStrengthForTeam(0) > 0) redBonus = " + " $ Int(GetFlagStrengthForTeam(0)); if (GetFlagStrengthForTeam(1) > 0) blueBonus = " + " $ Int(GetFlagStrengthForTeam(1)); ShowLineTo(Sender,"| Red team strength is "$Int(GetTeamStrengthNoFlagStrength(0))$redBonus$", Blue team strength is "$Int(GetTeamStrengthNoFlagStrength(1))$blueBonus$" (difference "$Int(GetTeamStrength(1)-GetTeamStrength(0))$")."); ShowLineTo(Sender,"| Average strength is "$Left(""$averagePlayerStrengthThisGame,4)$" ("$Left(""$FloatWeUseForAverageGameStrength(),4)$"), teamscore bonus is "$Int(GetFlagStrength())$"."); } // Does a dummy mid-game rebalance, so that a semi-admin (or in fact any player) can see the proposed switches. function GetSuggestedChanges() { bSuggesting = True; // SuggestedChanges = ""; SuggestedChanges = "[" $ Int(GetTeamStrength(1) - GetTeamStrength(0)) $ "]"; // UpdateStatsAtEndOfGame(); // We undo this later with CopyConfigIntoArrays() but that will reset caching! TODO: do we even need this? // TODO CONSIDER: Make this a rebalance request, so multiple mutates from different players will take action // ForceFullTeamsRebalance(); MidGameRebalance(True); // despite bDo=True, it won't actually do it because ChangePlayerToTeam checks for bSuggesting. // CopyConfigIntoArrays(); bSuggesting = False; } function ShowLineTo(PlayerPawn p, String line) { if (p == None) { ; if (bLogging) { Log("[AutoTeamBalance] "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ line); }; } else { p.ClientMessage(line); } } function ListMutsTo(PlayerPawn Sender) { local Mutator m; local String s; local String package,nom; m = Level.Game.BaseMutator; while (m != None) { // s = s $ m.Class.Name; package = StrBeforeFirst(String(m.Class),"."); nom = StrAfterFirst(String(m.Class),"."); m = m.NextMutator; if (package==nom || package~="botpack") s = s $ nom; else s = s $ package $"."$ nom; if (m != None) s = s $ ", "; } if (Sender == None) { ; Log(".AutoTeamBalance. "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Mutators: "$s);; } else { Sender.ClientMessage("Mutators are: "$s); } } function String GetNicksFor(PlayerPawn p) { local int i,j; local String pip,list,pnick,onick; i = FindPlayerRecordGuaranteed(p); if (i == -1) return ""; pip = ip[i]; pnick = nick[i]; list = " "; for (j=0;j60) break; } } } list = Mid(list,1); // remove leading space if (Len(list)>2) list = Left(list,Len(list)-2); // remove trailing space-comma return list; } function String GetEffectiveSemiAdminPass() { if (SemiAdminPass == "defaults_to_admin_pass") return ConsoleCommand("get engine.gameinfo AdminPassword"); else return SemiAdminPass; } // Catch mutate messages (from players, semi-admins or admins) function Mutate(String str, PlayerPawn Sender) { local String args[256]; // local array args; local int argcount; local String localPass; // the password we will require for semi-admin commands local String pass_if_needed; // for the help (to display whether pass is needed or not) // temporary utility vars local String msg; local int i; local Pawn p; local PlayerPawn pp; local bool bTempBool; local String mutStr; local class mutClass; local Actor a; local Mutator mut,nmut; ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Mutate("$str$","$sender$") was called."); }; // #ifdef LOG_LAG // debugTimerReason = "ATB caught \"mutate "$str$"\" from "$sender.getHumanName(); // debugTimerStart = Level.TimeSeconds; // #endif // What password must they supply to perform semi-admin commands? ("" = doesn't need to provide a password) localPass = GetEffectiveSemiAdminPass(); if (Sender.bAdmin) localPass = ""; // no pass is required if (Left(str,4) ~= "ATB ") str = Mid(str,4); argcount = SplitString(str," ",args); // Commands which do not require a password: if (args[0]~="TEAMSTRENGTH") { Sender.ClientMessage(GetTeamStrengthString()); } if (args[0] ~= "SUGGEST") { GetSuggestedChanges(); if (SuggestedChanges == "") { Sender.ClientMessage("ATB has no idea how to improve the team balance."); } else { Sender.ClientMessage("Proposed team changes: "$SuggestedChanges); } } /* // We don't get SuggestedChanges back! The current dodgy implementation always broadcasts! if (args[0] ~= "BROADCASTSUGGESTION" || args[0]~="SUGGESTSHOW") { GetSuggestedChanges(); // if (SuggestedChanges != "") { // BroadcastMessageAndLog("Teams could be improved: "$SuggestedChanges); // } } */ // BroadcastMessageAndLog("Proposed team changes: "$SuggestedChanges); if (bLetPlayersViewStrengths || localPass=="") { if ( args[0]~="STRENGTHS" || args[0]~="STRENGTH" ) { // debugTimerReason = "mutate strengths from "$Sender.getHumanName(); // debugTimerStart = Level.TimeSeconds; ShowStrengthsTo(Sender, (args[1] ~= "EXTRA")); } } if ( args[0]~="STATS" ) { ShowStatsTo(Sender,Sender.bAdmin || localPass==""); } if ( args[0]~="LOGSTATS" ) { ShowStatsTo(None,true); } if ( args[0]~="LISTMUTS" || args[0]~="LISTMUTATORS" ) { Sender.ClientMessage("Game type is "$ Level.Game.Name); ListMutsTo(Sender); } if ( (args[0]~="LISTNICKS" || args[0]~="LISTFAKES" || args[0]~="LISTFAKERS") && (bAllowUsersToListFakes || Sender.bAdmin) ) { ListFakesTo(Sender); } // Commands which do require the password: if (localPass=="" || args[argcount-1]~=localPass) { // Semi-admin privilege commands: switch ( Caps(args[0]) ) { case "TEAMS": if (!Level.Game.GameReplicationInfo.bTeamGame) { Sender.ClientMessage("AutoTeamBalance cannot balance teams: this isn't a team game!"); } else { // TODO: do we need a check here for bTournament+bHelpInPugs? MidGameRebalance(True); } break; case "FORCETEAMS": // Sender.ClientMessage("AutoTeamBalance performing full teams rebalance..."); // if (bBroadcastHelloGoodbye) { BroadcastMessageAndLog(Sender.getHumanName()$" has forced a full teams rebalance."); } // To make this balance as accurate as possible, we update the stats now, so we can use the scores from this game so-far. // But since this would mess up the end-game stats updating (counting this part of the game twice), we restore the stats from the config afterwards. UpdateStatsAtEndOfGame(); ForceFullTeamsRebalance(); CopyConfigIntoArrays(); break; case "TORED": // if (bBroadcastHelloGoodbye) { BroadcastMessageAndLog(Sender.getHumanName()$" is trying to fix the teams."); } ChangePlayerToTeam(FindPlayerNamed(args[1]),0,true); // Sender.ClientMessage("Red team strength is now "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$"."); BroadcastTeamStrengths(); break; case "TOBLUE": // if (bBroadcastHelloGoodbye) { BroadcastMessageAndLog(Sender.getHumanName()$" is trying to fix the teams."); } ChangePlayerToTeam(FindPlayerNamed(args[1]),1,true); // Sender.ClientMessage("Red team strength is now "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$"."); BroadcastTeamStrengths(); break; case "TOGREEN": ChangePlayerToTeam(FindPlayerNamed(args[1]),2,true); // Sender.ClientMessage("Red team strength is now "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$"."); BroadcastTeamStrengths(); break; case "TOGOLD": ChangePlayerToTeam(FindPlayerNamed(args[1]),3,true); // Sender.ClientMessage("Red team strength is now "$Int(GetTeamStrength(0))$", Blue team strength is "$Int(GetTeamStrength(1))$"."); BroadcastTeamStrengths(); break; case "SWITCH": SwitchTwoPlayers(Sender,args[1],args[2]); break; case "SWAP": SwitchTwoPlayers(Sender,args[1],args[2]); break; case "WARN": msg=""; for (i=2;i(DynamicLoadObject(mutStr, class'Class')); mut = Spawn(mutClass,None,,Self.Location); if (mut == None) { // BroadcastMessageAndLog("! Prepare for new mutator !"); Sender.ClientMessage("Failed to load mutator \""$args[1]$"\". Try \"mutate addmut \"."); } else { // BroadcastMessageAndLog("! Prepare for new mutator "$args[1]); BroadcastMessageAndLog("[+] Adding mutator: "$mutClass.Name); // mut.NextMutator = Level.Game.BaseMutator.NextMutator; // Level.Game.BaseMutator.NextMutator = mut; Level.Game.BaseMutator.AddMutator(mut); } break; case "DELMUT": if (args[1]=="") { // BroadcastMessageAndLog("! Prepare for mutator removal !"); Sender.ClientMessage("List mutators with \"mutate listmuts\", delete one with \"mutate delmut \"."); } else { mut = Level.Game.BaseMutator; // while (mut != None) { // if (StrContains(CAPS(""$mut),CAPS(args[1]))) { // Sender.ClientMessage("Destroying mutator: "$mut); // mut.Destroy(); break; // Sender.ClientMessage("Result: "$mut); // mut = None; while (mut.NextMutator != None) { nmut = mut.NextMutator; if (StrContains(CAPS(""$nmut),CAPS(args[1]))) { BroadcastMessageAndLog("[X] Destroying mutator: "$nmut.Class.Name); mut.NextMutator = nmut.NextMutator; // Destroy old mut, otherwise Timer() and Tick() functi_ns may continue! nmut.Destroy(); if (nmut!=None) { ; if (bDebugLogging) { Log("+AutoTeamBalance+ "$ PrePad(Int(Level.TimeSeconds)," ",4) $" "$ "Destroyed mutator != None"); }; } break; } mut = mut.NextMutator; } } break; Default: break; } } // Admin only commands: // These are not really needed for AutoTeamBalance, but useful commands nonetheless. // You can use "mutate set" instead of "admin set". It provides confirmation of the new value, or the existing value if a change could not be made. if (Sender.bAdmin) { switch ( Caps(args[0]) ) { // GET and SET are redundant. They act pretty much the same as "admin GET|SET", but are unneccessarily verbose, much like myself. // Allows admins to read variables from the config files (and maybe some live variables too; untested) case "GET": // Prev_nt reading of password records (so that granting someone you trust temp admin is "safer"): if (StrContains(Caps(args[2]),"PASSWORD")) { // "PASS" or "PASS "? BroadcastMessageAndLog("[WARNING] Temporary admin "$Sender.getHumanName()$" tried to perform: mutate "$msg); Sender.bAdmin = False; // KickBan(Sender); } else { Sender.ClientMessage( args[1] $ ":" $ args[2] $ " = " $ ConsoleCommand("get " $ args[1] $ " " $ args[2]) ); } break; case "SET": // Always worth logging: // Log("AutoTeamBalance: "$Sender.getHumanName()$" performed mutate "$str$" (previous value was " $ ConsoleCommand("get " $ args[1] $ " "$ args[2])); msg=""; for (i=3;i | 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Å¡yDÀš-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' ÄNCAò[„‡z‚wC*rC¡¡½rCº-çpppppp[AutoTeamBalance] SD† , V.AddMutator(VC) No need to add mutator self again.wU-çpppppppp[AutoTeamBalance] SD† , V.AddMutator(VC) Destroying other instance with VC.Destroy().AC-D'Ca…ÄC ÂOQB„‰AçppV.Destroyed() I was destroyed at U†Â RAÔPCÆ‹Ç-c -f•‚‚‚‚„-[-Z- -_†‘a/!\ .—†‘-¤eÅ-a?@‚-_„-[-Z àS[['ÄG)w¦*¦[[\D´‚-|†‘a/!QF†‘XDTeam,ÿ§,²›FDpp?Team=SD Ï+†‘ G-çppppppppp+AutoTeamBalance+ SD† , ModifyLogin(V[,\,"D")F†‘XDTeam,ÿB.ž†‘§%eD«%B©¬"&eD«&B©¬"%1%&1%%A%&A%E†ßwE*È‚E-±Ea/!:E“šÈ–,»Ea/!¥1¡eDE¥AEEœ <-R%]†‘FDName9š~ë]ëSÿÿÿÿ&Cš%1&1%v™%e’&e§,&@-çppppppppppppp+AutoTeamBalance+ SD† , ModifyLogin() S%1vS&1 and S%evS&e so sending new player to WEAKER team  .%j™%1’&1§,&-çppppppppp+AutoTeamBalance+ SD† , ModifyLogin() S%1vS&1 so sending new player to SMALLER team  .?›FDpp?Team=SDC TfClלf.ž†‘§g†¹wg*¢‚g-±ga/!C:g“š¢–C,¥Chggœ6„›%f©¯%h#›&f©¯&hÙ-çppppppppppp[AutoTeamBalance] SD† , FixTeamsizeBug() Fixing team size (S%f©¯,S&f©¯) should be (S%h,S&h)%f©¯%h&f©¯&h ÅUDH‚¡æp-çppppp+AutoTeamBalance+ SD† , ModifyPlayer(D) called.S.‡DÙ‚‚wS*´S“¹?%r.ÁS*Ù‚-³†?’`, ‹-çppppppp[AutoTeamBalance] SD† , ModifyPlayer() Player join detected: S ( :S“š)Î-F7 pS has joined the game!@&]SÅD VE]™q¦…ƒ‚‚‚-- —, .—†‘-¤h±­??,?%EEƒ^EEEE X7^²t«^ r7* Dš:7“š:T“š cw7“´* F%G&b¯GF °ºb?,( H¯7Téš:7“š&H©HG®¯GF«H@^¯†?7“£_¯†?T“£z³ºGºb ”°¯ºbºG?, \‚°^?@ °_?,-çppppppppppp[AutoTeamBalance] SD† , CheckTwoNewPlayers() Auto switching T with 7. Team difference should change from Ub to UG.7“&:7“š'T“&:T“š(7kppTeams are fairer with you on  :T“š. You will not be switched again.ET [FÌYINÚ$· ëcë6(é‚rIHIa/!Ê-çppppppp+AutoTeamBalance+ SD† , MutatorBroadcastMessage() Checking (I) cIc’~c:&HÌIHc-VW €Ë\ILæ-ºçëJë6(¿rIJ¯-çppppppp+AutoTeamBalance+ SD† , MutatorTeamMessage() Checking (I) JIJJËIJXJY-Z €^K5òH¼^KTeam | Name | IP | Ping | PktLoss | Strength | Hours | Last†½w*¦‚a/! d:»-[KÄK-¦—dÿÿÿÿKppppppppppppppppR“š |  | K | S“à | R“Ç | SDd | SDd  | d œ\KName | Score | Frags | Deaths | Items | Spree | Secret | Time†\w*E‚a/! Kpppppppppppppppp | SD“¨ | S³ | SD“¹ | SÊ | SÈ | SÉ | SD¯†?“£œ ``D  ÀD/a0 ‡UA§U-±¤-çppppppp+AutoTeamBalance+ SD† , ListFakesTo() Skipping U (VU)@LaU{L`2ppU has other nicks: L@`5ppU has ip: ,U10 aEÄ'*'5*' bL'$HÅŽ1-aV(+/-) GameStrength UsedStrength Lpp[Team] Strength V| Name | TimeRM%B–M,2†8w2*!‚2 š:2“šMe:2!—eÿÿÿÿ”-aM/$2N¯MeVpSD®N?\±N?%Vp+VVpppppp(V) SDM SD2 Lpppppppppp[ :2“š] SDe V| 2 | €pUe , hours22œ“¥Msf±%?%bp + SD%б&?%cp + SD&Lpppppppp| Red team strength is SD9%b, Blue team strength is SD9&c (difference SD¯&%).Lpppppp| Average strength is €pUc, (€pUS,), teamscore bonus is SD". c_E*Í;-{')pp[SD¯&%]'-{( ddP¨ÏlVrd*S-çppp[AutoTeamBalance] SD† , ejd e ef`Z¶Ðqi†‘ªîwi*N>Vi¡.O0Vi¡.ii¦±„zNO|NbotpackWpWOÏWpppWN.Oëwi*WpW, Irf*çpppp.AutoTeamBalance. SD† , Mutators: WofpMutators are: W f\apÓ<f,\#šfÿÿÿÿgfhf * |%–|$ö‚zg| ›f|P| ö‚{hPš~*pp P, ÿÿÿÿ*pp*P, ö—}*,<¥|U**&4—}*,*€*“}*,* f€@gj6‡DÖWOzKdefaults_to_admin_passget engine.gameinfo AdminPasswordUK €@Æi}&%Ù q-çppppppp+AutoTeamBalance+ SD† , Mutate(},V) was called.6—-›º|€},ATB }},+T} |%TEAMSTRENGTH Jœ|%SUGGEST_mz)8ATB has no idea how to improve the team balance.œ&pProposed team changes: )ý„-R zý„|%STRENGTHS|%STRENGTH'|&EXTRA8|%STATS5„-› zV|%LOGSTATS5*'Ë„|%LISTMUTS|%LISTMUTATORS/pGame type is W†‘¶`?‚„„|%LISTNICKS|%LISTFAKES|%LISTFAKERS„-T-›DÊ„z|“+&ë% ûTEAMSñ†‘§-®EAutoTeamBalance cannot balance teams: this isn't a team game!ø'Ê FORCETEAMS=8OÊ GTORED &%' Ê pTOBLUE &&' Ê ›TOGREEN &,' Ê ÅTOGOLD &,' Ê íSWITCHF&,Ê SWAPF&,Ê |WARN,y–+o|pp ¥, &ê„z& r*7ppCould not find player matching "&".y.‡ Z,@@@úE€;F=ppp was sent the message "".Ê ýFLASH&â–+Ø|pp ¥•7 Z,Ê 9RENAME &ñ‚‚{& {,w.‡*€w“*“·,.‡psetname ,<ppppRenamed "" to ",".6<ppCould not find PlayerPawn with name "&".Ê çLISTIDSPlayer list with IDs:/a0 ‡Qãâ{Q“·Player:ppp[SQ“²] Q10Ê › KICK˜ „--›,j –+` |pp ¥  &Û „z& r*7ppCould not find player matching "&".˜ *pp was kicked for )pYou have been kicked for: -X-›-›'Y-›-XÊ s KICKIDp „--›, –+ |pp ¥Ó NJ&³ „„z& r*‚šJ&% {&07ppCould not find player matching "&".p *pp was kicked for )pYou have been kicked for: -X-›-›'Y-›-XÊ *KICKBAN'„--›,ù –+ï |pp ¥¬  &j „z& r*7ppCould not find player matching "&".'*pp was banned for )pYou have been banned for: -X-›-›'Z-›-XÊ KICKBANID„--›,²–+¨|pp ¥eNJ&E„„z& r*‚šJ&% {&07ppCould not find player matching "&".*pp was banned for )pYou have been banned for: -X-›-›'Z-›-XÊ ãFORCETRAVELà„-K-›d-}Admin is forcing a server switch!†àw*Éa/!.‡2.‡1&$(œxÊ öADDMUTi&P•Ñi ¥FaP*  ¿ rF*YppFailed to load mutator "&". Try "mutate addmut ".óp[+] Adding mutator: WP¶†‘ª AFÊ ÄDELMUTxz&]List mutators with "mutate listmuts", delete one with "mutate delmut ".ÁF†‘ªÁwF¦*~F¦ªëpV~ë&p[X] Destroying mutator: W~¡¶F¦~¦~a§w~*§-çppp+AutoTeamBalance+ SD† , Destroyed mutator != NoneÁFF¦•Ê ÿÿÊÝ-›ë% ÍGETxë,PASSWORDppp[WARNING] Temporary admin  tried to perform: mutate -›(ÊIpppp&:, = pppget & ,Ý SET,2–+(|pp ¥åpppppset & , Ipppp&:, = pppget & ,(pppppp changed &., to Ý GGETPROP(pp& =  ¾&Ý ;SETPROP,°–+¦|pp ¥cÒ&(pp& =  ¾&8(pppp changed & to Ý ÉCONSOLE&£–+™|pp ¥Vp Ý èCC,-–+#|pp ¥à &Žr*7ppCould not find player matching "&".å9pppRunning "" on player  Ý SAVECONFIG=Ý SAVEALLÝ ×GRANTADMIN &‘„r*–}&,7ppCould not find player matching "&".Ôb/ppAdmin toggled on .Ý ÿÿÝù|%HELPzOO [password]Ž-B`ppAutoTeamBalance1.4.9s commands: teams !teams !red !blue !spec !play !vote !statsÓ<ppAutoTeamBalance1.4.9s commands: teams !teamsqppAutoTeamBalance 1.4.9s mutate commands: mutate [atb] ( strengths [extra] | listmuts | listfakes )ßzCppAutoTeamBalance 1.4.9s semi-admin console commands:œ mutate [atb] ( teams | forceteams | tored

| toblue

| switch

| flash | warn

| kick

| kickban

‰p | listids | kickid | kickbanid | addmut | delmut | logstats | forcetravel ) O $ mutate help []ù-›ÒppAutoTeamBalance 1.4.9s admin-only console commands: mutate [atb] ( saveconfig | grantadmin

| get | set | getprop | setprop | console | cc

)Æ} F…kQF”· j@ kmr*Q5ppCould not find player matching "j". ¸r@*Q5ppCould not find player matching "k". Qš:“š:@“šQYppppPlayers "" and "@" are on the same team! l:@“šm:“šl'@m'  mRb*»"|z‚wR*Ra/!g.‡Rg-›g-›g“-Ûg-› ÍnpM4l%èa@„-[-Zß-çppppppp+AutoTeamBalance+ SD† , HandleEndGame() Set Timer() for 2 seconds. [bOverTime=T†‘-ß,bGameEnded=T†‘-¸]Í €oRc@ý( <‚+†‘M†‘G o.—†‘çn.—†‘éR.—†‘æRùR“noЖR,G-U.—†‘-¤8d „qjdaä./%†-w%*‚‚%a/!%a/!#š~V%¡UTServerÿÿÿÿj,¨†‘¡a/!3j,Ö†‘¡a/!4j,õš:†Ó:$j,S#%+†‘.—†‘-¤è-\-y %ppp%, you have SDS cookies.yjå %ppp% you have strength SDSyj %Teams not yet assigned.aj%%œ sGG°7Ÿ`D®†À?k+†‘D-U8G†kwG*T‚Ga/!Ga/!G“š »$AW Î$AX á$A@ ô$AY ÿÿAa GppYou are on the ë :G“š team.A,GGœX-'”„-[-Za?@'a( tHeÀ§Aü,ÿÿÿÿH %I &b™H’I,,&&pppp SHvSI.Ÿ˜H“I,,%&pppp SHvSI.‚-Z ›,ÿÿÿÿ' ú-[Óš,ÿÿÿÿö‚šHI-A Ó-BBD%CD&w‚™HI—B’C{,&&pppp Strength SB v SC.Ó‚˜HI—C’B{,%&pppp Strength SB v SC.äš,ÿÿÿÿ §-çpppppppppppppp[AutoTeamBalance] SD† , CheckMidGameBalance(SHvSI): doing warning SBvSC => weaker=S, problem=&º-G&΂-v-F[‚-w-v(X‚-G {&7-u pTeams look uneven!&QBXpTeams look uneven!&ú †úw *ã‚ a/! a/!Œš: “š,‰-w=-H.‡  ppTeams look uneven!& Type !teams to fix themQB‰ CppTeams look uneven!& Type !teams to fix them!W(ã-H.‡  ppppppTeams look uneven!& Type H-w!teams or !? ,QB ippppppTeams look uneven!& Type H-w!teams or !? ,!W(·-C @€?úDúDã-D a ä$ @(B€A  œo uk7$iTxk†vwk*_‚ka/!ka/!.‡kkkœ w]H.–U-]^_ €@x`T1V-`ab €zf4MVMK†‘-¸&-_ -_'KM†‘= |I?ªWîˆ-Bo‚‚ a/! a/!;‚w.’†‘* .’†‘-­¬„|!RED |!R.‡ %( å„|!BLUE |!B.‡ &(  „|!GREEN |!G.‡ ,( o„„|!GOLD|!YELLOW |!Y.‡ ,( "‚ a/! a/!"„„|!SPEC|!SPECTATE |!S.‡ 2.‡ -1?OverrideClass=Botpack.CHSpectator$(“ a/!“„|!PLAY |!P.‡ 2.‡ 1?OverrideClass=$( „„|!VOTE|!MAPVOTE |!V†‘ª%&bdbmapvote votemenu.‡ l„|!CTFSTATS|!CTF†‘ª &smartctf stats.‡ Ê|!STATS†‘ª &smartctf stats.‡ '.‡ 'ñ|!STRENGTHS'.‡ (5‚|!WHO „-T.‡ -›D.‡ å a/!²„„„|!WEBSITE |!W|!WEB|!WWW²{}(.‡ }å|!FORUMå{~(.‡ ~ˆ a/!0„|!TS|!TEAMSPEAKg.‡ ˆ„|!GETTS|!GetTeamSpeak(.‡ http://www.teamspeak.com/°„‚‚w.‡ *r.Á *|TEAMS|!TEAMS°‚-w+„-z .—†‘-¤ -çppppppp[AutoTeamBalance] SD† , MutatorTeamMessage()   requested rebalance with "".i.‡ ì}ë!MUTATE .‡ &  €}S(dÃSp>> Opening T›-çpppppp[AutoTeamBalance] SD† , Sending S to 0T://S2S1T$( hg`fC#T:h“šy‚‚†‘§-® ™T% –T,#Ttz##Í‚z# .—†‘-¤#%t$z#h?No TeamSpeak channel has been configured for this game.A©-çpppppp+AutoTeamBalance+ SD† , SendPlayerToTeamspeak(h): target url #1‚#teamspeak://#?phh#pppp>#??nickname=p?0#?(h# @ qhº.lýU%õ–U}q3íqU&Û„„„„„‚™3íA ˜3íZ‚™3ía ˜3íz‚™3í0 ˜3í9 š3í_ š3í+ š3í-ipiì3ëipi_¥Ui i€@A -8ß¡q2 (†‘§-® ‘-çppp+AutoTeamBalance+ SD† , ForceFullTeamsRebalance() Running...×-}AutoTeamBalance is attempting to balance the teams...-†iw-*R-rD-D­?-“²?,@Y-lrU%R-çppppppppppp+AutoTeamBalance+ SD† , ForceFullTeamsRebalance() Player - on team R-“š has db-key - and score U-“¨.--œëm%ÿÿÿÿV%%–,@ý‚‚wY*šU%$„šÿÿÿÿ—lVVl¥‰Ò›ÿÿÿÿmVU&¥mÒ-çppppppp+AutoTeamBalance+ SD† , ForceFullTeamsRebalance() [Ranking] Sl Ypšÿÿÿÿ8.’†‘s8¼-t8-»-u8-­8¼, 8-»(8-­(0-R%Z%&Z%%-–mV%ïš~ëY“·ëSÿÿÿÿ&Y-¡Zl¥äG°Ã?-X'O-X(%“–.’†‘«Z%¥V%W&%q–œm,þjû-Xj““.’†‘«&VÉ-çppppppppppp+AutoTeamBalance+ SD† , ForceFullTeamsRebalance() i=S Putting pid=S pl=Y into team Sj.Yj-¡jZl’WJš.’†‘«¦Wÿÿÿÿggšÿÿÿÿ¥W&¥¨äšœm&&V%W?B% –.’†‘«–ZWWZ¥©°-çppppppp+AutoTeamBalance+ SD† , ForceFullTeamsRebalance() Sm is odd so sending last player to WEAKER team S.Y-¡Zl8¼s8-»-t8-­-u C C E Vkƒg-IJe-I pppRed strength SD%, Blue strength SD&a& F G  D I J{‰ r.’†‘*F-Jn¯&%Dwell balancedz±ºn?,Dreasonably balanced¤±ºn?,(Da little unbalancedűºn?,FDunbalanced뱺n?,dDvery unbalancedv‘’&jn,ppppppTeams are D (+SDºn to  v)žž-IppppRed team strength is SD%, Blue team strength is SD&. n€@H ci—yŽ,G±†?’x,<[%G–[,@[w%¥[$c“²w&Y%[%¢–[,@˜›[w%¥Y¥[t9“DY›±?’A†“-çpppppp+AutoTeamBalance+ SD† , MidGameRebalance() refusing to rebalance since lastBalanceTime=S is too close to current time U†9,cÓ‚.—†‘-¤ –D,¥9/‚-C³º?“ % &?,9%/‚-C±º¯%&?{9%O˜9%'xÄÿÿÿ$žš9,cAutoTeamBalance refuses to rebalance teams again so soon. ±-v(Çš9&XÐXsÄ-uJ†ÁwJ*ª‚‚Ja/!Ja/!Ja/!.‡J JppppS9 more playerX must type !teams for rebalance.QBJJœí)ppppS9 more playerX must type !teams for rebalance.xD†  J wját$±w?%& °w?%ÿÿÿÿ% €K këО´@„†‘a/!\†‘-Õ k‚-k -{D†|-k7E %F &°‚šE% šF% #-çppppppp+AutoTeamBalance+ SD† , MidGameRebalance() SE v SF-l'R–EF-lK-k&%vv–FE-lK-k%&²„‚šEF -A -l-lk-k M ZK1¨Òxmyn\¯xyZ“ m nn‚°\?% –Z,({¯mn¿‚‚°º\?E –Z, ²º{?,(:†aw:*J‚‚: š::“šmr:“´*z:[¯†?:“£¸[ðBYº¯\«z?,]®««® @Y®«@ö¯€?A€??,d«[®«@öA€?J„rG* ‚°]\°Yº\G:oY\]::œÓírG*µ-Z)ppCould not find any player on  m to switchå-çppppppppppppppppppppppppp[AutoTeamBalance] SD† , MidGameTeamBalanceSwitchOnePlayer(T-Z) failed while S %vS & U%vU& SD!%-SD!& diff=U\ bestDiff=Uo bestP=VG bestScore=U\E(k³oº\i³º?Z?,)pp n team looks too strong. Considering switching three players...k(-ZGn-™LG*Á-{)ppp) [SDo]²oº\ x€N |kÁ¦[%\&o¯\[^o_«««pB?,<®?,ºo®?,ºo`º?“ & %}¯&%í‚‚°ºo?E °`?,²º}«À?"Teams are not uneven.(4†w4*x‚‚4š:4“š%r4“´*~4@¯†?4“£5†xw5*a‚‚‚w45 5š:5“š&r5“´*5A¯†?5“£]¯¯®\«~?,[«?,a„°º]ºo ³`?,^?úD@DA¸^pC_®«^®«@öA€?««®?,]®«@ö¯€?A€??,da°____^]p4q555œ¬44œ‚wp* wq*×-|p&-q%-çLpq-{)ppp) [SD^]'¤)AutoTeamBalance could not find two switches to improve the teams.œ-çppppppppppppppppppppppppppp[AutoTeamBalance] SD† , MidGameTeamBalanceSwitchTwoPlayers(T-|) failed while S %vS & SD[vSD\ U!%-U!& diff=Uo bestDiff=U^ bestScore=U_ redP=Vp blueP=VqE( [€P d)ÈÌÔ-d R ;LÓËÖ´¬rH*`pppType !teams to move ; to  “&:;“šapp; moves to  “&:;“š.`pppType !teams to swap ; with Happp; and H switchƒ-F±rH* ;ppPlease type !? “&:;“š to make the teams even!Q’B&U ;pPlease type !teams to switch team with HQ’B& HpPlease type !teams to switch team with ;Q’B&ppTeams may be better if a.²§-u `Q’B&²` `„S ö€Ý¾a/!¼-çppppppp[AutoTeamBalance] SD† , ChangePlayerToTeam(,S!): refusing to change the team of a spectator! •š!:“š“-çpppppppp[AutoTeamBalance] SD† , ChangePlayerToTeam(,S!): doing nothing since player is already on team S! ®„–!%?‚w.’†‘*$™!.’†‘«¬-çppppppppp[AutoTeamBalance] SD† , ChangePlayerToTeam(,S!): WARN FAIL teamnum must be in range 0-S“.’†‘«&. -{Ô{))p), )ppp) to  ! <a/!.Ïtaunt wave˜w.’†‘*-B.’†‘-­.’†‘-­(C-çppppppppppp[AutoTeamBalance] SD† , ChangePlayerToTeam(,S!): R“š -> S!†‘G!¢w.’†‘*.’†‘-­-BE*! ¿‚- .—†‘-¤¸“¨€?›-e! 2%IW{ D&IX{ W,I@{ j,IY{ ÿÿIa{%ppp has been moved to the  ! team..‡ ppYou have been moved to the ë ! team!I,E-E@@úD›‚{!t-z Type !TS to change teamspeak channel.a, U C= §îgZ-çpppp[AutoTeamBalance] SD† , Broadcasting: C%C V D*A kïçp[ATB] D%D W J E ÷ï"r.‡J* ‰-çpppppp+AutoTeamBalance+ SD† , Flashing message to J: EŸša%aB.‡J I @.‡JJfa.‡JKEa -Ja å$ @(BB X F W šóú|-çppppppp[AutoTeamBalance] SD† , FlashToAllPlayers(SG): Flashing "F"p†øwp*á‚pa/!pa/! pFgGppœ Y [ +b Ãõ /‚.—†‘-¤ -z(W†‘§-®({†‘a/!3-sµzV†‘¡Botpack.TeamGamePlus-t†‘a/!4ù›.º†‘è&(-u-v \ €”€Z ^ Mv wø¿$†‘a/!3-w^zV†‘¡Botpack.TeamGamePlus-x‹†‘¡a/!4-y¶†‘§-®-z-{ _ €”€] b‚ “úJba/!-GH‚ba/!ba/! €` H‰ ¦û2Ha/!-H0Ha/! €b h ’ ›üœc%q†”wq*}‚‚qa/!qa/! š:q“šh¥cqqœc c€d g › ¨ý¸d%K†°wK*™‚‚‚‚K-±Ka/!Ka/!Ka/!K-Þ¥dKKœd K€f i ¤ æþQHw.’†‘*i.’†‘µâONone €@h I¬ ®9II €j j!¯ “7j.ž†‘§©¬ €l k² )«!k" €n l9· Å©e]†¡w]*Š‚‚]-±]a/! š:]“šl¸e]]]œe ]€p s " p–,7†‘a/!3?]xzV†‘¡Botpack.TeamGamePlus¬?]`A£†‘a/!P¬?]`AƆ‘a/!4zV†‘¡.SiegeGI¬?]€@¬?]¨A €r KÖ *Â!³@€?/$K<²@#K´-J¯†?K“£´±J?,´®«/$K@«#K¯€?@#K J€t r#è + ò)‚rr?O<,rК<ÿÿÿÿÆ-çpppppp[AutoTeamBalance] SD† , Using UnknownStrength SB for r?Bð—°<?%†-çpppppppp[AutoTeamBalance] SD† , * Player r had negative avg_score=U< so resetting to 0.<ä°< €?®«< <«¯€?< ?Pð< <€v K  l²=†ªw=*“„=a/!=a/!h|=K=“™~ë=ëK%L===œL =€„x mN b^†Œw^*u„^a/!^a/!uš.‡^“²m^^^œ* ^€„z } O! (ö-U'x-çppppp+AutoTeamBalance+ SD† , CopyConfigIntoArrays()  running%]–$£zCSTC L%L &LL,L L,L ,LSz  2008/01/01-00:00:00¥%‡–,@^%¥dô-çppppp+AutoTeamBalance+ SD† , CopyConfigIntoArrays()  done L@| .P6 ¶—p-çppppp+AutoTeamBalance+ SD† , CopyArraysIntoConfig()  running.%(–.$½‚z. z..C.Cpppppppp. .  U. U.  . ¥.w•-çppppp+AutoTeamBalance+ SD† , CopyArraysIntoConfig()  done  MQB ú,$-SM..*M B €@@ NK œJ8Na/!Q|.‡NÝHQ0.0.0.0 €@C n[ PU>nQ-}>pp>@V†‘¡.M-~>p>:M†‘ªMwM*fVM¡.b%–b}fcífb&‚™cíA ˜cíZ>p>ìc¥b·MM¦JwM*>p>+‡> >€@E o:y $",o €G /,ˆ Â$-?D­?/“²?,@;—?^%?"l/¸‚‚›"ÿÿÿÿz/" z/";?"?^&?h—"ÿÿÿÿe-çpppppppp+AutoTeamBalance+ SD† , FindPlayerRecord() PARTIAL match for / @ /: " ÷÷-çppppppp+AutoTeamBalance+ SD† , FindPlayerRecord() FAILED match for / @ /.Lm/ó—"ÿÿÿÿ•-çppppppp+AutoTeamBalance+ SD† , FindPlayerRecord(p) COPY [SL] <- [S"]L"L ?ùDLD" L ó|/L ™-Pçppppppppp.AutoTeamBalance. SD† , Fakenicker / was previously "  (ip ")ó-Q%ppppp/ was previously "  (ip ")%›L?;?L?^&?L ?€I ;» e2Wÿ-çppppppppppppppppppp[AutoTeamBalance] SD† , SwapPlayerRecords() Swapping records S <-> S ( :(SD) <->  :(SD))OzP QRS T zz      zO PQR S T K UlÛ B7Š_U`U ÿÿÿÿ'%à–'$-gz_' -hz`'q‚-g-h 'h-çppppppppppppppp+AutoTeamBalance+ SD† , FindPlayerRecordNoFastHash(p) EXACT match for _,`: [S ] (U ,U  ,  ) Ö£-ho‚( ™ %çppppppppppppppppp.AutoTeamBalance. SD† , FindPlayerRecordNoFastHash(p) DUPLICATE IP match for _,`: [S ] '  (U ,U  ,  ) „š ÿÿÿÿ±-' d 'd-   -çppppppppppppppppp+AutoTeamBalance+ SD† , FindPlayerRecordNoFastHash(p) IP match for _,`: [S ] '  (U ,U  ,  )ÖÖ-g£‚( ™ %çppppppppppppppppp.AutoTeamBalance. SD† , FindPlayerRecordNoFastHash(p) DUPLICATE NICK match for _,`: [S ] '  (U ,U  ,  )Ö„š ÿÿÿÿ±-' d 'd-  Ö-çppppppppppppppppp+AutoTeamBalance+ SD† , FindPlayerRecordNoFastHash(p) NICK match for _,`: [S ]   (U ,U  ),  ¥'4‚š ÿÿÿÿ‚-çppppppp[AutoTeamBalance] SD† , FindPlayerRecordNoFastHash(_,`) failed to return a record.   €L Vm Fˆÿÿÿÿo?„–% ™$pX‚-J&‚z zçpppppppppppppppppp.AutoTeamBalance. SD† , CreateNewPlayerRecord() true DEL [S]    U U    (score U)q–,@^%ŠrV*<šnV€-çppppppppppppppppp+AutoTeamBalance+ SD† , CreateNewPlayerRecord() NEW [S]    U U    (score U) €N sn# +MesWs Ws?Bs s  Q <+ 6O D-JD„„„„{ {µ?%µ ?%{ çppppppppppppppppppp.AutoTeamBalance. SD† , CLEAR [S] z    U U    (score U)z    R XU: šQmrXe‚–r$–r’X,€[‚zr zrr¥r ÿÿÿÿ r€S V oE 2STN%L–N$B‚zN zNN¥Nÿÿÿÿ N€U X pO ‹T;2‚-†‘-¸q9r €W Z q] Wáe%Ù–e,sD®?,@«?“$,@ÃY‚zs zssZ¯~.s i¬®ÍÌÌ=s ®€?ZÏ„še% °i[Ys[i¥eY s€\ ] [Y _ rr ZÔ‰-çppp+AutoTeamBalance+ SD† , FindOldestPlayerRecordSlow() Looking for an old record to replace...M%f&È–f$MsMf¡f,€—Ì-çppppppppppppp+AutoTeamBalance+ SD† , FindOldestPlayerRecordSlow() Oldest/smallest record was M  with UM  hours and age U.M  days (M , strength UM).M f€^ js €]ˆkjO\€‚–O$–O’\,€lOv°lkjOkl¥Oj O€` mt’ a%m%#–m,@u¥m b u™ 'b¼šd,@o-çppp+AutoTeamBalance+ SD† , Saving Database after Cleanup.Pb¥dº–d,@¨{d vd<d¥d c pv» ËfÑ]D®?,@«Ã?“$,@ntÿÿÿÿt%à–t, PD®?,@­?“’]t,@?“$,@ ‚zP zPtPàoPÖ°onnotP¥t7´štÿÿÿÿš-çppppp[AutoTeamBalance] SD† , Warning: Despite St attempts, could not find a record to replace, so replacing a random one.tD®?,@«Ã?“$,@<t;pt d qwÕ Ýj¯~.q €e ^Þ þl5_w^ ¬©_®^ €? _€g u-é Ão—uVuްÃoƒ:Ž-çpppppp+AutoTeamBalance+ SD† , NumFromDateString() u -> ULuLu €i r.ô èq aVr#za`“Ja%,¹a“Ja,,&b“Ja,,&g®®?b¬« ¶C?a?, « ¶C?`cJa,,dJa, ,eJa, ,g®®®g?‘c,?‘‘d,,<?‘‘‘e,,<,<g `€k = x_–N¶-çppppppp[AutoTeamBalance] SD† , UpdateStatsAtEndOfGame() not updating stats since CountHumanPlayers S < SN.-}%AutoTeamBalance cannot update stats with fewer than 4 players. Ž-çppppp[AutoTeamBalance] SD† , UpdateStatsAtEndOfGame() Updating stats for S players.È-}AutoTeamBalance is updating player stats.n-Lçppp.AutoTeamBalance. SD† , LogEndStats: Team Name IP Ping PktLoss Rank Hours Last Score Frags Deaths Items Spree Secret Time†Àw*©‚a/! v{©‚—vÿÿÿÿ-Lçppppppppppppppppppppppppppppppppp.AutoTeamBalance. SD† , LogEndStats: R“š   S“à R“Ç Uv Uv  v  U“¨ S³ U“¹ SÊ SÈ SÉ U¯†?“£œ‚Tpp on ~pV†‘.Pb]-çppp+AutoTeamBalance+ SD† , UpdateStatsAtEndOfGame() done m o x4 €F(†‘§-®*Q.’†‘@%Á–@Q«·„ru*.±@Qµ¬u¬u@Qµ¥@H@%>–@Q«4‚›uÎ@.´u¬@Qµ¬u*>¥@Èu @€™Q„n syF ~ƒDpx‚wp*)š:s“špÎ p€q tK ª„UM-N¬¯†?`¯†?t“£S€? €s qzV I†ør%h%N†ÙwN*‚‚wNqNa/! N»³«NN“¨«qq“¨¥rÂ¥hNNœ"¬«ÈB?h?’hr N€u 0$h p‰8šx%(«0“¨0+hšx&(«?0³0+¹šx,(¬«0®?0³0“¨@+Ùšx,(z0++™x,(¬«0®?,0³0“¨€@G°(?À½ðÿ(?}ÿ„°(?Q±(?}æ-çpppppp+AutoTeamBalance+ SD† , Adjusting 0's extreme score U((ö(?Q?}( (€w {‰ 7:½šÿÿÿÿ·-çpppppp[AutoTeamBalance] SD† , UpdateStatsForPlayer() FAILED to find a record for  ÿÿÿÿtD¯†?`vD¯†?“£#—vtvtÞ–v,<Ø-çppppppp+AutoTeamBalance+ SD† , UpdateStatsForPlayer(V) Not updating this player since his timeInGame Sv < 60s.S¬¬?vpBpBb$ò‚‚‚.—†‘-¤ ›|% —v,´ yå-çppppppp+AutoTeamBalance+ SD† , UpdateStatsForPlayer() giving bonus S| to .¸b?|L›x,-Mb/bLb¬¬«b ApB¯†?`R w±RMRMs®RS6u-çpppppppppppppppp+AutoTeamBalance+ SD† , UpdateStatsForPlayer(p) [S]  avg_score = ( (U * UR) + Ub*US) / Us¬®«R«bSs¸ S  ± ®S?±®6?,¸-y`-~pppp has earned SD¯6 cookies!µ-\ ppYou earned SD¯6 cookies this game.y,|-~pppp has gained SD¯6 points of strength!|-\ ppYour strength increased by SD¯6 points this game.y,  ±6®?,K-yõ-~pppp has lost SD¯6 cookies.H-\ ppYou lost SD¯6 cookies this game.y, ©-~pppp has lost SD¯6 points of strength. -\ ppYour strength decreased by SD¯6 points this game.y, €bt6y ORÑ c¢4²†®V?, w%RcO†ÓwO*¼‚Oa/!„O O¸R$O¸c#O¥wOOœPøšw%RF¶6Bc?BR¬R?wc¬c?wV† ~ u/é ¼¦7R °R@R@¬«uSR € B Sù Ä©%®«cO«?P¯€?O €A i|ý $«@,„zpiNone zi0.0.0.0€i~i: €@C xT (­‹T%ƒ'j~xfe™j%Tg€xjxx’j}f¥T€Tgx¥TƒT T€E H ! Ÿ®ÍhppppS†Ü/S†Ø0,/S†Ù0,ippppS†Ú0,:S†×0,:S†Ö0,pph-i h€@G k' Ó¯.&–}kwkpvkk €@I x}, 6°š~xy% €K zV0 ©°|êz}jj €M {3 þ°—~{|ÿÿÿÿ €O }6 M±0}~ €@Q k09 ¢±k’~kl}l €@S UW= ²ny~Uu'šyÿÿÿÿUf›yÿÿÿÿUU’y}uy~Uu'U y€@U ~H ²>@ €@W v>L (³9w~vA*šwÿÿÿÿv7€vw w€@Y xXU ó=yUxB.šyÿÿÿÿx;€xy y€@[ CY^ z´@l~CmD1šlÿÿÿÿl>’ml l€] nUh jµ€z~nm*šzÿÿÿÿÿÿÿÿ~zUn’z}mmhšzÿÿÿÿz~’’z}mz z€_ o?y Ö¶„no%|–o}oVíoo&^‚™V,A ˜V,ZV’V, npnìV¥on n€@a {V‹ ˸ñ{-p(|%á–|}{Wí{|&«„„‚™Wí0 ˜Wí9 šWí.‚šWí- -p{p{ìW-p'×ׂ-p-Ep{|{¥|p{ {€@c FZ  »ºFGH €@e X¥ W»¹rP%±–P}XzXP}qqXpp€XPqX’P}qP’“P}q}q§rprXP&¥Pr r€@g Y[³ ­¼hs~YrM™s%|€YsYY’s}r`|YY| s€@@mA@m@@nÿÿÿÿz@nÿÿÿÿt@nÿÿÿÿu@`ûÿÿÿv@nÿÿÿÿ@nþÿÿÿ]@nÿÿÿÿx@md@nþÿÿÿd@nþÿÿÿh@_ýÿÿÿk@nþÿÿÿ`@nþÿÿÿ_@nþÿÿÿ^@`ýÿÿÿf@nöÿÿÿ\@`üÿÿÿx@nÿÿÿÿy@nÿÿÿÿ{@nþÿÿÿj@nöÿÿÿe@me@nèÿÿÿM@\÷ÿÿÿL@^ùÿÿÿe@`üÿÿÿg@`ýÿÿÿs@nöÿÿÿj@nþÿÿÿc@gêÿÿÿ@bêÿÿÿn@nÿÿÿÿw@]÷ÿÿÿc@^éÿÿÿi@nþÿÿÿn@`ëÿÿÿx@`ìÿÿÿw@_÷ÿÿÿ`@`âÿÿÿK@`ìÿÿÿw@]îÿÿÿN@_çÿÿÿ`@^îÿÿÿU@^Þÿÿÿ^@]çÿÿÿP@nþÿÿÿa@^üÿÿÿN@]÷ÿÿÿ@@]üÿÿÿG@`÷ÿÿÿa@`îÿÿÿK@aêÿÿÿf@d÷ÿÿÿ_@^ìÿÿÿt@_÷ÿÿÿA@nöÿÿÿ4@^îÿÿÿk@]îÿÿÿO@nþÿÿÿb@hêÿÿÿ}@cûÿÿÿO@hûÿÿÿD@nÿÿÿÿ@hûÿÿÿB@]÷ÿÿÿQ@hëÿÿÿA@hëÿÿÿH@hëÿÿÿ&@\÷ÿÿÿb@]üÿÿÿi@]üÿÿÿF@]üÿÿÿH@hëÿÿÿL@hëÿÿÿN@hëÿÿÿM@]çÿÿÿX@nöÿÿÿ@nþÿÿÿk@hêÿÿÿ@hêÿÿÿ~@\ýÿÿÿr@hûÿÿÿC@^ìÿÿÿ^@]ýÿÿÿq@]ýÿÿÿp@]ýÿÿÿm@]ýÿÿÿn@]ýÿÿÿo@^÷ÿÿÿe@]ýÿÿÿl@hùÿÿÿP@^üÿÿÿ[@^ìÿÿÿu@hëÿÿÿ[@mèÿÿÿ[@dçÿÿÿ@mèÿÿÿ|AoÿÿÿSAoŸÿÿÿY@]éÿÿÿh@]éÿÿÿ~@]Æÿÿÿc@]éÿÿÿf@dîÿÿÿg‹+} U¡Ž+B a¡ˆ+F n¡‹+W z¡Ž+XF¢AO T¢AE `¢‹=W m¢ˆŒQ y¢‹Ab E£AP Q£‹AQ ]£Ž=Bj£‹AS x£ˆCF D¤Ž XQ¤ˆŽv _¤Al l¤ˆCW y¤ˆ¹F F¥AV S¥ŽUX `¥ˆ’F m¥Ž­Xy¥ˆd G¦‹+ T¦ˆCd a¦޹Xn¦ˆ|F |¦ˆ‹F I§ˆ‹G U§Ž5Xb§ˆU} p§ˆ‰Q }§‹@V I¨ˆAI V¨Ž3Xc¨‹5S q¨ˆŒF }¨·` J©‹AS V©‹'r c©ˆ+~ p©ˆ5R |©ŽCXHªˆF VªމXbªŽ·Xpªˆa ~ªŽ#XK«ˆAI Y«ŽPv e«ŽPwr«¹h @¬ŽKL¬ŽCBZ¬ˆJh h¬ŽNXu¬ŽS@C­ˆvQ Q­ŽxX]­‹…z k­ˆ‰W x­ˆ®F E®•BjzR®ˆA@ |¨ŽR I©‹p V©ŽXc©Ž+Eq©Ž4X©ˆ5P Mªˆ5Q ZªŽJXgªŽfXuªˆ‰F C«ˆžQ O«޵X[«޾Xi«ˆçF w«ŸAHC¬AZ Q¬ŽX^¬ŽCl¬Ž!Xz¬‹#v H­‹&D U­+B b­ŽCXo­ˆCc }­ˆJF I®Nn V®ŽpXc®ŽzX q®‹ŒK ~®‹ŒL K¯‹«z X¯¹a d¯A[ q¯ˆA^ ~¯ˆc J°ŽVV°ŽXd°ˆb r°Ž&]~°ˆ3M L±Ž7X X±ˆCY e±ˆCZ r±Hd ±Pd L²ŽYX Y²ŽdX f²ˆ“v s²ˆ™F ²ˆ¤Q L³Ž®f Y³ˆ¹r f³A^ s³ˆAu @´ŸAGM´‹AN [´AR g´ˆ'G s´‹+z @µŽ+F LµŽ-IYµŽ-JgµŸ4O uµˆ5j B¶ˆ5k O¶‹He [¶ˆMP h¶ˆMQ u¶ŽNqB·ŽSAP·ŸUO^·ŽXXl·ŽtXz·‹|} H¸Ž…] T¸ˆ•F a¸ˆ F m¸ˆ¤^ z¸Ž®]G¹¹b U¹¹d b¹ˆÅF o¹‹ÕD |¹ˆáI IºˆãI Uº‹çL bº‹éz oºAs |ºAh I»A V»ˆA~ c»ˆAT p»AW }»ˆAX J¼ŸAKW¼l e¼‹W r¼ˆ F ¼ˆ#F K½ˆ'F X½Ž.s e½Ž@Br½‹AV @¾ˆC] M¾Mi Z¾MZ g¾ˆNj s¾ˆNk @¿Nm M¿ŽP|Z¿ŽP}h¿ŽvXv¿ˆ‘F DÀˆ¤G QÀ‹©z ^Àˆ­F kÀˆ¾W wÀ‹Åz CÁˆÕF PÁˆßv \Á‹ãl iÁˆãF vÁAm CÂA~ PÂAo ]ÂAx jÂAy wˆA| DËAL QÃA] ]ÃŽA]iÈ` wÃm CÄT PÄŽB]Ä‹W kÄŽ BxÄŽ#BFň#L TÅ‹&m aÅ‹&y nÅ‹'| zÅŽ+AFƈ1I TÆŽ?Baƈ@} oƈAF {ƈC[ GLjCC SÇ‹JD `ÇNs lLjNu yÇPj EÈPk RÈPs _ÈP~ lÈPT yÈ‹SW FɈXC SɈ…F _Ɉ…I lÉŒO yɈ™G EʈžF RÊ«\ _ʈµ_ lÊ‹Ãi xʈÅl EË‹ÉD QˈÝp ^Ë‹ßo jË‹ál wˈáF DÌãt QÌ‹çu ]Ì‹çl j̈éF wÌ‹Ar DÍAf QÍAg ^͈Av k͈Aw xÍAk EÎAA RΈA ^ΈA@ kΈAB xÎAU EψAY RÏŸAN_ψb mÏŽV zψd GÐŽfTÐŽX bÐj oÐk |ÐŽJIÑŽBWÑŽJeÑ‹ q sÑ‹!r Ñ#t KÒ#u XÒ‹+@ eÒ½+DqÒŽ-B@ÓŽ.XNÓ3N \Ó‹?V hÓˆCZ tÓˆC\ AÔˆC^ MÔC\ ZÔˆJg fÔNi rÔNr ÔNT LÕNt YÕP fÕPt sÕPu @Ö‹SD LÖŽ`CXÖˆdq fÖˆf sÖpN Ö‹…D K׌M X׌N e×™W r׈ Q ~× T KØ W X؈¢F dؤY pؤ` }Øޱf IÙ޵RVÙˆµ^ dÙ¹c qÙˆ¹f ~Ù‹ÕJ KÚ‹ÙD XÚˆÙF eÚ‹ÛD qÚˆÛF ~Úˆßq JÛ‹ãL VÛ‹éU cÛ‹Ap oÛ‹Aq |Û‹A\ I܈A] VÜAt cÜA_ pÜAc }܈Ad J݈Ae WÝAu dÝAi qÝAj ~ÝA} KÞAB XÞ‹Aq eÞAF rÞAs ÞˆAt LßAw Y߈A{ f߈AA sßAG @à‹AC MàAE ZàAJ gàA\ tàŸALAáŸAMOáŸAO]áŸAPká½Yyá‹y Hâ‹e Uân bâo oâŽ!B{â#s Iã‹#w Uã‹#x bãŽ%Xnã‹%E |ãŽ&BHä‹'z Vä‹'{ bä‹+C oä‹-G |ä‹-H Iåˆ-K Våˆ-L cåˆ1Z oåˆ1H |å‹@a Iæ‹A@ UæˆC{ bæˆC_ næC` {æCa HçˆHf UçKZ açNl nçNm {çNo HèNp UèPi aèPp nèPx zèPy GéPz TéP{ aéUa né‹VW zé‹WW Fê‹XW Rê‹YW _êˆYC lêŽbCyêˆj} Gëtr Të‹xf `ëŽxQ më‹€S z뎃XGì‹‹E Uì‹‹F bì‹‹G oì‹H |ì‹I Ií‹‹J V펌Xb펎Xp펑X ~툓Q KR Xî™S eî™T rX h LF Yï§S fl rm n Lðˆ«o Yðˆ«p fðˆ«q sð‹Åj @ñ‹Åk Mñ‹Çm Zñ‹Ç| gñ‹ÍJ sñ‹ÓD @ò‹ÓJ MòˆÝR Zò‹ßn gò‹áL tò‹ãs Aó‹çI Nó‹é_ [óAR hóAS uóAT BôAU OôAV \ôAW iôAX vôAY CõAZ PõA[ ]õAn jõAo wõA` DöˆAa QöˆAb ^öAx köAy xöAz E÷A{ R÷A| _÷AC l÷AD y÷Am EøAn RøAp _øAr løAv yøAz FùˆA} SùAH `ùAI mùAD zùAF Gú‹AK TúAM aúj nú°M {úŽdHû°M Vûj cû p pûŽ'X}û8C Kü‹8T Xü‹8U eü:C rü°:T ü°:U LýŽJBYý‹RW gýUB sýŸXO@þŸYONþˆdL \þˆh} iþˆl} vþˆn} Cÿˆp} Pÿˆz\ ]ÿŽ…XjÿއXxÿˆ¤F F€‹¥Z S€‹«[ `€ޱXm€޳X{€¿` I‹ÉX VˆÉF c‹ËD p‹ËJ }‹ÍD J‚‹ÏD W‚‹ÏJ d‚‹ÑD q‚‹ÑJ ~‚‹×D Kƒ‹×J Xƒ‹ÙJ eƒ‹ÛJ rƒ‹Ýn ƒ‹Ýo L„ãr Y„‹åL f„‹åu s„‹åI @…ÐAE4WöM…AJ dûŸAQqûŸARûŸASMüŒÀADb [üŒÄAAn}‡ŒÂABVkŽ_AˆF PŒÔACo\ŒàA[_K’ŒACxj¢ŒÅAH_b©g A°ŒA]|M°ŒA^m I²h v¼i C½ŒÌANqP½A AÁŒËALQMÁA ^ÄŒA5a jÄŒAD]KÏŒAE'hÓŒA'f OÔŒA_FuߌAp{àŒA`wkâŒAazbç‹'A \ëŒA6kië‹)A TíŒÆA&g]`íŽ+KGËŒAFoUËŒAbtDÑŒÍAMbxÒ/A ZÖŒAcLfÖŽ1X rÙŒAdpÙŒAGSoàŒAeGBæˆ5F IùŒA7pUùŒAH0Eû‹8A uûŒAT0Aü°:A qüŒAfP}üŒAIMþ=A L“ŒA(uX“ŒAgSM–ŒAhb`‹AA B¡ŒA8HO¡ŒA nW¼Dl E¾ˆDb R¾ŸDc _¾ŒAJ@l¾‹HA lÄŒAih yÄŒAj^´lA \µŒA)hµnA Q¶ŒA9T]¶pA q¸ŒA"r~¸rA p¼ŒAa|¼tA ]¿ŒA#^j¿vA HÆŒA RTÆŽxA fÈŒANBsÈŽzAuÊŒAOVCˈ|i YÑ‹|` eÑŒAPTrÑŒAQFV‹ÙA \ŽŒAXIiŽ‹ÛA rŒAYLˆÝA K‘ŒAU~X‘ˆßA V“ŒA?c“‹áA b•ŒAVYo•‹ãA H™ŒAZ,U™‹åA AšŒAcMš‹çA pœŒA[f}œ‹éA cž