diff --git a/.vscode/settings.json b/.vscode/settings.json index 4f25b97d..5b90e522 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,9 @@ { + // The bot is AutoHotkey v1 only. AHK++ also registers .ahk for v2 ("ahk2"), + // whose formatter wraps long lines in ways v1 cannot parse. + "files.associations": { + "*.ahk": "ahk" + }, // Format AutoHotkey files on save with Helper/Format-Ahk.ps1 (same rules as the pre-commit hook). // Needs the "Custom Local Formatters" extension (jkillian.custom-local-formatters). "customLocalFormatters.formatters": [ diff --git a/Accounts/_SendFriendRequest.ahk b/Accounts/_SendFriendRequest.ahk index adeec2ba..1afbab99 100644 --- a/Accounts/_SendFriendRequest.ahk +++ b/Accounts/_SendFriendRequest.ahk @@ -134,7 +134,7 @@ LogToFile(message, logFile := "") { logFile := LogsDir . "\Log_" . StrReplace(A_ScriptName, ".ahk") . ".txt" else logFile := LogsDir . "\" . logFile - FormatTime, readableTime, %A_Now%, MMMM dd, yyyy HH:mm:ss + FormatTime, readableTime, %A_Now%, yyyy-MM-dd HH:mm:ss try { FileAppend, % "[" readableTime "] " message "`n", %logFile% return !ErrorLevel @@ -161,6 +161,13 @@ LogTrace(message, logFile := "") { if (IsObject(botConfig) && botConfig.get("logLevel") = "trace") LogToFile("[trace] " . message, logFile) } +LogThrottled(level, key, message, intervalSec := 60, logFile := "") { + static lastLogged := {} + if (lastLogged.HasKey(key) && A_TickCount - lastLogged[key] < intervalSec * 1000) + return + lastLogged[key] := A_TickCount + LogToFile("[" . level . "] " . message, logFile) +} LogToDiscord(message, screenshotFile := "", ping := false, xmlFile := "", screenshotFile2 := "", altWebhookURL := "", altUserId := "") { } diff --git a/PTCGPB.ahk b/PTCGPB.ahk index 22aa434e..a225c6f9 100644 --- a/PTCGPB.ahk +++ b/PTCGPB.ahk @@ -319,6 +319,8 @@ NextStep: } KillADBProcesses() + CleanupLogs() + LogInfo("PTCGPB started | version=" . localVersion . " | AHK=" . A_AhkVersion . " | OS=" . A_OSVersion . (A_Is64bitOS ? " x64" : " x86") . " | DPI=" . A_ScreenDPI . " | language=" . botConfig.get("BotLanguage") . " | dir=" . A_ScriptDir) if (botConfig.get("automaticUpdateChecks")) CheckForUpdate(false) PTCGPB_CheckHelperPrograms() @@ -1831,6 +1833,7 @@ CancelToolsAndSystemSettings: return OpenSpecialEventExtractor: + LogUserAction("Open special event extractor") extractorPath := A_ScriptDir . "\SpecialEvents\SpecialEventExtractor.ahk" if (FileExist(extractorPath)) { Run, %extractorPath% @@ -1939,6 +1942,7 @@ CancelBackupSettings: return BackupNow: + LogUserAction("Backup now") Gui, BackupSettings:Submit, NoHide if (!ValidateAndSaveBackupSettings(true)) return @@ -2025,6 +2029,7 @@ ClearSpecialMissionHistory: { changed := AccountMetadata_ClearFlagEverywhere("X") changed := changed = "" ? 0 : changed + 0 + LogUserAction("Clear account flag", "flag=X | accounts changed=" . changed) MsgBox, 64, Clear Special Mission History Complete, % "Done`nAccounts changed: " . changed } @@ -2036,6 +2041,7 @@ ClearReceiveGiftHistory: { changed := AccountMetadata_ClearFlagEverywhere("R") changed := changed = "" ? 0 : changed + 0 + LogUserAction("Clear account flag", "flag=R | accounts changed=" . changed) MsgBox, 64, Clear Receive Gift History Complete, % "Done`nAccounts changed: " . changed } @@ -2062,6 +2068,7 @@ PullHistoryConfirmClearAll: Gui, ToolsAndSystemSelect:-Disabled changed := AccountMetadata_ClearPullHistoryEverywhere() changed := changed = "" ? 0 : changed + 0 + LogUserAction("Clear pull history", "accounts changed=" . changed) MsgBox, 64, Clear Pull History Complete, % "Done`nAccounts changed: " . changed return @@ -2071,6 +2078,7 @@ PullHistoryConfirmFlagOnly: Gui, ToolsAndSystemSelect:-Disabled changed := AccountMetadata_ClearFlagEverywhere("H") changed := changed = "" ? 0 : changed + 0 + LogUserAction("Clear account flag", "flag=H | accounts changed=" . changed) MsgBox, 64, Clear History Flag Complete, % "Done`nAccounts changed: " . changed return @@ -2089,6 +2097,7 @@ ClearForceInjectFlags: { changed := AccountMetadata_ClearFlagEverywhere("FI") changed := changed = "" ? 0 : changed + 0 + LogUserAction("Clear account flag", "flag=FI | accounts changed=" . changed) MsgBox, 64, Clear Force Inject Flags Complete, % "Done`nAccounts changed: " . changed } @@ -2278,6 +2287,7 @@ return ; =================== Logic - Balance XMLs Button Action =================== BalanceXMLs: + LogUserAction("Balance XMLs") Gui, Submit, NoHide if (!SaveAllSettings()) return @@ -2569,6 +2579,7 @@ BalanceXMLs_RunWithProgress(command) { ; =================== Logic - Launch All Mumu Button Action =================== LaunchAllMumu: + LogUserAction("Launch all MuMu") Gui, Submit, NoHide if (!SaveAllSettings()) return @@ -2593,6 +2604,7 @@ return ; =================== Logic - Arrange Button Action =================== ArrangeWindows: + LogUserAction("Arrange windows") Gui, Submit, NoHide if (!SaveAllSettings()) @@ -2708,6 +2720,7 @@ PTCGPB_RebuildTrayMenu() { OpenCockpit: global g_botStarted + LogUserAction("Open Cockpit") if (!g_botStarted) { MsgBox, 48,, Start the bot first, then open Cockpit from tray. return @@ -2721,6 +2734,7 @@ OpenCockpit: return OpenCardDatabase: + LogUserAction("Open card database") cardDbVbs := A_ScriptDir . "\Accounts\Cards\start_card_dashboard.vbs" cardDbHtml := A_ScriptDir . "\Accounts\Cards\card_database.html" @@ -2734,11 +2748,13 @@ OpenCardDatabase: return RunXMLDuplicateTool: + LogUserAction("Open XML duplicate finder") Tool := A_ScriptDir . "\Accounts\xml_duplicate_finder.ahk" RunWait, %Tool% Return RunXMLManagerTool: + LogUserAction("Open XML manager") Tool := A_ScriptDir . "\Accounts\xmlManager.ahk" RunWait, %Tool% Return @@ -2763,6 +2779,7 @@ InjectReqDlgGuiClose: return GuiClose: + LogUserAction("Close PTCGPB") Gui, Submit, NoHide if (!SaveAllSettings()) return @@ -2773,14 +2790,17 @@ GuiClose: return CheckForUpdates: + LogUserAction("Check for updates") CheckForUpdate(true) return ShowVersionManager: + LogUserAction("Open version manager") ShowVersionManager() return RunInstanceManager: + LogUserAction("Open instance manager") instanceManagerFile := A_ScriptDir . "\Scripts\Include\UpdateInstance.ahk" if (FileExist(instanceManagerFile)) Run, "%instanceManagerFile%" @@ -3198,7 +3218,10 @@ StartBot() { if (!ConfirmDiagnosticLogLevelForRun()) return + CleanupLogs() + LogUserAction("Start Bot", "mode=" . botConfig.get("deleteMethod") . " | instances=" . botConfig.get("Instances") . " | mains=" . (botConfig.get("runMain") ? botConfig.get("Mains") : 0) . " | groupReroll=" . (botConfig.get("groupRerollEnabled") ? 1 : 0) . " | logLevel=" . LogConfiguredLevel()) LogSettingsSnapshotForRun(botConfig.settingsFile) + LogSettingsChangesSinceLastStart(botConfig.settingsFile) ResetAccountLists() @@ -3738,6 +3761,7 @@ PopulateVersionManager(channel) { InstallSelectedRelease(release) { global localVersion + LogInfo("Install version requested | installed=" . localVersion . " | selected=" . release.version) comparison := VersionCompare(release.version, localVersion) if (comparison < 0) { warning := "You selected " . release.version . ", which is older than the installed version " . localVersion . "." diff --git a/Scripts/1.ahk b/Scripts/1.ahk index 0a291f8b..de77d14c 100644 --- a/Scripts/1.ahk +++ b/Scripts/1.ahk @@ -213,6 +213,11 @@ session.set("setSpeed", 3) ;always 1x/3x if(InStr(botConfig.get("deleteMethod"), "Inject") || botConfig.get("deleteMethod") = "Rename Account") session.set("injectMethod", true) +startPackList := "" +for idx, packName in session.get("packList") + startPackList .= (idx > 1 ? "," : "") . packName +LogInfo("Instance started | mode=" . botConfig.get("deleteMethod") . " | packs=" . startPackList . " | runsSoFar=" . session.get("rerolls") . " | stuckRecovery=" . DeadCheck . " | stopAfterRun=" . (session.get("stopToggle") ? 1 : 0) . " | groupReroll=" . (botConfig.get("groupRerollEnabled") ? 1 : 0)) + initializeAdbShell() RemoveOldFiles() adbGrantOverlayPermission() @@ -327,6 +332,8 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { while (StartCurrentTimeDiff > 0 && EndCurrentTimeDiff < 0) { FormatTime, formattedEndTime, %EndTime%, HH:mm:ss + if (!session.get("dateChange")) + LogInfo("Waiting for daily server reset until " . formattedEndTime) CreateStatusMessage("Waiting for daily server reset until " . formattedEndTime ,,,, false) session.set("dateChange", true) writeLastActivityEpoch(session.get("scriptName"), 4000) @@ -406,6 +413,8 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { } } + LogInfo("Run started | run=" . (session.get("rerolls") + 1) . " | pack=" . session.get("openPack") . (session.get("injectMethod") ? " | account=" . session.get("accountFileName") : "")) + waitForAppBootScreen() SetConfiguredGameSpeed() Delay(1) @@ -443,6 +452,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { if(botConfig.get("deleteMethod") = "Inject Wonderpick 96P+" && new_packcount < botConfig.get("injectWonderpickMinPacks")) { ; we now have a proper pack count and can evaluate if this is valid or if this is a waste of time + LogInfo("Skipping account below minimum pack count | account=" . session.get("accountFileName") . " | packs=" . new_packcount . " | minimum=" . botConfig.get("injectWonderpickMinPacks")) MarkAccountAsUsed() session.set("loadedAccount", false) restartGameInstance("New Run", false) @@ -451,6 +461,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { } } if(!session.get("injectMethod") || !session.get("loadedAccount")) { + LogInfo("Creating new account (tutorial)") DoTutorial() session.set("accountOpenPacks", 0) ;tutorial packs don't count } @@ -460,10 +471,11 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { if(botConfig.get("deleteMethod") = "Rename Account") { if (!AccountRename_CanRenameCurrentAccount()) { + LogInfo("Rename skipped (account age or rename cooldown) | account=" . session.get("accountFileName")) CreateStatusMessage("Rename Account`nSkipped (age/rename cooldown)",,,, false) if (session.get("injectMethod") && session.get("loadedAccount")) { MarkAccountAsClaimed() - LogDebug("Marked non-renameable account as claimed: " . session.get("accountFileName")) + LogInfo("Marked non-renameable account as claimed: " . session.get("accountFileName")) session.set("loadedAccount", false) } continue @@ -482,7 +494,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { } if (session.get("injectMethod") && session.get("loadedAccount")) { MarkAccountAsClaimed() - LogDebug("Marked renamed account as claimed (reusable): " . session.get("accountFileName")) + LogInfo("Marked renamed account as claimed (reusable): " . session.get("accountFileName")) session.set("loadedAccount", false) } continue @@ -492,9 +504,12 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { EnterGameFromWelcomeIfNeeded() ; 1.7.0 forces restart onto Welcome; tap before GoToMain ESC GoToMain() wonderPicked := DoWonderPick() + LogInfo("Wonder Pick done | picked=" . (wonderPicked ? 1 : 0)) } session.set("friendsAdded", AddFriends()) + if (botConfig.get("deleteMethod") = "Inject Wonderpick 96P+") + LogInfo("Friends added | result=" . session.get("friendsAdded")) if(botConfig.get("deleteMethod") = "Inject Wonderpick 96P+"){ if(session.get("friendsAdded") == false){ @@ -620,6 +635,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { Goto, EndOfRun if (checkShouldDoMissions()) { + LogInfo("Doing beginner missions") GoToMain() HomeAndMission() if(session.get("missionDoneList")["beginnerMissionsDone"]) @@ -684,6 +700,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { } if(botConfig.get("wonderpickForEventMissions") && (!IsObject(accountMeta) || AccountEligibility_FlagIsExpired(accountMeta, "W", 24))) { + LogInfo("Doing Wonder Pick for event missions") GoToMain() FindImageAndClick("WonderPick_WonderPickButtonInHome", 59, 429) ;click until in wonderpick Screen DoWonderPickOnly() @@ -705,7 +722,9 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { InitPackOpening(true) } session.set("openedGiftPack", false) + LogInfo("Receiving gifts") giftClaimed := ReceiveGiftExtended() + LogInfo("Receive gifts done | claimed=" . (giftClaimed ? 1 : 0)) if(giftClaimed) { HandleGiftedPacksAfterReceiveGift() } @@ -742,6 +761,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { botConfig.set("showcaseLikes", showcaseNumber, "Extra") botConfig.saveConfigToSettings("Extra") + LogInfo("Liking showcases | remaining today=" . showcaseNumber) FindImageAndClick("Common_ActivatedSocialInMainMenu", 143, 518, , 500) showcaseLikes() } @@ -776,7 +796,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { CreateStatusMessage(generateStatusText(), "AvgRuns", 0, 605, false, true) ; Log to file - LogInfo("Packs: " . session.get("packsThisRun") . " | Total time: " . session.get("mminutes") . "m " . session.get("sseconds") . "s | Avg: " . session.get("aminutes") . "m " . session.get("aseconds") . "s | Runs: " . session.get("rerolls")) + LogInfo("Run finished | Packs: " . session.get("packsThisRun") . " | Total time: " . session.get("mminutes") . "m " . session.get("sseconds") . "s | Avg: " . session.get("aminutes") . "m " . session.get("aseconds") . "s | Runs: " . session.get("rerolls")) SendMetadataToPTCGPB(session.get("packsThisRun")) @@ -784,6 +804,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { if (botConfig.get("deleteMethod") = "Inject 13P+" && session.get("accountOpenPacks") >= session.get("maxAccountPackNum")) { if (session.get("injectMethod") && session.get("loadedAccount")) { if (!session.get("keepAccount") || session.get("s4tFoundTradeable")) { + LogInfo("Account reached max pack count; marking as used: " . session.get("accountFileName") . " | packs=" . session.get("accountOpenPacks")) MarkAccountAsUsed() } session.set("loadedAccount", false) @@ -796,13 +817,13 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { if (!session.get("keepAccount") || session.get("s4tFoundTradeable")) { if ((botConfig.get("deleteMethod") = "Inject Rewards" || botConfig.get("deleteMethod") = "Rename Account") && !session.get("s4tFoundTradeable")) { MarkAccountAsClaimed() ; No 24h lock — account stays available for pack-opening - LogDebug("Marked injected account as claimed: " . session.get("accountFileName")) + LogInfo("Marked injected account as claimed: " . session.get("accountFileName")) } else { MarkAccountAsUsed() ; Remove account from queue - LogDebug("Marked injected account as used: " . session.get("accountFileName")) + LogInfo("Marked injected account as used: " . session.get("accountFileName")) } } else { - LogDebug("Keeping injected account: " . session.get("accountFileName")) + LogInfo("Keeping injected account: " . session.get("accountFileName")) } ; Reset loadedAccount so it will be loaded fresh next iteration @@ -855,6 +876,7 @@ if(DeadCheck = 1 && botConfig.get("deleteMethod") != "Create Bots (13P)") { } EnsureAccountLanguageMetadata() + LogInfo("Saved account: " . session.get("accountFileName") . " | packs=" . session.get("accountOpenPacks")) if (botConfig.get("deleteMethod") = "Create Bots (13P)" && IsFunc("EnsureAccountFriendInfo")) EnsureAccountFriendInfo("Create Bots (13P)", false, true) @@ -1138,6 +1160,7 @@ FindImageAndClick(needleName := "DEFAULT", clickx := 0, clicky := 0, searchVaria vRet := Gdip_ImageSearch_wbb(pBitmap, pNeedle, vPosXY, 92, 299, 115, 317, 0) if(vRet = 1) { session.set("cantOpenMorePacks", 1) + LogThrottled("info", "cantOpenMorePacks", "Not enough pack points or hourglasses to open more packs | pack=" . session.get("openPack")) return 0 } } @@ -1324,6 +1347,7 @@ ControlClick(X, Y) { ; Pause Script PauseScript: + LogUserAction("Pause") CreateStatusMessage("Pausing...",,,, false) g_scriptPaused := true Pause, On, 1 @@ -1331,6 +1355,7 @@ return ; Resume Script ResumeScript: + LogUserAction("Resume") CreateStatusMessage("Resuming...",,,, false) session.set("StartSkipTime", A_TickCount) ;reset stuck timers session.set("failSafe", A_TickCount) @@ -1339,6 +1364,7 @@ ResumeScript: return TogglePauseScript: + LogUserAction(g_scriptPaused ? "Resume" : "Pause") if (g_scriptPaused) { CreateStatusMessage("Resuming...",,,, false) session.set("StartSkipTime", A_TickCount) ;reset stuck timers @@ -1354,14 +1380,17 @@ return ; Stop Script StopScript: + LogUserAction("Stop button") ToggleStop() return DevMode: + LogUserAction("Dev mode toggled") ToggleDevMode() return ReloadScript: + LogUserAction("Reload") CleanupBeforeExit() SafeReload("Toolbar reload") return @@ -1372,6 +1401,7 @@ return ; === Single instance stop handlers (GUI button) === StopImmediatelySingle: + LogUserAction("Stop this instance immediately") targetHwnd := session.get("RememberStopPreferenceSingleHwnd") GuiControlGet, RememberStopPreferenceSingle, , %targetHwnd% if (RememberStopPreferenceSingle) { @@ -1384,6 +1414,7 @@ StopImmediatelySingle: return StopWaitEndSingle: + LogUserAction("Stop this instance after run") Gui, StopConfirm:Submit, NoHide GuiControlGet, RememberStopPreferenceSingle, , ui_RememberStopPreferenceSingle if (RememberStopPreferenceSingle) { @@ -1402,6 +1433,7 @@ return ; === All instances stop handlers (Shift+F7 from instance 1) === StopImmediatelyAll: + LogUserAction("Stop all instances immediately") targetHwnd := session.get("RememberStopPreferenceHwnd") GuiControlGet, RememberStopPreference, , %targetHwnd% if (RememberStopPreference) { @@ -1413,6 +1445,7 @@ StopImmediatelyAll: return StopWaitEndAll: + LogUserAction("Stop all instances after run") targetHwnd := session.get("RememberStopPreferenceHwnd") GuiControlGet, RememberStopPreference, , %targetHwnd% if (RememberStopPreference) { @@ -1427,6 +1460,7 @@ StopWaitEndAll: return StopAndKillMuMuAll: + LogUserAction("Stop all instances and close MuMu") GuiControlGet, RememberStopPreference, , ui_RememberStopPreference Gui, StopConfirmAll:Submit, NoHide if (RememberStopPreference) { diff --git a/Scripts/Include/BotMissions.ahk b/Scripts/Include/BotMissions.ahk index 4c7e7feb..2e0f9c32 100644 --- a/Scripts/Include/BotMissions.ahk +++ b/Scripts/Include/BotMissions.ahk @@ -100,6 +100,8 @@ ClaimAllMissionRewards(claimDaily := false, claimSpecial := false, accountMeta : if (!claimDaily && !claimSpecial) return {daily: false, special: false, eliteDeckRestart: false} + LogInfo("Claiming mission rewards | daily=" . (claimDaily ? 1 : 0) . " | special=" . (claimSpecial ? 1 : 0)) + method := botConfig.get("deleteMethod") ; --- Special-mission eligibility & advance --- diff --git a/Scripts/Include/BotPackEvaluation.ahk b/Scripts/Include/BotPackEvaluation.ahk index b3409708..bdffd46f 100644 --- a/Scripts/Include/BotPackEvaluation.ahk +++ b/Scripts/Include/BotPackEvaluation.ahk @@ -103,6 +103,7 @@ PullPackOpeningResultLog(failedDir, uniquePrefix) { ReportPackRecognitionFailure(reason := "Card Recognition Failed, use fallback mechanism") { global session, botConfig + LogWarn("Card recognition failed | pack=" . session.get("openPack") . " | reason=" . reason) root := getScriptBaseFolder() failedDir := root . "\Logs\failed" uniquePrefix := A_Now . "_" . session.get("scriptName") . "_pack" . session.get("packsInPool") @@ -282,6 +283,8 @@ UpdatePackCountAfterOpening(defaultOpenedPacks := 1) { session.set("packsInPool", session.get("packsInPool") + openedPackCount) session.set("packsThisRun", session.get("packsThisRun") + openedPackCount) + LogInfo("Pack opened | pack=" . session.get("openPack") . " | opened=" . openedPackCount . " | accountPacks=" . session.get("accountOpenPacks") . " | thisRun=" . session.get("packsThisRun")) + return openedPackCount } diff --git a/Scripts/Include/BotPackOpening.ahk b/Scripts/Include/BotPackOpening.ahk index cfc3aa29..39231e2f 100644 --- a/Scripts/Include/BotPackOpening.ahk +++ b/Scripts/Include/BotPackOpening.ahk @@ -386,6 +386,7 @@ SelectPack(HG := false) { break }else if(FindOrLoseImage("Pack_NotEnoughItemsForOpenPack", 0)) { session.set("cantOpenMorePacks", 1) + LogThrottled("info", "cantOpenMorePacks", "Not enough pack points or hourglasses to open more packs | pack=" . session.get("openPack")) } if(session.get("cantOpenMorePacks")) return @@ -426,6 +427,7 @@ SelectPack(HG := false) { break } else if(FindOrLoseImage("Pack_NotEnoughItemsForOpenPack", 0)) { session.set("cantOpenMorePacks", 1) + LogThrottled("info", "cantOpenMorePacks", "Not enough pack points or hourglasses to open more packs | pack=" . session.get("openPack")) } else if(FindOrLoseImage("Pack_HourglassImageAfterOpenPackClick", 0, 1) || FindOrLoseImage("Pack_HourglassAndPokeGoldImageAfterOpenPackClick", 0, 1)) { adbClick_wbb(205, 458) ; Handle unexpected HG pack confirmation } else if(FindOrLoseImage("Common_AlertForAppCrachDuringOpenPack", 0)) { @@ -484,6 +486,7 @@ PackOpening(tenPackOpening := false) { break ;wait for pack to be ready to Trace and click skip } else if(FindOrLoseImage("Pack_NotEnoughItemsForOpenPack", 0)) { session.set("cantOpenMorePacks", 1) + LogThrottled("info", "cantOpenMorePacks", "Not enough pack points or hourglasses to open more packs | pack=" . session.get("openPack")) } else if(FindOrLoseImage("Pack_HourglassImageAfterOpenPackClick", 0, 1) || FindOrLoseImage("Pack_HourglassAndPokeGoldImageAfterOpenPackClick", 0, 1)) { adbClick_wbb(205, 453) ; handle unexpected no packs available } else if(FindOrLoseImage("Pack_GetItemDialogAfterOpenPack", 0)){ @@ -641,6 +644,7 @@ HourglassOpening(HG := false, NEIRestart := true, tenPackOpening := false) { break }else if(FindOrLoseImage("Pack_NotEnoughItemsForOpenPack", 0)) { session.set("cantOpenMorePacks", 1) + LogThrottled("info", "cantOpenMorePacks", "Not enough pack points or hourglasses to open more packs | pack=" . session.get("openPack")) } if(session.get("cantOpenMorePacks")) return diff --git a/Scripts/Include/BotStopSignals.ahk b/Scripts/Include/BotStopSignals.ahk index c79db5c8..aa32fe0f 100644 --- a/Scripts/Include/BotStopSignals.ahk +++ b/Scripts/Include/BotStopSignals.ahk @@ -38,6 +38,7 @@ OnMonitorWake(wParam, lParam, msg, hwnd) { ; Message handler for "stop after run" signal from instance 1 OnStopAfterRunMessage(wParam, lParam, msg, hwnd) { global session + LogInfo("Received stop-after-run signal from instance 1") session.set("stopToggle", true) CreateStatusMessage("Stopping script at the end of the run...",,,, false) return 0 diff --git a/Scripts/Include/BotToggles.ahk b/Scripts/Include/BotToggles.ahk index b5960a51..724fbbc4 100644 --- a/Scripts/Include/BotToggles.ahk +++ b/Scripts/Include/BotToggles.ahk @@ -9,6 +9,7 @@ ToggleStop() { savedStopPreferenceSingle := (botConfig.get("stopPreferenceSingle") = "") ? "none" : botConfig.get("stopPreferenceSingle") if (savedStopPreferenceSingle != "none" && savedStopPreferenceSingle != "ERROR" && savedStopPreferenceSingle != "") { + LogUserAction("Stop this instance", "saved preference=" . savedStopPreferenceSingle) ; Execute the saved preference directly without showing popup if (savedStopPreferenceSingle = "immediate") { CleanupBeforeExit() @@ -48,6 +49,7 @@ ToggleStopAll() { savedStopPreference := (botConfig.get("stopPreference") = "") ? "none" : botConfig.get("stopPreference") if (savedStopPreference != "none" && savedStopPreference != "ERROR" && savedStopPreference != "") { + LogUserAction("Stop all instances", "saved preference=" . savedStopPreference) ; Execute the saved preference directly without showing popup if (savedStopPreference = "immediate") { StopAllInstances() @@ -90,10 +92,12 @@ ToggleTestScript() { global session if(!session.get("GPTest")) { + LogUserAction("Enter GP Test mode") CreateStatusMessage("In GP Test Mode",,,, false) session.set("GPTest", true) } else { + LogUserAction("Exit GP Test mode") CreateStatusMessage("Exiting GP Test Mode",,,, false) session.set("GPTest", false) } diff --git a/Scripts/Include/CardDetection.ahk b/Scripts/Include/CardDetection.ahk index d7a05088..72c37a4d 100644 --- a/Scripts/Include/CardDetection.ahk +++ b/Scripts/Include/CardDetection.ahk @@ -363,6 +363,7 @@ FoundStars(star, cards := "") { logMessage .= "File name: " . accountFile . "\nBacking up to the Accounts\\SpecificCards folder and continuing..." LogToDiscord(logMessage, screenShot, true, (DiscordShouldSendAccountXml() ? accountFullPath : ""), fcScreenshot) LogInfo(StrReplace(logMessage, "\n", " "), "GPlog.txt") + LogInfo(statusPrefix . " | pack=" . session.get("openPack") . " | packsInPool=" . session.get("packsInPool") . " | saved=" . accountFile) } ;------------------------------------------------------------------------------- @@ -457,6 +458,7 @@ GodPackFound(validity, cards := "", alreadyAtHome := false, preCapturedScreensho logMessage .= invalid . " God Pack found in instance: " . session.get("scriptName") . "\nFile name: " . accountFile . "\nBacking up to the Accounts\\GodPacks folder and continuing..." LogInfo(StrReplace(logMessage, "\n", " "), "GPlog.txt") + LogInfo(validity . " God Pack | stars=" . starCount . "/5 | pack=" . packDisplayName . " | packsInPool=" . session.get("packsInPool") . " | saved=" . accountFile) if (validity = "Valid") { LogToDiscord(logMessage, screenShot, true, (DiscordShouldSendAccountXml() ? accountFullPath : ""), fcScreenshot) @@ -698,6 +700,7 @@ FoundTradeable(found3Dmnd := 0, found4Dmnd := 0, found1Star := 0, foundGimmighou logMessage := statusMessage . " in instance: " . session.get("scriptName") . " (" . session.get("packsInPool") . " packs, " . dictionaryData[botConfig.get("defaultBotLanguage")][session.get("openPack")] . ") Screenshot file: " . screenShotFileName LogInfo(logMessage, "S4T.txt") + LogInfo(logMessage) ; Legacy path has only count vars (no card IDs/rarity array). Reuse the shared ; builder via count-only fallback: Discord format stays consistent with the @@ -858,6 +861,7 @@ FoundTradeableNew(foundCards, pack := "", cards := "", rarity := "", isTenPackOp logMessage := statusMessage . " in instance: " . scriptName . " (" . packsInPool . " packs, " . packName . ") Screenshot file: " . screenShotFileName LogInfo(logMessage, "S4T.txt") + LogInfo(logMessage) headerTitle := isTenPackOpening ? "10-Packs Opening results" : "Pack Opening results" CardDetection_SendS4TDiscord(headerTitle, Chr(0x1F4E6), packName, packDetailsMessage, wishlistMatches, accountFileName, screenShot, savedXmlPath) diff --git a/Scripts/Include/Error.ahk b/Scripts/Include/Error.ahk index 1457b4c3..af44b7ab 100644 --- a/Scripts/Include/Error.ahk +++ b/Scripts/Include/Error.ahk @@ -30,6 +30,7 @@ ErrorCheckInScreen(pBitmap, searchVariation := 20){ , needleObj.coords.endY , searchVariation) if (vRet = 1 && !interceptProc) { + LogThrottled("warn", "errorPopup_" . value, "Game error popup detected | popup=" . value) errorFuncList[value].Call() } } diff --git a/Scripts/Include/HourglassSpend.ahk b/Scripts/Include/HourglassSpend.ahk index ba35bc86..2c95f730 100644 --- a/Scripts/Include/HourglassSpend.ahk +++ b/Scripts/Include/HourglassSpend.ahk @@ -182,6 +182,8 @@ SpendAllHourglassInject13P() { SpendAllHourglass() { global botConfig, session + LogInfo("Spending hourglasses | packs=" . (botConfig.get("spendHourglassPackCount") > 0 ? botConfig.get("spendHourglassPackCount") : "all")) + if (botConfig.get("deleteMethod") = "Inject 13P+") { SpendAllHourglassInject13P() return @@ -191,22 +193,22 @@ SpendAllHourglass() { GoToMain() SelectPack("HGPack") - if(session.get("cantOpenMorePacks")) + if (session.get("cantOpenMorePacks")) return PackOpening() RecordHourglassPacksOpened() - if(session.get("cantOpenMorePacks") || !CanContinueHourglassSpendWonderpick()) + if (session.get("cantOpenMorePacks") || !CanContinueHourglassSpendWonderpick()) return while (CanContinueHourglassSpendWonderpick()) { - if(session.get("packMethod")) { + if (session.get("packMethod")) { session.set("friendsAdded", PackMethod_RenewFriends()) if (!PackMethod_ConsumeStayOnPackScreen()) { GoToMain() SelectPack("HGPack") } - if(session.get("cantOpenMorePacks")) + if (session.get("cantOpenMorePacks")) break PackOpening() RecordHourglassPacksOpened() @@ -215,7 +217,7 @@ SpendAllHourglass() { RecordHourglassPacksOpened() } - if(session.get("cantOpenMorePacks") || !CanContinueHourglassSpendWonderpick()) + if (session.get("cantOpenMorePacks") || !CanContinueHourglassSpendWonderpick()) break } } diff --git a/Scripts/Include/Logging.ahk b/Scripts/Include/Logging.ahk index a5a6bd0b..9aa85cdc 100644 --- a/Scripts/Include/Logging.ahk +++ b/Scripts/Include/Logging.ahk @@ -186,7 +186,7 @@ LogToFile(message, logFile := "") { } else logFile := LogsDir . "\" . logFile - FormatTime, readableTime, %A_Now%, MMMM dd, yyyy HH:mm:ss + readableTime := LogTimestamp() ; Keep the existing short retries for transient sharing violations, but do ; not hang the bot indefinitely when the destination is not writable. @@ -199,6 +199,148 @@ LogToFile(message, logFile := "") { return false } +; Locale-independent and sortable, so log retention can parse it back and +; entries from different files can be lined up when reading a bug report. +LogTimestamp() { + FormatTime, readableTime, %A_Now%, yyyy-MM-dd HH:mm:ss + return readableTime . "." . A_MSec +} + +; =================== Log retention =================== +; Deletes files in Logs\ that were not written within the retention window and +; drops entries older than the window from the files that remain. Run it only +; while no bot instance is writing (GUI startup / before Start launches them). +CleanupLogs(retentionDays := 7) { + if !InStr(FileExist(LogsDir), "D") + return + + cutoff := A_Now + cutoff += -retentionDays, Days + + deletedFiles := 0 + trimmedFiles := 0 + Loop, Files, %LogsDir%\*, FR + { + if (A_LoopFileName = ".gitkeep") + continue + if (A_LoopFileTimeModified < cutoff) { + FileDelete, %A_LoopFileFullPath% + if (!ErrorLevel) + deletedFiles++ + continue + } + ; Subfolders (e.g. Logs\failed) hold raw diagnostic dumps, not our log + ; format, so they are only aged out as whole files. + if (A_LoopFileDir != LogsDir) + continue + result := TrimLogFileBefore(A_LoopFileFullPath, cutoff) + if (result = "deleted") + deletedFiles++ + else if (result = "trimmed") + trimmedFiles++ + } + + ; Remove subfolders that are empty now (FileRemoveDir fails on non-empty ones). + Loop, Files, %LogsDir%\*, DR + FileRemoveDir, %A_LoopFileFullPath% + + if (deletedFiles || trimmedFiles) + LogToFile("[info] Log cleanup | kept last " . retentionDays . " days | deleted files=" . deletedFiles . " | trimmed files=" . trimmedFiles) +} + +; Removes every entry stamped before cutoff. Lines without a timestamp (multi-line +; entries) stay with the entry they follow. Files without any parseable old +; timestamp are left untouched. Returns "", "trimmed" or "deleted". +TrimLogFileBefore(filePath, cutoff) { + f := FileOpen(filePath, "r") + if (!IsObject(f)) + return "" + + bomLength := f.Pos + keepFrom := -1 + sawOldEntry := false + while (!f.AtEOF) { + linePos := f.Pos + stamp := ParseLogTimestamp(f.ReadLine()) + if (stamp = "") + continue + if (stamp >= cutoff) { + keepFrom := linePos + break + } + sawOldEntry := true + } + + if (!sawOldEntry) { + f.Close() + return "" + } + + if (keepFrom < 0) { + f.Close() + FileDelete, %filePath% + return ErrorLevel ? "" : "deleted" + } + + ; Copy raw bytes so the file encoding is preserved exactly. + tmpPath := filePath . ".tmp" + out := FileOpen(tmpPath, "w", "CP0") + if (!IsObject(out)) { + f.Close() + return "" + } + if (bomLength > 0) { + f.Pos := 0 + f.RawRead(bom, bomLength) + out.RawWrite(bom, bomLength) + } + f.Pos := keepFrom + VarSetCapacity(chunk, 1048576) + while (bytesRead := f.RawRead(chunk, 1048576)) + out.RawWrite(chunk, bytesRead) + out.Close() + f.Close() + + FileMove, %tmpPath%, %filePath%, 1 + if (ErrorLevel) { + FileDelete, %tmpPath% + return "" + } + return "trimmed" +} + +; Returns the entry time as YYYYMMDDHH24MISS, or "" when the line does not start +; with a timestamp. Understands the current "yyyy-MM-dd HH:mm:ss" stamps and the +; older locale-dependent "[MMMM dd, yyyy HH:mm:ss]" ones. +ParseLogTimestamp(line) { + if (RegExMatch(line, "^\[?(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})", m)) + return m1 . m2 . m3 . m4 . m5 . m6 + if (RegExMatch(line, "^\[(\S+) (\d{2}), (\d{4}) (\d{2}):(\d{2}):(\d{2})\]", m)) { + month := LogMonthNumber(m1) + if (month != "") + return m3 . month . m2 . m4 . m5 . m6 + } + return "" +} + +LogMonthNumber(monthName) { + static months := "" + if (!IsObject(months)) { + ; Legacy stamps used the user's locale; English covers logs copied from + ; another machine. + months := {} + englishMonths := ["January", "February", "March", "April", "May", "June", "July" + , "August", "September", "October", "November", "December"] + Loop, 12 { + month := Format("{:02}", A_Index) + months[englishMonths[A_Index]] := month + FormatTime, localName, 2000%month%01, MMMM + months[localName] := month + } + } + return months.HasKey(monthName) ? months[monthName] : "" +} + LogLevelValue(level) { level := Trim(level) StringLower, level, level @@ -282,33 +424,105 @@ LogSettingsSnapshotForRun(settingsFile := "") { sanitizedSettings := "" Loop, Parse, settingsContents, `n, `r + sanitizedSettings .= RedactSettingsLine(A_LoopField) . "`n" + + ; Bypass level filtering: every explicitly started run needs this diagnostic context. + LogToFile("[info] Run settings snapshot (URLs redacted):`n" . RTrim(sanitizedSettings, "`r`n")) + return true +} + +RedactSettingsLine(line) { + equalPos := InStr(line, "=") + if (equalPos > 0) { + key := Trim(SubStr(line, 1, equalPos - 1)) + value := SubStr(line, equalPos + 1) + + ; URL settings commonly contain credentials (for example Discord webhooks). + ; Keep the key in the diagnostic snapshot, but never write its value. + if (value != "" && (InStr(key, "url", false) || InStr(key, "webhook", false))) + value := "" + else + value := RegExReplace(value, "i)\b[a-z][a-z0-9+.-]*://\S+|\bwww\.\S+", "") + + return SubStr(line, 1, equalPos) . value + } + ; Also protect URLs that might appear in comments or non-key lines. + return RegExReplace(line, "i)\b[a-z][a-z0-9+.-]*://\S+|\bwww\.\S+", "") +} + +; Reads key=value lines (section headers ignored; keys are unique across +; sections) into an object with URL values redacted. Returns "" if unreadable. +ReadRedactedSettings(filePath) { + if (!FileExist(filePath)) + return "" + FileRead, contents, %filePath% + if (ErrorLevel) + return "" + + settings := {} + Loop, Parse, contents, `n, `r { - line := A_LoopField + line := RedactSettingsLine(A_LoopField) equalPos := InStr(line, "=") + if (equalPos <= 1 || SubStr(LTrim(line), 1, 1) = ";") + continue + settings[Trim(SubStr(line, 1, equalPos - 1))] := SubStr(line, equalPos + 1) + } + return settings +} - if (equalPos > 0) { - key := Trim(SubStr(line, 1, equalPos - 1)) - value := SubStr(line, equalPos + 1) - - ; URL settings commonly contain credentials (for example Discord webhooks). - ; Keep the key in the diagnostic snapshot, but never write its value. - if (value != "" && (InStr(key, "url", false) || InStr(key, "webhook", false))) - value := "" - else - value := RegExReplace(value, "i)\b[a-z][a-z0-9+.-]*://\S+|\bwww\.\S+", "") - - line := SubStr(line, 1, equalPos) . value - } else { - ; Also protect URLs that might appear in comments or non-key lines. - line := RegExReplace(line, "i)\b[a-z][a-z0-9+.-]*://\S+|\bwww\.\S+", "") - } +; Logs which settings differ from the ones used at the previous Start, so a bug +; report shows what the user changed before a problem appeared. The previous +; values are kept (redacted) in Logs\SettingsAtLastStart.txt. +LogSettingsChangesSinceLastStart(settingsFile) { + current := ReadRedactedSettings(settingsFile) + if (!IsObject(current)) + return - sanitizedSettings .= line . "`n" + previousFile := LogsDir . "\SettingsAtLastStart.txt" + previous := ReadRedactedSettings(previousFile) + if (IsObject(previous)) { + changes := "" + changeCount := 0 + for key, value in current { + if (previous.HasKey(key) && previous[key] == value) + continue + changes .= "`n " . key . ": " . (previous.HasKey(key) ? previous[key] : "") . " -> " . value + changeCount++ + } + for key, value in previous { + if (current.HasKey(key)) + continue + changes .= "`n " . key . ": " . value . " -> " + changeCount++ + } + if (changeCount) + LogToFile("[info] Settings changed since last start (" . changeCount . "):" . changes) + else + LogToFile("[info] Settings unchanged since last start") } - ; Bypass level filtering: every explicitly started run needs this diagnostic context. - LogToFile("[info] Run settings snapshot (URLs redacted):`n" . RTrim(sanitizedSettings, "`r`n")) - return true + stored := "" + for key, value in current + stored .= key . "=" . value . "`n" + FileDelete, %previousFile% + FileAppend, %stored%, %previousFile% +} + +; Logs at most once per intervalSec for the same key, so conditions that are +; detected on every poll (error popups, waits) do not flood the log. +LogThrottled(level, key, message, intervalSec := 60, logFile := "") { + static lastLogged := {} + if (lastLogged.HasKey(key) && A_TickCount - lastLogged[key] < intervalSec * 1000) + return + lastLogged[key] := A_TickCount + LogMessage(level, message, logFile) +} + +; High-level entries for things the user did (button, hotkey, menu). They use +; a fixed prefix so a bug report can be filtered down to them. +LogUserAction(action, details := "") { + LogInfo("User action | " . action . (details != "" ? " | " . details : "")) } GetActiveDiscordProfile() { diff --git a/Scripts/Include/Utils.ahk b/Scripts/Include/Utils.ahk index c2af7c63..28c6ce29 100644 --- a/Scripts/Include/Utils.ahk +++ b/Scripts/Include/Utils.ahk @@ -179,7 +179,7 @@ resetShowcaseLikesIfNewCycle() { SplitPath, showcaseLogPath,, showcaseLogDir if !FileExist(showcaseLogDir) FileCreateDir, %showcaseLogDir% - FormatTime, readableTime, %A_Now%, MMMM dd, yyyy HH:mm:ss + FormatTime, readableTime, %A_Now%, yyyy-MM-dd HH:mm:ss FileAppend, % "[" . readableTime . "] Showcase likes reset for daily cycle " . cycleKey . "`n", %showcaseLogPath% return true } @@ -561,6 +561,8 @@ LogReloadMessage(message) { if (IsFunc("LogToFile")) { logFn := Func("LogToFile") logFn.Call(message, "Reload.txt") + ; Also in the script's own log, so its timeline shows why it restarted. + logFn.Call("[info] " . message) return } @@ -1012,7 +1014,7 @@ AppendGPlog(message) { SplitPath, logPath,, logDir if !FileExist(logDir) FileCreateDir, %logDir% - FormatTime, readableTime, %A_Now%, MMMM dd, yyyy HH:mm:ss + FormatTime, readableTime, %A_Now%, yyyy-MM-dd HH:mm:ss Loop, { FileAppend, % "[" readableTime "] " message "`n", %logPath% if !ErrorLevel diff --git a/Scripts/Main.ahk b/Scripts/Main.ahk index 72ce6d06..dcdfa3a7 100644 --- a/Scripts/Main.ahk +++ b/Scripts/Main.ahk @@ -129,6 +129,7 @@ initializeAdbShell() RemoveOldFiles() CreateStatusMessage("Initializing bot...",,,, false) +LogInfo("Main started | groupReroll=" . (botConfig.get("groupRerollEnabled") ? 1 : 0) . " | autoGPTest=" . (session.get("autoUseGPTest") ? 1 : 0) . " | recovering=" . (session.get("isDead") ? 1 : 0)) if(session.get("isDead")){ closePTCGPApp() session.set("isDead", false) @@ -197,12 +198,14 @@ if(firstRun) { if(DismissExpiredFriendRequestInApprove()) { continue } else if(FindOrLoseImage("Friend_AcceptButtonInApproveSubmenu", 0)) { + LogThrottled("info", "acceptingFriends", "Accepting friend requests", 300) session.set("failSafe", A_TickCount) failSafeTime := 0 Loop { Delay(1) clickButton := FindOrLoseImage("Common_ColorChangeButton", 0, failSafeTime, 80) if(FindOrLoseImage("FriendLimit", 0, failSafeTime)) { + LogThrottled("warn", "friendLimit", "Friend limit reached; cannot accept more friend requests", 300) done := true break } else if(DismissExpiredFriendRequestInApprove()) { @@ -674,6 +677,7 @@ Screenshot(fileType := "Valid", subDir := "", ByRef fileName := "") { ; Pause Script PauseScript: + LogUserAction("Pause") CreateStatusMessage("Pausing...",,,, false) g_scriptPaused := true Pause, On, 1 @@ -681,6 +685,7 @@ return ; Resume Script ResumeScript: + LogUserAction("Resume") CreateStatusMessage("Resuming...",,,, false) g_scriptPaused := false Pause, Off @@ -689,6 +694,7 @@ ResumeScript: return TogglePauseScript: + LogUserAction(g_scriptPaused ? "Resume" : "Pause") if (g_scriptPaused) { CreateStatusMessage("Resuming...",,,, false) session.set("StartSkipTime", A_TickCount) ;reset stuck timers @@ -704,6 +710,7 @@ return ; Stop Script - Main.ahk always exits immediately (no "end of run" concept) StopScript: + LogUserAction("Stop button") if (!botConfig.get("groupRerollEnabled")) { CreateStatusMessage("Stopping script...",,,, false) ExitApp @@ -727,6 +734,7 @@ StopScript: return StopMainImmediately: + LogUserAction("Stop Main immediately") Gui, StopMain:Submit, NoHide GuiControlGet, RememberStopPreferenceMain, , ui_RememberStopPreferenceMain if (RememberStopPreferenceMain) { @@ -748,6 +756,7 @@ StopMainAfterGPTestButton: } StopMainAfterGPTest: + LogUserAction("Stop Main after GP Test") session.set("stopAfterGPTest", true) if (!session.get("GPTest")) { session.set("GPTest", true) @@ -765,6 +774,7 @@ StopMainGuiEscape: return RateLimitSleep: + LogUserAction("Rate limit: sleep") session.set("rateLimitAction", "sleep") Gui, RateLimit:Destroy return @@ -785,6 +795,7 @@ VipTrimModeChanged: return VipTrimStart: + LogUserAction("Trim VIP list") Gui, VipTrim:Submit, NoHide GuiControlGet, VipTrimTopValue, , ui_VipTrimTop @@ -823,10 +834,12 @@ VipTrimGuiEscape: return ImportCollectionScript: + LogUserAction("Import collection") ImportMainCollection(session.get("scriptName")) return ReloadScript: + LogUserAction("Reload") SafeReload("Main toolbar reload") return @@ -902,6 +915,7 @@ ToggleTestScript(triggerSource := "auto", selectedMode := "__unset__") { UpdateGPTestButtonLabel() session.set("triggerTestNeeded", true) session.set("testStartTime", A_TickCount) + LogInfo("Entering GP Test mode | trigger=" . triggerSource . " | unopenedPack=" . session.get("hasUnopenedPack")) CreateStatusMessage("In GP Test Mode",,,, false) session.set("StartSkipTime", A_TickCount) ;reset stuck timers session.set("failSafe", A_TickCount) @@ -920,6 +934,7 @@ ToggleTestScript(triggerSource := "auto", selectedMode := "__unset__") { session.set("firstRun", True) session.set("testStartTime", "") } + LogInfo("Exiting GP Test mode | trigger=" . triggerSource . " | duration=" . totalTestTime . "s") CreateStatusMessage("Exiting GP Test Mode",,,, false) } } @@ -1049,6 +1064,7 @@ GPTestScript() { session.set("triggerTestNeeded", false) session.set("rateLimitAction", "") session.set("vipListTrimApplied", false) + LogInfo("GP Test: favouriting VIP friends") FavoriteVipFriends() if (!session.get("GPTest") || session.get("rateLimitAction") = "sleep") return @@ -1067,7 +1083,9 @@ GPTestScript() { Sleep, 1000 } } + LogInfo("GP Test: removing non-VIP friends") RemoveNonVipFriends() + LogInfo("GP Test: done") } ; Returns true if a friend add/remove op can proceed (and increments the counter).