From 368d35c34c1062887e832ff3309d1954c5f97672 Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Mon, 22 Sep 2025 17:44:47 +0700 Subject: [PATCH 01/11] Refactor element handling and browser interaction Replaced direct XPath-based element retrieval with a new `GetElement` method, supporting both `By` selectors and `Func` delegates. Updated `Click` and `Input` methods to accept `IWebElement` directly, reducing redundancy and improving maintainability. Removed deprecated `WaitPageLoaded` and `WaitPageChanged` methods in favor of more generic `Wait` methods. Enhanced error handling with detailed messages for `WebDriverTimeoutException`. Updated the `Retry` class to include more descriptive timeout errors. Refactored all commands to use the new `GetElement` method and streamlined interaction logic. Removed redundant code and unused methods, improving readability and maintainability. Updated the `IChromeBrowser` interface to reflect these changes. Performed general code cleanup, including removing unused variables, redundant comments, and unnecessary method calls. --- .../Features/ClaimQuest/ClaimQuestCommand.cs | 16 +++- .../Features/ClaimQuest/ToQuestPageCommand.cs | 13 ++- .../CompleteImmediatelyCommand.cs | 22 ++--- .../DisableContextualHelpCommand.cs | 12 +-- .../ToOptionsPageCommand.cs | 6 +- MainCore/Commands/Features/LoginCommand.cs | 24 +++--- .../NpcResource/NpcResourceCommand.cs | 54 ++++-------- .../StartAdventure/ExploreAdventureCommand.cs | 9 +- .../StartAdventure/ToAdventurePageCommand.cs | 13 ++- .../StartActiveFarmListCommand.cs | 6 +- .../StartFarmList/StartAllFarmListCommand.cs | 6 +- .../Features/TrainTroop/TrainTroopCommand.cs | 8 +- .../UpgradeBuilding/GetBuildPlanCommand.cs | 23 ----- .../UpgradeBuilding/HandleUpgradeCommand.cs | 62 ++++++++------ .../UseHeroItem/ToHeroInventoryCommand.cs | 9 +- .../UseHeroItem/UseHeroItemCommand.cs | 24 +++--- .../Commands/Navigate/SwitchTabCommand.cs | 9 +- .../Commands/Navigate/SwitchVillageCommand.cs | 16 ++-- .../Navigate/ToBuildingByLocationCommand.cs | 17 ++-- MainCore/Commands/Navigate/ToDorfCommand.cs | 8 +- MainCore/Errors/Retry.cs | 2 + MainCore/Services/ChromeBrowser.cs | 83 +++++++++++-------- MainCore/Services/IChromeBrowser.cs | 13 ++- 23 files changed, 216 insertions(+), 239 deletions(-) diff --git a/MainCore/Commands/Features/ClaimQuest/ClaimQuestCommand.cs b/MainCore/Commands/Features/ClaimQuest/ClaimQuestCommand.cs index f0046470..605f6028 100644 --- a/MainCore/Commands/Features/ClaimQuest/ClaimQuestCommand.cs +++ b/MainCore/Commands/Features/ClaimQuest/ClaimQuestCommand.cs @@ -34,14 +34,22 @@ private static async ValueTask HandleAsync( quest = QuestParser.GetQuestCollectButton(browser.Html); if (quest is null) return Result.Ok(); - result = await browser.Click(By.XPath(quest.XPath), cancellationToken); + var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(quest.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; continue; } + else + { + var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(quest.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); - result = await browser.Click(By.XPath(quest.XPath), cancellationToken); - if (result.IsFailed) return result; - await delayService.DelayClick(cancellationToken); + result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; + await delayService.DelayClick(cancellationToken); + } } while (QuestParser.IsQuestClaimable(browser.Html)); diff --git a/MainCore/Commands/Features/ClaimQuest/ToQuestPageCommand.cs b/MainCore/Commands/Features/ClaimQuest/ToQuestPageCommand.cs index fb5c09b3..0de394dd 100644 --- a/MainCore/Commands/Features/ClaimQuest/ToQuestPageCommand.cs +++ b/MainCore/Commands/Features/ClaimQuest/ToQuestPageCommand.cs @@ -12,8 +12,11 @@ private static async ValueTask HandleAsync( IChromeBrowser browser, CancellationToken cancellationToken) { - var adventure = QuestParser.GetQuestMaster(browser.Html); - if (adventure is null) return Retry.ButtonNotFound("quest master"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => QuestParser.GetQuestMaster(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + + var result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; static bool TableShow(IWebDriver driver) { @@ -21,11 +24,7 @@ static bool TableShow(IWebDriver driver) doc.LoadHtml(driver.PageSource); return QuestParser.IsQuestPage(doc); } - - var result = await browser.Click(By.XPath(adventure.XPath), cancellationToken); - if (result.IsFailed) return result; - - result = await browser.WaitPageChanged("tasks", TableShow, cancellationToken); + result = await browser.Wait(TableShow, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/CompleteImmediately/CompleteImmediatelyCommand.cs b/MainCore/Commands/Features/CompleteImmediately/CompleteImmediatelyCommand.cs index 2d2a3b36..6b966e39 100644 --- a/MainCore/Commands/Features/CompleteImmediately/CompleteImmediatelyCommand.cs +++ b/MainCore/Commands/Features/CompleteImmediately/CompleteImmediatelyCommand.cs @@ -16,26 +16,16 @@ private static async ValueTask HandleAsync( if (oldQueueCount == 0) return Result.Ok(); - var completeNowButton = CompleteImmediatelyParser.GetCompleteButton(browser.Html); - if (completeNowButton is null) return Retry.ButtonNotFound("complete now"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => CompleteImmediatelyParser.GetCompleteButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(completeNowButton.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - static bool ConfirmShown(IWebDriver driver) - { - var doc = new HtmlDocument(); - doc.LoadHtml(driver.PageSource); - var confirmButton = CompleteImmediatelyParser.GetConfirmButton(doc); - return confirmButton is not null; - } - result = await browser.Wait(ConfirmShown, cancellationToken); - if (result.IsFailed) return result; - - var confirmButton = CompleteImmediatelyParser.GetConfirmButton(browser.Html); - if (confirmButton is null) return Retry.ButtonNotFound("confirm complete now"); + (_, isFailed, element, errors) = await browser.GetElement(doc => CompleteImmediatelyParser.GetConfirmButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - result = await browser.Click(By.XPath(confirmButton.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; static bool QueueDifferent(IWebDriver driver, int oldQueueCount) diff --git a/MainCore/Commands/Features/DisableContextualHelp/DisableContextualHelpCommand.cs b/MainCore/Commands/Features/DisableContextualHelp/DisableContextualHelpCommand.cs index e3bb16e8..7d808dca 100644 --- a/MainCore/Commands/Features/DisableContextualHelp/DisableContextualHelpCommand.cs +++ b/MainCore/Commands/Features/DisableContextualHelp/DisableContextualHelpCommand.cs @@ -13,16 +13,16 @@ private static async ValueTask HandleAsync( CancellationToken cancellationToken ) { - var option = OptionParser.GetHideContextualHelpOption(browser.Html); - if (option is null) return Retry.NotFound("hide contextual help", "option"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => OptionParser.GetHideContextualHelpOption(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(option.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - var button = OptionParser.GetSubmitButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("submit"); + (_, isFailed, element, errors) = await browser.GetElement(doc => OptionParser.GetSubmitButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - result = await browser.Click(By.XPath(button.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/DisableContextualHelp/ToOptionsPageCommand.cs b/MainCore/Commands/Features/DisableContextualHelp/ToOptionsPageCommand.cs index 60513b8c..fa33c418 100644 --- a/MainCore/Commands/Features/DisableContextualHelp/ToOptionsPageCommand.cs +++ b/MainCore/Commands/Features/DisableContextualHelp/ToOptionsPageCommand.cs @@ -13,10 +13,10 @@ private static async ValueTask HandleAsync( CancellationToken cancellationToken ) { - var button = OptionParser.GetOptionButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("options"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => OptionParser.GetOptionButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(button.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/LoginCommand.cs b/MainCore/Commands/Features/LoginCommand.cs index 3043da00..95c55093 100644 --- a/MainCore/Commands/Features/LoginCommand.cs +++ b/MainCore/Commands/Features/LoginCommand.cs @@ -13,23 +13,23 @@ private static async ValueTask HandleAsync( { if (LoginParser.IsIngamePage(browser.Html)) return Result.Ok(); - var buttonNode = LoginParser.GetLoginButton(browser.Html); - if (buttonNode is null) return Retry.ButtonNotFound("login"); - var usernameNode = LoginParser.GetUsernameInput(browser.Html); - if (usernameNode is null) return Retry.TextboxNotFound("username"); - var passwordNode = LoginParser.GetPasswordInput(browser.Html); - if (passwordNode is null) return Retry.TextboxNotFound("password"); - var (username, password) = GetLoginInfo(command.AccountId, context); Result result; - result = await browser.Input(By.XPath(usernameNode.XPath), username, cancellationToken); - if (result.IsFailed) return result; - result = await browser.Input(By.XPath(passwordNode.XPath), password, cancellationToken); + + var (_, isFailed, element, errors) = await browser.GetElement(doc => LoginParser.GetUsernameInput(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + result = await browser.Input(element, username, cancellationToken); if (result.IsFailed) return result; - result = await browser.Click(By.XPath(buttonNode.XPath), cancellationToken); + + (_, isFailed, element, errors) = await browser.GetElement(doc => LoginParser.GetPasswordInput(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + result = await browser.Input(element, password, cancellationToken); if (result.IsFailed) return result; - result = await browser.WaitPageChanged("dorf", cancellationToken); + + (_, isFailed, element, errors) = await browser.GetElement(doc => LoginParser.GetLoginButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/NpcResource/NpcResourceCommand.cs b/MainCore/Commands/Features/NpcResource/NpcResourceCommand.cs index 9c76a8e6..40c1369b 100644 --- a/MainCore/Commands/Features/NpcResource/NpcResourceCommand.cs +++ b/MainCore/Commands/Features/NpcResource/NpcResourceCommand.cs @@ -74,8 +74,6 @@ private static async ValueTask HandleAsync( if (result.IsFailed) return result; await Task.Delay(5000); - result = await browser.WaitPageLoaded(cancellationToken); - if (result.IsFailed) return result; browser.Logger.Information("After NPC:"); LogResource(browser); @@ -116,8 +114,11 @@ private static bool CanStart(IChromeBrowser browser, AppDbContext context, Villa private static async Task OpenNPCDialog(IChromeBrowser browser, CancellationToken cancellationToken) { - var button = NpcResourceParser.GetExchangeResourcesButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("Exchange resources"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => NpcResourceParser.GetExchangeResourcesButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + + var result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; static bool DialogShown(IWebDriver driver) { @@ -126,9 +127,6 @@ static bool DialogShown(IWebDriver driver) return NpcResourceParser.IsNpcDialog(doc); } - var result = await browser.Click(By.XPath(button.XPath), cancellationToken); - if (result.IsFailed) return result; - result = await browser.Wait(DialogShown, cancellationToken); if (result.IsFailed) return result; @@ -141,7 +139,10 @@ private static async Task InputAmount(IChromeBrowser browser, long[] val for (var i = 0; i < 4; i++) { - var result = await browser.Input(By.XPath(inputs[i].XPath), $"{values[i]}", cancellationToken); + var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(inputs[i].XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + + var result = await browser.Input(element, $"{values[i]}", cancellationToken); if (result.IsFailed) return result; } @@ -183,22 +184,10 @@ private static long[] GetRatio(Dictionary settings) private static async Task Distribute(IChromeBrowser browser, CancellationToken cancellationToken) { - var result = await browser.Wait(driver => - { - var doc = new HtmlDocument(); - doc.LoadHtml(driver.PageSource); - var button = NpcResourceParser.GetDistributeButton(browser.Html); - if (button is null) return false; + var (_, isFailed, element, errors) = await browser.GetElement(doc => NpcResourceParser.GetDistributeButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var elements = driver.FindElements(By.XPath(button.XPath)); - return elements.Count > 0 && elements[0].Enabled; - }, cancellationToken); - if (result.IsFailed) return result; - - var button = NpcResourceParser.GetDistributeButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("distribute"); - - result = await browser.Click(By.XPath(button.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); @@ -206,23 +195,10 @@ private static async Task Distribute(IChromeBrowser browser, Cancellatio private static async Task Redeem(IChromeBrowser browser, CancellationToken cancellationToken) { - var result = await browser.Wait(driver => - { - var doc = new HtmlDocument(); - doc.LoadHtml(driver.PageSource); - var button = NpcResourceParser.GetRedeemButton(doc); - - if (button is null) return false; - - var elements = driver.FindElements(By.XPath(button.XPath)); - return elements.Count > 0 && elements[0].Enabled; - }, cancellationToken); - if (result.IsFailed) return result; - - var button = NpcResourceParser.GetRedeemButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("redeem"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => NpcResourceParser.GetRedeemButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - result = await browser.Click(By.XPath(button.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs index ed5e7de5..6210bcb2 100644 --- a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs +++ b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs @@ -19,6 +19,11 @@ private static async ValueTask HandleAsync( if (adventureButton is null) return Retry.ButtonNotFound("adventure"); logger.Information("Start adventure {Adventure}", AdventureParser.GetAdventureInfo(adventureButton)); + var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(adventureButton.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + + var result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; static bool ContinueShow(IWebDriver driver) { var doc = new HtmlDocument(); @@ -26,10 +31,6 @@ static bool ContinueShow(IWebDriver driver) var continueButton = AdventureParser.GetContinueButton(doc); return continueButton is not null; } - - var result = await browser.Click(By.XPath(adventureButton.XPath), cancellationToken); - if (result.IsFailed) return result; - result = await browser.Wait(ContinueShow, cancellationToken); if (result.IsFailed) return result; diff --git a/MainCore/Commands/Features/StartAdventure/ToAdventurePageCommand.cs b/MainCore/Commands/Features/StartAdventure/ToAdventurePageCommand.cs index 8e99b339..9ede4767 100644 --- a/MainCore/Commands/Features/StartAdventure/ToAdventurePageCommand.cs +++ b/MainCore/Commands/Features/StartAdventure/ToAdventurePageCommand.cs @@ -12,8 +12,11 @@ private static async ValueTask HandleAsync( IChromeBrowser browser, CancellationToken cancellationToken) { - var adventure = AdventureParser.GetHeroAdventureButton(browser.Html); - if (adventure is null) return Retry.ButtonNotFound("hero adventure"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => AdventureParser.GetHeroAdventureButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + + var result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; static bool TableShow(IWebDriver driver) { @@ -21,11 +24,7 @@ static bool TableShow(IWebDriver driver) doc.LoadHtml(driver.PageSource); return AdventureParser.IsAdventurePage(doc); } - - var result = await browser.Click(By.XPath(adventure.XPath), cancellationToken); - if (result.IsFailed) return result; - - result = await browser.WaitPageChanged("adventures", TableShow, cancellationToken); + result = await browser.Wait(TableShow, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs b/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs index df2b192c..526ad840 100644 --- a/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs +++ b/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs @@ -22,10 +22,10 @@ private static async ValueTask HandleAsync( foreach (var farmList in farmLists) { - var startButton = FarmListParser.GetStartButton(browser.Html, farmList); - if (startButton is null) return Retry.ButtonNotFound($"Start farm {farmList}"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => FarmListParser.GetStartButton(doc, farmList), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(startButton.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; await delayService.DelayClick(cancellationToken); diff --git a/MainCore/Commands/Features/StartFarmList/StartAllFarmListCommand.cs b/MainCore/Commands/Features/StartFarmList/StartAllFarmListCommand.cs index 71c7621c..182b45f0 100644 --- a/MainCore/Commands/Features/StartFarmList/StartAllFarmListCommand.cs +++ b/MainCore/Commands/Features/StartFarmList/StartAllFarmListCommand.cs @@ -13,10 +13,10 @@ private static async ValueTask HandleAsync( CancellationToken cancellationToken ) { - var startAllButton = FarmListParser.GetStartAllButton(browser.Html); - if (startAllButton is null) return Retry.ButtonNotFound("Start all farms"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => FarmListParser.GetStartAllButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(startAllButton.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs index 589154a5..2a5dddba 100644 --- a/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs +++ b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs @@ -64,17 +64,17 @@ private static async ValueTask TrainTroop( long amount, CancellationToken cancellationToken) { - var inputBox = TrainTroopParser.GetInputBox(browser.Html, troop); - if (inputBox is null) return Retry.TextboxNotFound("troop amount input"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => TrainTroopParser.GetInputBox(doc, troop), cancellationToken); + if (isFailed) return Result.Fail(errors); Result result; - result = await browser.Input(By.XPath(inputBox.XPath), $"{amount}", cancellationToken); + result = await browser.Input(element, $"{amount}", cancellationToken); if (result.IsFailed) return result; var trainButton = TrainTroopParser.GetTrainButton(browser.Html); if (trainButton is null) return Retry.ButtonNotFound("train troop"); - result = await browser.Click(By.XPath(trainButton.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/UpgradeBuilding/GetBuildPlanCommand.cs b/MainCore/Commands/Features/UpgradeBuilding/GetBuildPlanCommand.cs index 49f59c0d..af919a9b 100644 --- a/MainCore/Commands/Features/UpgradeBuilding/GetBuildPlanCommand.cs +++ b/MainCore/Commands/Features/UpgradeBuilding/GetBuildPlanCommand.cs @@ -125,28 +125,5 @@ List layoutBuildings }; return normalBuildPlan; } - - private static bool IsJobComplete(JobDto job, List buildings, List queueBuildings) - { - if (job.Type == JobTypeEnums.ResourceBuild) return false; - - var plan = JsonSerializer.Deserialize(job.Content)!; - - var queueBuilding = queueBuildings - .Where(x => x.Location == plan.Location) - .OrderByDescending(x => x.Level) - .Select(x => x.Level) - .FirstOrDefault(); - - if (queueBuilding >= plan.Level) return true; - - var villageBuilding = buildings - .Where(x => x.Location == plan.Location) - .Select(x => x.Level) - .FirstOrDefault(); - if (villageBuilding >= plan.Level) return true; - - return false; - } } } \ No newline at end of file diff --git a/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs b/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs index 2839ae65..2490eef1 100644 --- a/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs +++ b/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs @@ -108,10 +108,10 @@ private static async Task SpecialUpgrade( CancellationToken cancellationToken ) { - var button = UpgradeParser.GetSpecialUpgradeButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("Watch ads upgrade"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => UpgradeParser.GetSpecialUpgradeButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(button.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; result = await browser.HandleAds(cancellationToken); @@ -147,26 +147,37 @@ static bool videoFeatureShown(IWebDriver driver) var result = await browser.Wait(videoFeatureShown, cancellationToken); if (result.IsFailed) return result; + bool isFailed; + IWebElement element; + List errors; + var videoFeature = browser.Html.GetElementbyId("videoFeature"); if (videoFeature.HasClass("infoScreen")) { var checkbox = videoFeature.Descendants("div").FirstOrDefault(x => x.HasClass("checkbox")); if (checkbox is null) return Retry.ButtonNotFound("Don't show watch ads confirm again"); - result = await browser.Click(By.XPath(checkbox.XPath), cancellationToken); + + (_, isFailed, element, errors) = await browser.GetElement(By.XPath(checkbox.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; var watchButton = videoFeature.Descendants("button").FirstOrDefault(x => x.HasClass("green")); if (watchButton is null) return Retry.ButtonNotFound("Watch ads"); - result = await browser.Click(By.XPath(watchButton.XPath), cancellationToken); + + (_, isFailed, element, errors) = await browser.GetElement(By.XPath(watchButton.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; } await Task.Delay(Random.Shared.Next(20_000, 25_000), CancellationToken.None); - var node = browser.Html.GetElementbyId("videoFeature"); - if (node is null) return Retry.ButtonNotFound($"play ads"); + (_, isFailed, element, errors) = await browser.GetElement(doc => doc.GetElementbyId("videoFeature"), cancellationToken); + if (isFailed) return Result.Fail(errors); - result = await browser.Click(By.XPath(node.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; driver.SwitchTo().DefaultContent(); @@ -184,26 +195,31 @@ static bool videoFeatureShown(IWebDriver driver) driver.Close(); driver.SwitchTo().Window(current); - result = await browser.Click(By.XPath(node.XPath), cancellationToken); + (_, isFailed, element, errors) = await browser.GetElement(doc => doc.GetElementbyId("videoFeature"), cancellationToken); + if (isFailed) return Result.Fail(errors); + + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; driver.SwitchTo().DefaultContent(); } while (true); - result = await browser.WaitPageChanged("dorf", cancellationToken); - if (result.IsFailed) return result; await Task.Delay(Random.Shared.Next(5_000, 10_000), CancellationToken.None); var dontShowThisAgain = browser.Html.GetElementbyId("dontShowThisAgain"); if (dontShowThisAgain is not null) { - result = await browser.Click(By.XPath(dontShowThisAgain.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; var okButton = browser.Html.DocumentNode.Descendants("button").FirstOrDefault(x => x.HasClass("dialogButtonOk")); if (okButton is null) return Retry.ButtonNotFound("ok"); - result = await browser.Click(By.XPath(okButton.XPath), cancellationToken); + + (_, isFailed, element, errors) = await browser.GetElement(By.XPath(okButton.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; } @@ -214,15 +230,11 @@ private static async Task Upgrade( this IChromeBrowser browser, CancellationToken cancellationToken) { - var button = UpgradeParser.GetUpgradeButton(browser.Html); - if (button is null) return Retry.ButtonNotFound("upgrade"); - - var result = await browser.Click(By.XPath(button.XPath), cancellationToken); - if (result.IsFailed) return result; + var (_, isFailed, element, errors) = await browser.GetElement(doc => UpgradeParser.GetUpgradeButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - result = await browser.WaitPageChanged("dorf", cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - return Result.Ok(); } @@ -232,15 +244,11 @@ private static async Task Construct( CancellationToken cancellationToken ) { - var button = UpgradeParser.GetConstructButton(browser.Html, building); - if (button is null) return Retry.ButtonNotFound("construct"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => UpgradeParser.GetConstructButton(doc, building), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(button.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - - result = await browser.WaitPageChanged("dorf", cancellationToken); - if (result.IsFailed) return result; - return Result.Ok(); } } diff --git a/MainCore/Commands/Features/UseHeroItem/ToHeroInventoryCommand.cs b/MainCore/Commands/Features/UseHeroItem/ToHeroInventoryCommand.cs index e21feeb1..3403c7ac 100644 --- a/MainCore/Commands/Features/UseHeroItem/ToHeroInventoryCommand.cs +++ b/MainCore/Commands/Features/UseHeroItem/ToHeroInventoryCommand.cs @@ -12,10 +12,10 @@ private static async ValueTask HandleAsync( IChromeBrowser browser, CancellationToken cancellationToken) { - var avatar = InventoryParser.GetHeroAvatar(browser.Html); - if (avatar is null) return Retry.ButtonNotFound("avatar hero"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => InventoryParser.GetHeroAvatar(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - var result = await browser.Click(By.XPath(avatar.XPath), cancellationToken); + var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; static bool TabActived(IWebDriver driver) @@ -24,7 +24,8 @@ static bool TabActived(IWebDriver driver) doc.LoadHtml(driver.PageSource); return InventoryParser.IsInventoryPage(doc); } - result = await browser.WaitPageChanged("hero", TabActived, cancellationToken); + + result = await browser.Wait(TabActived, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); diff --git a/MainCore/Commands/Features/UseHeroItem/UseHeroItemCommand.cs b/MainCore/Commands/Features/UseHeroItem/UseHeroItemCommand.cs index 92a5b093..f1ca2e43 100644 --- a/MainCore/Commands/Features/UseHeroItem/UseHeroItemCommand.cs +++ b/MainCore/Commands/Features/UseHeroItem/UseHeroItemCommand.cs @@ -35,8 +35,12 @@ private static async Task ClickItem( HeroItemEnums item, CancellationToken cancellationToken) { - var node = InventoryParser.GetItemSlot(browser.Html, item); - if (node is null) return Retry.NotFound($"{item}", "item"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => InventoryParser.GetItemSlot(doc, item), cancellationToken); + if (isFailed) return Result.Fail(errors); + + Result result; + result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; static bool loadingCompleted(IWebDriver driver) { @@ -45,10 +49,6 @@ static bool loadingCompleted(IWebDriver driver) return InventoryParser.IsInventoryLoaded(doc); } - Result result; - result = await browser.Click(By.XPath(node.XPath), cancellationToken); - if (result.IsFailed) return result; - result = await browser.Wait(driver => loadingCompleted(driver), cancellationToken); if (result.IsFailed) return result; return Result.Ok(); @@ -59,11 +59,11 @@ private static async Task EnterAmount( long amount, CancellationToken cancellationToken) { - var node = InventoryParser.GetAmountBox(browser.Html); - if (node is null) return Retry.TextboxNotFound("amount"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => InventoryParser.GetAmountBox(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); Result result; - result = await browser.Input(By.XPath(node.XPath), amount.ToString(), cancellationToken); + result = await browser.Input(element, amount.ToString(), cancellationToken); if (result.IsFailed) return result; return Result.Ok(); } @@ -72,8 +72,8 @@ private static async Task Confirm( IChromeBrowser browser, CancellationToken cancellationToken) { - var node = InventoryParser.GetConfirmButton(browser.Html); - if (node is null) return Retry.ButtonNotFound("confirm"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => InventoryParser.GetConfirmButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); static bool loadingCompleted(IWebDriver driver) { @@ -83,7 +83,7 @@ static bool loadingCompleted(IWebDriver driver) } Result result; - result = await browser.Click(By.XPath(node.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; result = await browser.Wait(driver => loadingCompleted(driver), cancellationToken); diff --git a/MainCore/Commands/Navigate/SwitchTabCommand.cs b/MainCore/Commands/Navigate/SwitchTabCommand.cs index b6223061..5266f921 100644 --- a/MainCore/Commands/Navigate/SwitchTabCommand.cs +++ b/MainCore/Commands/Navigate/SwitchTabCommand.cs @@ -25,6 +25,12 @@ public static async ValueTask SwitchTab( if (tab is null) return Retry.NotFound($"{tabIndex}", "tab"); if (BuildingTabParser.IsTabActive(tab)) return Result.Ok(); + var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(tab.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + Result result; + result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; + bool tabActived(IWebDriver driver) { var doc = new HtmlDocument(); @@ -37,9 +43,6 @@ bool tabActived(IWebDriver driver) return true; } - Result result; - result = await browser.Click(By.XPath(tab.XPath), cancellationToken); - if (result.IsFailed) return result; result = await browser.Wait(tabActived, cancellationToken); if (result.IsFailed) return result; diff --git a/MainCore/Commands/Navigate/SwitchVillageCommand.cs b/MainCore/Commands/Navigate/SwitchVillageCommand.cs index de405516..da3ce7d4 100644 --- a/MainCore/Commands/Navigate/SwitchVillageCommand.cs +++ b/MainCore/Commands/Navigate/SwitchVillageCommand.cs @@ -13,10 +13,16 @@ CancellationToken cancellationToken { var villageId = command.VillageId; - var node = VillagePanelParser.GetVillageNode(browser.Html, villageId); - if (node is null) return Skip.VillageNotFound; + var villageNode = VillagePanelParser.GetVillageNode(browser.Html, villageId); + if (villageNode is null) return Skip.VillageNotFound; - if (VillagePanelParser.IsActive(node)) return Result.Ok(); + if (VillagePanelParser.IsActive(villageNode)) return Result.Ok(); + + var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(villageNode.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors); + Result result; + result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; bool villageChanged(IWebDriver driver) { @@ -27,10 +33,6 @@ bool villageChanged(IWebDriver driver) return villageNode is not null && VillagePanelParser.IsActive(villageNode); } - Result result; - result = await browser.Click(By.XPath(node.XPath), cancellationToken); - if (result.IsFailed) return result; - result = await browser.Wait(villageChanged, cancellationToken); if (result.IsFailed) return result; diff --git a/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs b/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs index 55017736..69a85123 100644 --- a/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs +++ b/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs @@ -21,8 +21,10 @@ public static async ValueTask ToBuilding( IChromeBrowser browser, CancellationToken cancellationToken) { - var node = GetBuilding(browser.Html, location); - if (node is null) return Retry.NotFound($"{location}", "nodeBuilding"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => GetBuilding(doc, location), cancellationToken); + if (isFailed) return Result.Fail(errors); + + var node = GetBuilding(browser.Html, location)!; Result result; if (location > 18 && node.HasClass("g0")) @@ -36,9 +38,10 @@ public static async ValueTask ToBuilding( else { var css = $"#villageContent > div.buildingSlot.a{location} > svg > path"; - result = await browser.Click(By.CssSelector(css), cancellationToken); - if (result.IsFailed) return result; - result = await browser.WaitPageChanged("build.php", cancellationToken); + (_, isFailed, element, errors) = await browser.GetElement(By.CssSelector(css), cancellationToken); + if (isFailed) return Result.Fail(errors); + + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; } } @@ -59,11 +62,9 @@ public static async ValueTask ToBuilding( } else { - result = await browser.Click(By.XPath(node.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; } - result = await browser.WaitPageChanged("build.php", cancellationToken); - if (result.IsFailed) return result; } return Result.Ok(); } diff --git a/MainCore/Commands/Navigate/ToDorfCommand.cs b/MainCore/Commands/Navigate/ToDorfCommand.cs index e04a3c8d..e05c8a94 100644 --- a/MainCore/Commands/Navigate/ToDorfCommand.cs +++ b/MainCore/Commands/Navigate/ToDorfCommand.cs @@ -26,13 +26,11 @@ CancellationToken cancellationToken return Result.Ok(); } - var button = NavigationBarParser.GetDorfButton(browser.Html, dorf); - if (button is null) return Retry.ButtonNotFound($"dorf{dorf}"); + var (_, isFailed, element, errors) = await browser.GetElement(doc => NavigationBarParser.GetDorfButton(doc, dorf), cancellationToken); + if (isFailed) return Result.Fail(errors); Result result; - result = await browser.Click(By.XPath(button.XPath), cancellationToken); - if (result.IsFailed) return result; - result = await browser.WaitPageChanged($"dorf{dorf}", cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; return Result.Ok(); } diff --git a/MainCore/Errors/Retry.cs b/MainCore/Errors/Retry.cs index 43effb8e..a25e665d 100644 --- a/MainCore/Errors/Retry.cs +++ b/MainCore/Errors/Retry.cs @@ -8,6 +8,8 @@ private Retry(string message) : base($"{message}. Bot must retry") public static Retry BrowserTimeout(string message) => new(message); + public static Retry BrowserTimeout(string exception, string expression) => new($"{expression} failed. {exception}"); + public static Retry NotFound(string name, string type) => new($"Cannot find {type} [{name}] "); public static Retry TextboxNotFound(string name) => NotFound(name, "textbox"); diff --git a/MainCore/Services/ChromeBrowser.cs b/MainCore/Services/ChromeBrowser.cs index e54bc61f..066fc1aa 100644 --- a/MainCore/Services/ChromeBrowser.cs +++ b/MainCore/Services/ChromeBrowser.cs @@ -2,6 +2,7 @@ using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System.IO.Compression; +using System.Runtime.CompilerServices; namespace MainCore.Services { @@ -112,25 +113,19 @@ public async Task Refresh(CancellationToken cancellationToken) { if (Driver is null) return Stop.DriverNotReady; await Driver.Navigate().RefreshAsync(); - var result = await WaitPageLoaded(cancellationToken); - return result; + return Result.Ok(); } - private static bool PageLoaded(IWebDriver driver) => ((IJavaScriptExecutor)driver).ExecuteScript("return document.readyState")?.Equals("complete") ?? false; - - private static bool PageChanged(IWebDriver driver, string url_nested) => driver.Url.Contains(url_nested) && PageLoaded(driver); - public async Task Navigate(string url, CancellationToken cancellationToken) { if (Driver is null) return Stop.DriverNotReady; await Driver.Navigate().GoToUrlAsync(url); - var result = await Wait(driver => PageChanged(driver, url), cancellationToken); - return result; + return Result.Ok(); } - private async Task> GetElement(By by, CancellationToken cancellationToken) + public async Task> GetElement(By by, CancellationToken cancellationToken, [CallerArgumentExpression("by")] string? expression = null) { - IWebElement wait() + IWebElement getElement() { var element = _wait.Until((driver) => { @@ -145,7 +140,7 @@ IWebElement wait() try { - var element = await Task.Run(wait, cancellationToken); + var element = await Task.Run(getElement, cancellationToken); return Result.Ok(element); } catch (OperationCanceledException) @@ -154,24 +149,59 @@ IWebElement wait() } catch (WebDriverTimeoutException ex) { - return Retry.BrowserTimeout(ex.Message); + if (expression is null) + return Retry.BrowserTimeout(ex.Message); + return Retry.BrowserTimeout(ex.Message, expression); + } + } + + public async Task> GetElement(Func nodeGenerator, CancellationToken cancellationToken, [CallerArgumentExpression("nodeGenerator")] string? expression = null) + { + IWebElement getElement() + { + var element = _wait.Until((driver) => + { + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(driver.PageSource); + + var node = nodeGenerator(htmlDoc); + if (node is null) return null; + + var elements = driver.FindElements(By.XPath(node.XPath)); + if (elements.Count == 0) return null; + var element = elements[0]; + if (!element.Displayed || !element.Enabled) return null; + return element; + }, cancellationToken); + return element; + } + + try + { + var element = await Task.Run(getElement, cancellationToken); + return Result.Ok(element); + } + catch (OperationCanceledException) + { + return Cancel.Error; + } + catch (WebDriverTimeoutException ex) + { + if (expression is null) + return Retry.BrowserTimeout(ex.Message); + return Retry.BrowserTimeout(ex.Message, expression); } } - public async Task Click(By by, CancellationToken cancellationToken) + public async Task Click(IWebElement element, CancellationToken cancellationToken) { if (Driver is null) return Stop.DriverNotReady; - var (_, isFailed, element, errors) = await GetElement(by, cancellationToken); - if (isFailed) return Result.Fail(errors); await Task.Run(new Actions(Driver).Click(element).Perform); return Result.Ok(); } - public async Task Input(By by, string content, CancellationToken cancellationToken) + public async Task Input(IWebElement element, string content, CancellationToken cancellationToken) { - var (_, isFailed, element, errors) = await GetElement(by, cancellationToken); - if (isFailed) return Result.Fail(errors); - void input() { element.SendKeys(Keys.Home); @@ -214,21 +244,6 @@ void wait() return Result.Ok(); } - public Task WaitPageLoaded(CancellationToken cancellationToken) - { - return Wait(PageLoaded, cancellationToken); - } - - public Task WaitPageChanged(string part, CancellationToken cancellationToken) - { - return Wait(driver => PageChanged(driver, part), cancellationToken); - } - - public Task WaitPageChanged(string part, Predicate customCondition, CancellationToken cancellationToken) - { - return Wait(driver => PageChanged(driver, part) && customCondition(driver), cancellationToken); - } - public async Task Close() { await Task.Run(() => _driver?.Quit()); diff --git a/MainCore/Services/IChromeBrowser.cs b/MainCore/Services/IChromeBrowser.cs index 69f384b6..e955ec06 100644 --- a/MainCore/Services/IChromeBrowser.cs +++ b/MainCore/Services/IChromeBrowser.cs @@ -1,4 +1,5 @@ using OpenQA.Selenium.Chrome; +using System.Runtime.CompilerServices; namespace MainCore.Services { @@ -9,13 +10,15 @@ public interface IChromeBrowser HtmlDocument Html { get; } ILogger Logger { get; set; } - Task Click(By by, CancellationToken cancellationToken); + Task Click(IWebElement element, CancellationToken cancellationToken); Task Close(); Task ExecuteJsScript(string javascript); - Task Input(By by, string content, CancellationToken cancellationToken); + Task> GetElement(Func nodeGenerator, CancellationToken cancellationToken, [CallerArgumentExpression("nodeGenerator")] string? expression = null); + Task> GetElement(By by, CancellationToken cancellationToken, [CallerArgumentExpression("by")] string? expression = null); + Task Input(IWebElement element, string content, CancellationToken cancellationToken); Task Navigate(string url, CancellationToken cancellationToken); @@ -28,11 +31,5 @@ public interface IChromeBrowser Task Shutdown(); Task Wait(Predicate condition, CancellationToken cancellationToken); - - Task WaitPageChanged(string part, CancellationToken cancellationToken); - - Task WaitPageChanged(string part, Predicate customCondition, CancellationToken cancellationToken); - - Task WaitPageLoaded(CancellationToken cancellationToken); } } \ No newline at end of file From c1bed3fd293b44f5d5b011b9e48403c1f9421a19 Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Mon, 22 Sep 2025 18:18:33 +0700 Subject: [PATCH 02/11] Enhance debugging and reliability in ChromeBrowser - Added `[CallerArgumentExpression]` to `Wait` and `GetElement` methods for improved error reporting and debugging. - Updated `Wait` in `ChromeBrowser.cs` to include better error handling and context in timeout exceptions. - Modified `Click` to include a `Wait` call for ensuring specific conditions (e.g., visibility of the `logo` element). - Added a new `GetElement` overload in `IChromeBrowser.cs` supporting `By` locators with `[CallerArgumentExpression]`. - Updated `Wait` signature in `IChromeBrowser.cs` to align with the implementation in `ChromeBrowser.cs`. - Introduced `IDelayService` in `LoginTask.cs` to add delays for smoother execution during the login process. - Improved maintainability and reliability across the codebase by enhancing error handling and debugging capabilities. --- MainCore/Services/ChromeBrowser.cs | 12 ++++++++++-- MainCore/Services/IChromeBrowser.cs | 4 +++- MainCore/Tasks/LoginTask.cs | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/MainCore/Services/ChromeBrowser.cs b/MainCore/Services/ChromeBrowser.cs index 066fc1aa..1993635f 100644 --- a/MainCore/Services/ChromeBrowser.cs +++ b/MainCore/Services/ChromeBrowser.cs @@ -197,6 +197,12 @@ public async Task Click(IWebElement element, CancellationToken cancellat { if (Driver is null) return Stop.DriverNotReady; await Task.Run(new Actions(Driver).Click(element).Perform); + + await Wait(driver => + { + var logo = driver.FindElements(By.Id("logo")); + return logo.Count > 0 && logo[0].Displayed && logo[0].Enabled; + }, cancellationToken); return Result.Ok(); } @@ -222,7 +228,7 @@ public async Task ExecuteJsScript(string javascript) return Result.Ok(); } - public async Task Wait(Predicate condition, CancellationToken cancellationToken) + public async Task Wait(Predicate condition, CancellationToken cancellationToken, [CallerArgumentExpression("condition")] string? expression = null) { void wait() { @@ -239,7 +245,9 @@ void wait() } catch (WebDriverTimeoutException ex) { - return Retry.BrowserTimeout(ex.Message); + if (expression is null) + return Retry.BrowserTimeout(ex.Message); + return Retry.BrowserTimeout(ex.Message, expression); } return Result.Ok(); } diff --git a/MainCore/Services/IChromeBrowser.cs b/MainCore/Services/IChromeBrowser.cs index e955ec06..9da34fb0 100644 --- a/MainCore/Services/IChromeBrowser.cs +++ b/MainCore/Services/IChromeBrowser.cs @@ -17,7 +17,9 @@ public interface IChromeBrowser Task ExecuteJsScript(string javascript); Task> GetElement(Func nodeGenerator, CancellationToken cancellationToken, [CallerArgumentExpression("nodeGenerator")] string? expression = null); + Task> GetElement(By by, CancellationToken cancellationToken, [CallerArgumentExpression("by")] string? expression = null); + Task Input(IWebElement element, string content, CancellationToken cancellationToken); Task Navigate(string url, CancellationToken cancellationToken); @@ -30,6 +32,6 @@ public interface IChromeBrowser Task Shutdown(); - Task Wait(Predicate condition, CancellationToken cancellationToken); + Task Wait(Predicate condition, CancellationToken cancellationToken, [CallerArgumentExpression("condition")] string? expression = null); } } \ No newline at end of file diff --git a/MainCore/Tasks/LoginTask.cs b/MainCore/Tasks/LoginTask.cs index 3280e72d..15829354 100644 --- a/MainCore/Tasks/LoginTask.cs +++ b/MainCore/Tasks/LoginTask.cs @@ -18,6 +18,7 @@ private static async ValueTask HandleAsync( ToOptionsPageCommand.Handler toOptionsPageCommand, DisableContextualHelpCommand.Handler disableContextualHelpCommand, ToDorfCommand.Handler toDorfCommand, + IDelayService delayService, IChromeBrowser chromeBrowser, CancellationToken cancellationToken) { @@ -25,6 +26,8 @@ private static async ValueTask HandleAsync( result = await loginCommand.HandleAsync(new(task.AccountId), cancellationToken); if (result.IsFailed) return result; + await delayService.DelayTask(cancellationToken); + var contextualHelpEnable = OptionParser.IsContextualHelpEnable(chromeBrowser.Html); if (!contextualHelpEnable) return Result.Ok(); From e28c3bc3a4fbc056205ea10bd17956754cc4a96b Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Mon, 22 Sep 2025 20:45:55 +0700 Subject: [PATCH 03/11] Add WaitPageChanged method for reliable page transitions Introduced a reusable `WaitPageChanged` method in `ChromeBrowser.cs` and `IChromeBrowser.cs` to handle URL changes and ensure pages are fully loaded. Updated various commands (`LoginCommand.cs`, `HandleUpgradeCommand.cs`, `ToBuildingByLocationCommand.cs`, `ToDorfCommand.cs`) to use this method after navigation actions. Removed redundant `Wait` logic from the `Click` method in `ChromeBrowser.cs`, consolidating it into `WaitPageChanged` for better modularity. Enhanced error handling in `WaitPageChanged` to provide detailed messages for failures. --- MainCore/Commands/Features/LoginCommand.cs | 3 +++ .../UpgradeBuilding/HandleUpgradeCommand.cs | 10 +++++++ .../Navigate/ToBuildingByLocationCommand.cs | 4 +++ MainCore/Commands/Navigate/ToDorfCommand.cs | 4 +++ MainCore/Services/ChromeBrowser.cs | 26 ++++++++++++++----- MainCore/Services/IChromeBrowser.cs | 2 ++ 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/MainCore/Commands/Features/LoginCommand.cs b/MainCore/Commands/Features/LoginCommand.cs index 95c55093..b6bf3908 100644 --- a/MainCore/Commands/Features/LoginCommand.cs +++ b/MainCore/Commands/Features/LoginCommand.cs @@ -32,6 +32,9 @@ private static async ValueTask HandleAsync( result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; + result = await browser.WaitPageChanged("dorf", cancellationToken); + if (result.IsFailed) return result; + return Result.Ok(); } diff --git a/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs b/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs index 2490eef1..811184a3 100644 --- a/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs +++ b/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs @@ -205,6 +205,9 @@ static bool videoFeatureShown(IWebDriver driver) } while (true); + result = await browser.WaitPageChanged("dorf", cancellationToken); + if (result.IsFailed) return result; + await Task.Delay(Random.Shared.Next(5_000, 10_000), CancellationToken.None); var dontShowThisAgain = browser.Html.GetElementbyId("dontShowThisAgain"); @@ -235,6 +238,10 @@ private static async Task Upgrade( var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; + + result = await browser.WaitPageChanged("dorf", cancellationToken); + if (result.IsFailed) return result; + return Result.Ok(); } @@ -249,6 +256,9 @@ CancellationToken cancellationToken var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; + + result = await browser.WaitPageChanged("dorf", cancellationToken); + if (result.IsFailed) return result; return Result.Ok(); } } diff --git a/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs b/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs index 69a85123..a3fbc8f0 100644 --- a/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs +++ b/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs @@ -66,6 +66,10 @@ public static async ValueTask ToBuilding( if (result.IsFailed) return result; } } + + result = await browser.WaitPageChanged("build", cancellationToken); + if (result.IsFailed) return result; + return Result.Ok(); } diff --git a/MainCore/Commands/Navigate/ToDorfCommand.cs b/MainCore/Commands/Navigate/ToDorfCommand.cs index e05c8a94..cde5acf4 100644 --- a/MainCore/Commands/Navigate/ToDorfCommand.cs +++ b/MainCore/Commands/Navigate/ToDorfCommand.cs @@ -32,6 +32,10 @@ CancellationToken cancellationToken Result result; result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; + + result = await browser.WaitPageChanged($"dorf{dorf}.php", cancellationToken); + if (result.IsFailed) return result; + return Result.Ok(); } diff --git a/MainCore/Services/ChromeBrowser.cs b/MainCore/Services/ChromeBrowser.cs index 1993635f..f646faf3 100644 --- a/MainCore/Services/ChromeBrowser.cs +++ b/MainCore/Services/ChromeBrowser.cs @@ -196,13 +196,8 @@ IWebElement getElement() public async Task Click(IWebElement element, CancellationToken cancellationToken) { if (Driver is null) return Stop.DriverNotReady; - await Task.Run(new Actions(Driver).Click(element).Perform); - await Wait(driver => - { - var logo = driver.FindElements(By.Id("logo")); - return logo.Count > 0 && logo[0].Displayed && logo[0].Enabled; - }, cancellationToken); + await Task.Run(new Actions(Driver).Click(element).Perform); return Result.Ok(); } @@ -228,6 +223,25 @@ public async Task ExecuteJsScript(string javascript) return Result.Ok(); } + public async Task WaitPageChanged(string url, CancellationToken cancellationToken) + { + var result = await Wait(driver => + { + return driver.Url.Contains(url); + }, cancellationToken); + + if (result.IsFailed) return result.WithError($"Failed to wait for URL change [{url}]"); + + result = await Wait(driver => + { + var logo = driver.FindElements(By.Id("logo")); + return logo.Count > 0 && logo[0].Displayed && logo[0].Enabled; + }, cancellationToken); + + if (result.IsFailed) return result.WithError("Failed to wait for logo to be displayed"); + return Result.Ok(); + } + public async Task Wait(Predicate condition, CancellationToken cancellationToken, [CallerArgumentExpression("condition")] string? expression = null) { void wait() diff --git a/MainCore/Services/IChromeBrowser.cs b/MainCore/Services/IChromeBrowser.cs index 9da34fb0..daa41067 100644 --- a/MainCore/Services/IChromeBrowser.cs +++ b/MainCore/Services/IChromeBrowser.cs @@ -33,5 +33,7 @@ public interface IChromeBrowser Task Shutdown(); Task Wait(Predicate condition, CancellationToken cancellationToken, [CallerArgumentExpression("condition")] string? expression = null); + + Task WaitPageChanged(string url, CancellationToken cancellationToken); } } \ No newline at end of file From 06d826beab4b712b56e171dd01f6c48ed3db701f Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Mon, 22 Sep 2025 21:13:45 +0700 Subject: [PATCH 04/11] Refactor task handling in BuildingsModified and JobsModified Refactored the `BuildingsModified` method to improve readability by reordering early exit checks and command executions. In the `JobsModified` method, moved `_taskManager.AddOrUpdate` for `UpgradeBuildingTask.Task` to the beginning of the method to ensure proper task handling before other operations. Removed redundant `_taskManager.AddOrUpdate` call at the end of `JobsModified` to eliminate duplication and improve clarity. --- .../UI/ViewModels/Tabs/Villages/BuildViewModel.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MainCore/UI/ViewModels/Tabs/Villages/BuildViewModel.cs b/MainCore/UI/ViewModels/Tabs/Villages/BuildViewModel.cs index de7e65c5..f9b8cb82 100644 --- a/MainCore/UI/ViewModels/Tabs/Villages/BuildViewModel.cs +++ b/MainCore/UI/ViewModels/Tabs/Villages/BuildViewModel.cs @@ -64,11 +64,6 @@ public BuildViewModel(IDialogService dialogService, IValidator [ReactiveCommand] public async Task BuildingsModified(BuildingsModified notification) { - if (!IsActive) return; - if (notification.VillageId != VillageId) return; - await LoadQueueCommand.Execute(notification.VillageId); - await LoadBuildingCommand.Execute(notification.VillageId); - using var scope = _serviceScopeFactory.CreateScope(AccountId); var context = scope.ServiceProvider.GetRequiredService(); var task = new CompleteImmediatelyTask.Task(AccountId, notification.VillageId); @@ -76,17 +71,22 @@ public async Task BuildingsModified(BuildingsModified notification) { _taskManager.Add(task); } + + if (!IsActive) return; + if (notification.VillageId != VillageId) return; + await LoadQueueCommand.Execute(notification.VillageId); + await LoadBuildingCommand.Execute(notification.VillageId); } [ReactiveCommand] public async Task JobsModified(JobsModified notification) { + _taskManager.AddOrUpdate(new UpgradeBuildingTask.Task(AccountId, notification.VillageId)); + if (!IsActive) return; if (notification.VillageId != VillageId) return; await LoadJobCommand.Execute(notification.VillageId); await LoadBuildingCommand.Execute(notification.VillageId); - - _taskManager.AddOrUpdate(new UpgradeBuildingTask.Task(AccountId, notification.VillageId)); } protected override async Task Load(VillageId villageId) From f66b8b272e9dae0b0ba2441e4dd63b045154115f Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Tue, 23 Sep 2025 18:04:12 +0700 Subject: [PATCH 05/11] Refactor Wait method and improve error message Simplified the `Wait` method call by condensing the lambda expression into a single line for better readability. Enhanced the error message in the `if (result.IsFailed)` block to include the `CurrentUrl` property, providing additional context for debugging when the URL change wait fails. --- MainCore/Services/ChromeBrowser.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/MainCore/Services/ChromeBrowser.cs b/MainCore/Services/ChromeBrowser.cs index f646faf3..cdca5106 100644 --- a/MainCore/Services/ChromeBrowser.cs +++ b/MainCore/Services/ChromeBrowser.cs @@ -225,12 +225,8 @@ public async Task ExecuteJsScript(string javascript) public async Task WaitPageChanged(string url, CancellationToken cancellationToken) { - var result = await Wait(driver => - { - return driver.Url.Contains(url); - }, cancellationToken); - - if (result.IsFailed) return result.WithError($"Failed to wait for URL change [{url}]"); + var result = await Wait(driver => driver.Url.Contains(url), cancellationToken); + if (result.IsFailed) return result.WithError($"Failed to wait for URL change [{url}], current URL is [{CurrentUrl}]"); result = await Wait(driver => { From 0cf5e3d929df163ea17cbe9b719cddd1c3ccda06 Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Tue, 23 Sep 2025 23:17:41 +0700 Subject: [PATCH 06/11] fix new building on military&resource tab cannot be built --- .../Features/UpgradeBuilding/ToBuildPageCommand.cs | 2 +- MainCore/Commands/Navigate/SwitchManagementTabCommand.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MainCore/Commands/Features/UpgradeBuilding/ToBuildPageCommand.cs b/MainCore/Commands/Features/UpgradeBuilding/ToBuildPageCommand.cs index 2e0db29d..d578920c 100644 --- a/MainCore/Commands/Features/UpgradeBuilding/ToBuildPageCommand.cs +++ b/MainCore/Commands/Features/UpgradeBuilding/ToBuildPageCommand.cs @@ -24,7 +24,7 @@ private static async ValueTask HandleAsync( await delayService.DelayClick(cancellationToken); - result = await switchManagementTabCommand.HandleAsync(new(villageId, plan.Location), cancellationToken); + result = await switchManagementTabCommand.HandleAsync(new(villageId, plan), cancellationToken); if (result.IsFailed) return result; await delayService.DelayClick(cancellationToken); diff --git a/MainCore/Commands/Navigate/SwitchManagementTabCommand.cs b/MainCore/Commands/Navigate/SwitchManagementTabCommand.cs index 0bde335e..b07a95a4 100644 --- a/MainCore/Commands/Navigate/SwitchManagementTabCommand.cs +++ b/MainCore/Commands/Navigate/SwitchManagementTabCommand.cs @@ -3,7 +3,7 @@ [Handler] public static partial class SwitchManagementTabCommand { - public sealed record Command(VillageId VillageId, int Location) : IVillageCommand; + public sealed record Command(VillageId VillageId, NormalBuildPlan Plan) : IVillageCommand; private static async ValueTask HandleAsync( Command command, @@ -12,17 +12,17 @@ private static async ValueTask HandleAsync( CancellationToken cancellationToken ) { - var (villageId, location) = command; + var (villageId, plan) = command; var building = context.Buildings .Where(x => x.VillageId == villageId.Value) - .FirstOrDefault(x => x.Location == location); + .FirstOrDefault(x => x.Location == plan.Location); if (building is null) return Result.Ok(); Result result; if (building.Type == BuildingEnums.Site) { - var tabIndex = building.Type.GetBuildingsCategory(); + var tabIndex = plan.Type.GetBuildingsCategory(); result = await SwitchTabCommand.SwitchTab(browser, tabIndex, cancellationToken); if (result.IsFailed) return result; From 91285e49ba5663155052539d8ef73e052a273fbd Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Wed, 24 Sep 2025 11:37:00 +0700 Subject: [PATCH 07/11] Refactored the retrieval of the "train button" element to use an asynchronous `GetElement` operation with a document parser (`TrainTroopParser.GetTrainButton`) and a cancellation token. --- MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs index 2a5dddba..12df480e 100644 --- a/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs +++ b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs @@ -74,6 +74,9 @@ private static async ValueTask TrainTroop( var trainButton = TrainTroopParser.GetTrainButton(browser.Html); if (trainButton is null) return Retry.ButtonNotFound("train troop"); + (_, isFailed, element, errors) = await browser.GetElement(doc => TrainTroopParser.GetTrainButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; From 28da3807c24b63b94f15d0180356ec805f3ba7a7 Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Thu, 25 Sep 2025 10:08:54 +0700 Subject: [PATCH 08/11] Refactor train button retrieval logic Replaced the old `TrainTroopParser.GetTrainButton` logic with a more robust approach using `browser.GetElement` and a lambda function. --- MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs index 12df480e..e09d3368 100644 --- a/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs +++ b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs @@ -71,9 +71,6 @@ private static async ValueTask TrainTroop( result = await browser.Input(element, $"{amount}", cancellationToken); if (result.IsFailed) return result; - var trainButton = TrainTroopParser.GetTrainButton(browser.Html); - if (trainButton is null) return Retry.ButtonNotFound("train troop"); - (_, isFailed, element, errors) = await browser.GetElement(doc => TrainTroopParser.GetTrainButton(doc), cancellationToken); if (isFailed) return Result.Fail(errors); From b5b2bd0d5c63433fac53896c5b75f9351e37660a Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Thu, 25 Sep 2025 11:49:11 +0700 Subject: [PATCH 09/11] Refactor error handling and improve logging Enhanced error messages across the codebase for better clarity. Refactored the `Retry` class to simplify and standardize error handling using a new `Retry.Error` property. Introduced the `WithError` method for chaining additional error context. Improved logging to provide more context during execution, including adventure details and specific failure points. Simplified XPath handling by using `nodeGenerator` functions for better readability. Updated tab index validation logic in `SwitchTabCommand.cs` to ensure accurate error reporting. Refactored exception handling in `ChromeBrowser` to use the new `Retry.Error` mechanism. Removed redundant error-handling methods and standardized error reporting with the `Result.Fail(errors).WithError(...)` pattern. Made namespace-specific changes to ensure consistency in error handling and logging. Improved debugging support and code readability by reducing redundancy and adding detailed error messages. Added additional wait logic to enhance automation reliability. --- .../StartAdventure/ExploreAdventureCommand.cs | 7 ++--- .../UpgradeBuilding/HandleUpgradeCommand.cs | 27 ++++++++----------- .../Commands/Navigate/SwitchTabCommand.cs | 10 +++---- .../Navigate/ToBuildingByLocationCommand.cs | 6 ++--- MainCore/Errors/Retry.cs | 18 ++----------- MainCore/Services/ChromeBrowser.cs | 18 ++++++------- 6 files changed, 33 insertions(+), 53 deletions(-) diff --git a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs index 6210bcb2..6f072c60 100644 --- a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs +++ b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs @@ -16,14 +16,16 @@ private static async ValueTask HandleAsync( if (!AdventureParser.CanStartAdventure(browser.Html)) return Skip.NoAdventure; var adventureButton = AdventureParser.GetAdventureButton(browser.Html); - if (adventureButton is null) return Retry.ButtonNotFound("adventure"); + if (adventureButton is null) return Retry.Error.WithError($"Failed to find adventure button"); + logger.Information("Start adventure {Adventure}", AdventureParser.GetAdventureInfo(adventureButton)); var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(adventureButton.XPath), cancellationToken); - if (isFailed) return Result.Fail(errors); + if (isFailed) return Result.Fail(errors).WithError($"Failed to find adventure button [{adventureButton.XPath}]"); var result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; + static bool ContinueShow(IWebDriver driver) { var doc = new HtmlDocument(); @@ -33,7 +35,6 @@ static bool ContinueShow(IWebDriver driver) } result = await browser.Wait(ContinueShow, cancellationToken); if (result.IsFailed) return result; - return Result.Ok(); } } diff --git a/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs b/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs index 811184a3..f4904a8d 100644 --- a/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs +++ b/MainCore/Commands/Features/UpgradeBuilding/HandleUpgradeCommand.cs @@ -154,20 +154,15 @@ static bool videoFeatureShown(IWebDriver driver) var videoFeature = browser.Html.GetElementbyId("videoFeature"); if (videoFeature.HasClass("infoScreen")) { - var checkbox = videoFeature.Descendants("div").FirstOrDefault(x => x.HasClass("checkbox")); - if (checkbox is null) return Retry.ButtonNotFound("Don't show watch ads confirm again"); - - (_, isFailed, element, errors) = await browser.GetElement(By.XPath(checkbox.XPath), cancellationToken); - if (isFailed) return Result.Fail(errors); + (_, isFailed, element, errors) = await browser.GetElement(doc => doc.GetElementbyId("videoFeature").Descendants("div").FirstOrDefault(x => x.HasClass("checkbox")), cancellationToken); + if (isFailed) return Result.Fail(errors).WithError("Failed to find [Don't show watch ads confirm again] checkbox"); result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - var watchButton = videoFeature.Descendants("button").FirstOrDefault(x => x.HasClass("green")); - if (watchButton is null) return Retry.ButtonNotFound("Watch ads"); + (_, isFailed, element, errors) = await browser.GetElement(doc => doc.GetElementbyId("videoFeature").Descendants("button").FirstOrDefault(x => x.HasClass("green")), cancellationToken); + if (isFailed) return Result.Fail(errors).WithError("Failed to find [Watch ads] button"); - (_, isFailed, element, errors) = await browser.GetElement(By.XPath(watchButton.XPath), cancellationToken); - if (isFailed) return Result.Fail(errors); result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; } @@ -175,7 +170,7 @@ static bool videoFeatureShown(IWebDriver driver) await Task.Delay(Random.Shared.Next(20_000, 25_000), CancellationToken.None); (_, isFailed, element, errors) = await browser.GetElement(doc => doc.GetElementbyId("videoFeature"), cancellationToken); - if (isFailed) return Result.Fail(errors); + if (isFailed) return Result.Fail(errors).WithError("Failed to find [Play ads video] button"); result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; @@ -196,7 +191,7 @@ static bool videoFeatureShown(IWebDriver driver) driver.SwitchTo().Window(current); (_, isFailed, element, errors) = await browser.GetElement(doc => doc.GetElementbyId("videoFeature"), cancellationToken); - if (isFailed) return Result.Fail(errors); + if (isFailed) return Result.Fail(errors).WithError("Failed to find [Play ads video] button"); result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; @@ -213,14 +208,14 @@ static bool videoFeatureShown(IWebDriver driver) var dontShowThisAgain = browser.Html.GetElementbyId("dontShowThisAgain"); if (dontShowThisAgain is not null) { + (_, isFailed, element, errors) = await browser.GetElement(By.XPath(dontShowThisAgain.XPath), cancellationToken); + if (isFailed) return Result.Fail(errors).WithError("Failed to find [Don't show this again] checkbox"); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - var okButton = browser.Html.DocumentNode.Descendants("button").FirstOrDefault(x => x.HasClass("dialogButtonOk")); - if (okButton is null) return Retry.ButtonNotFound("ok"); - - (_, isFailed, element, errors) = await browser.GetElement(By.XPath(okButton.XPath), cancellationToken); - if (isFailed) return Result.Fail(errors); + (_, isFailed, element, errors) = await browser.GetElement(doc => doc.DocumentNode.Descendants("button").FirstOrDefault(x => x.HasClass("dialogButtonOk")), cancellationToken); + if (isFailed) return Result.Fail(errors).WithError("Failed to find [OK] button"); result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; diff --git a/MainCore/Commands/Navigate/SwitchTabCommand.cs b/MainCore/Commands/Navigate/SwitchTabCommand.cs index 5266f921..aa4d24a0 100644 --- a/MainCore/Commands/Navigate/SwitchTabCommand.cs +++ b/MainCore/Commands/Navigate/SwitchTabCommand.cs @@ -20,13 +20,14 @@ public static async ValueTask SwitchTab( CancellationToken cancellationToken) { var count = BuildingTabParser.CountTab(browser.Html); - if (tabIndex > count) return Retry.OutOfIndexTab(tabIndex, count); + if (tabIndex >= count) return Retry.Error.WithError($"Found {count} tabs but need tab #{tabIndex + 1} active"); + var tab = BuildingTabParser.GetTab(browser.Html, tabIndex); - if (tab is null) return Retry.NotFound($"{tabIndex}", "tab"); if (BuildingTabParser.IsTabActive(tab)) return Result.Ok(); var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(tab.XPath), cancellationToken); - if (isFailed) return Result.Fail(errors); + if (isFailed) return Result.Fail(errors).WithError($"Failed to find tab element [{tab.XPath}]"); + Result result; result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; @@ -35,10 +36,7 @@ bool tabActived(IWebDriver driver) { var doc = new HtmlDocument(); doc.LoadHtml(driver.PageSource); - var count = BuildingTabParser.CountTab(doc); - if (tabIndex > count) return false; var tab = BuildingTabParser.GetTab(doc, tabIndex); - if (tab is null) return false; if (!BuildingTabParser.IsTabActive(tab)) return false; return true; } diff --git a/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs b/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs index a3fbc8f0..53933694 100644 --- a/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs +++ b/MainCore/Commands/Navigate/ToBuildingByLocationCommand.cs @@ -22,7 +22,7 @@ public static async ValueTask ToBuilding( CancellationToken cancellationToken) { var (_, isFailed, element, errors) = await browser.GetElement(doc => GetBuilding(doc, location), cancellationToken); - if (isFailed) return Result.Fail(errors); + if (isFailed) return Result.Fail(errors).WithError($"Failed to find [building at #{location}]"); var node = GetBuilding(browser.Html, location)!; @@ -50,10 +50,10 @@ public static async ValueTask ToBuilding( if (location == 40) // wall { var path = node.Descendants("path").FirstOrDefault(); - if (path is null) return Retry.NotFound($"{location}", "wall bottom path"); + if (path is null) return Retry.Error.WithError("Failed to find [wall]"); var javascript = path.GetAttributeValue("onclick", ""); - if (string.IsNullOrEmpty(javascript)) return Retry.NotFound($"{location}", "JavaScriptExecutor onclick wall"); + if (string.IsNullOrEmpty(javascript)) return Retry.Error.WithError("Failed to find [wall's onclick event]"); var decodedJs = HttpUtility.HtmlDecode(javascript); diff --git a/MainCore/Errors/Retry.cs b/MainCore/Errors/Retry.cs index a25e665d..b57412f5 100644 --- a/MainCore/Errors/Retry.cs +++ b/MainCore/Errors/Retry.cs @@ -2,24 +2,10 @@ { public class Retry : Error { - private Retry(string message) : base($"{message}. Bot must retry") + private Retry() : base("Bot must retry") { } - public static Retry BrowserTimeout(string message) => new(message); - - public static Retry BrowserTimeout(string exception, string expression) => new($"{expression} failed. {exception}"); - - public static Retry NotFound(string name, string type) => new($"Cannot find {type} [{name}] "); - - public static Retry TextboxNotFound(string name) => NotFound(name, "textbox"); - - public static Retry ButtonNotFound(string name) => NotFound(name, "button"); - - public static Retry ElementNotFound(By by) => new($"Element {by} not found"); - - public static Retry ElementNotClickable(By by) => new($"Element {by} not clickable"); - - public static Retry OutOfIndexTab(int index, int count) => new($"Found {count} tabs but need tab {index + 1} active"); + public static Result Error => new Retry(); } } \ No newline at end of file diff --git a/MainCore/Services/ChromeBrowser.cs b/MainCore/Services/ChromeBrowser.cs index cdca5106..0f304bc2 100644 --- a/MainCore/Services/ChromeBrowser.cs +++ b/MainCore/Services/ChromeBrowser.cs @@ -149,9 +149,9 @@ IWebElement getElement() } catch (WebDriverTimeoutException ex) { - if (expression is null) - return Retry.BrowserTimeout(ex.Message); - return Retry.BrowserTimeout(ex.Message, expression); + var error = Retry.Error.WithError(ex.Message); + if (expression is not null) return error.WithError(expression); + return error; } } @@ -187,9 +187,9 @@ IWebElement getElement() } catch (WebDriverTimeoutException ex) { - if (expression is null) - return Retry.BrowserTimeout(ex.Message); - return Retry.BrowserTimeout(ex.Message, expression); + var error = Retry.Error.WithError(ex.Message); + if (expression is not null) return error.WithError(expression); + return error; } } @@ -255,9 +255,9 @@ void wait() } catch (WebDriverTimeoutException ex) { - if (expression is null) - return Retry.BrowserTimeout(ex.Message); - return Retry.BrowserTimeout(ex.Message, expression); + var error = Retry.Error.WithError(ex.Message); + if (expression is not null) return error.WithError(expression); + return error; } return Result.Ok(); } From feb37b68c7d1c0cef7ba97faac393e3ab4728257 Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Thu, 25 Sep 2025 12:02:48 +0700 Subject: [PATCH 10/11] Refactor `Skip` error handling for dynamic messages Refactored the `Skip` class to replace static error instances with a generic `Skip.Error` that supports dynamic error messages via the `WithError` method. Updated all references to use the new approach, enabling more descriptive and context-specific error handling. Removed redundant static `Skip` error instances to improve maintainability and reduce code duplication. --- .../Behaviors/ErrorLoggingBehaviorTest.cs | 1 - MainCore/Behaviors/AccountTaskBehavior.cs | 2 +- .../StartAdventure/ExploreAdventureCommand.cs | 2 +- .../StartFarmList/StartActiveFarmListCommand.cs | 2 +- .../StartFarmList/ToFarmListPageCommand.cs | 2 +- .../Commands/Navigate/SwitchVillageCommand.cs | 2 +- MainCore/Errors/Skip.cs | 15 ++------------- MainCore/Tasks/NPCTask.cs | 4 ++-- MainCore/Tasks/UpgradeBuildingTask.cs | 4 ++-- 9 files changed, 11 insertions(+), 23 deletions(-) diff --git a/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs b/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs index ebd8d0d4..31b2b600 100644 --- a/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs +++ b/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs @@ -66,7 +66,6 @@ public async Task ErrorLoggingBehaviorShouldLogCorrectErrorMessage(Result result new List { new object[] { Result.Fail(Cancel.Error), "Pause button is pressed" }, - new object[] { Result.Fail(Skip.VillageNotFound), "Village not found" }, new object[] { Result.Fail(Stop.EnglishRequired("abcxyz")), "Cannot parse abcxyz. Is language English ?. Bot must stop" }, }; } diff --git a/MainCore/Behaviors/AccountTaskBehavior.cs b/MainCore/Behaviors/AccountTaskBehavior.cs index 984ee7c0..f4ab3745 100644 --- a/MainCore/Behaviors/AccountTaskBehavior.cs +++ b/MainCore/Behaviors/AccountTaskBehavior.cs @@ -37,7 +37,7 @@ public override async ValueTask HandleAsync(TRequest request, Cancell { _taskManager.AddOrUpdate(new(accountId), first: true); request.ExecuteAt = request.ExecuteAt.AddSeconds(1); - return (TResponse)Skip.AccountLogout; + return (TResponse)Skip.Error.WithError("Account is logout. Re-login now"); } } diff --git a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs index 6f072c60..85750aac 100644 --- a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs +++ b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs @@ -13,7 +13,7 @@ private static async ValueTask HandleAsync( ILogger logger, CancellationToken cancellationToken) { - if (!AdventureParser.CanStartAdventure(browser.Html)) return Skip.NoAdventure; + if (!AdventureParser.CanStartAdventure(browser.Html)) return Skip.Error.WithError("No adventure available"); var adventureButton = AdventureParser.GetAdventureButton(browser.Html); if (adventureButton is null) return Retry.Error.WithError($"Failed to find adventure button"); diff --git a/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs b/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs index 526ad840..b169ada7 100644 --- a/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs +++ b/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs @@ -18,7 +18,7 @@ private static async ValueTask HandleAsync( .Where(x => x.IsActive) .Select(x => new FarmId(x.Id)) .ToList(); - if (farmLists.Count == 0) return Skip.NoActiveFarmlist; + if (farmLists.Count == 0) return Skip.Error.WithError("No farmlist is active"); foreach (var farmList in farmLists) { diff --git a/MainCore/Commands/Features/StartFarmList/ToFarmListPageCommand.cs b/MainCore/Commands/Features/StartFarmList/ToFarmListPageCommand.cs index 7ceaf851..b0fd70ab 100644 --- a/MainCore/Commands/Features/StartFarmList/ToFarmListPageCommand.cs +++ b/MainCore/Commands/Features/StartFarmList/ToFarmListPageCommand.cs @@ -18,7 +18,7 @@ private static async ValueTask HandleAsync( { var accountId = command.AccountId; var rallypointVillageId = await getHasRallypointVillageCommand.HandleAsync(new(accountId), cancellationToken); - if (rallypointVillageId == VillageId.Empty) return Skip.NoRallypoint; + if (rallypointVillageId == VillageId.Empty) return Skip.Error.WithError("No rallypoint found. Recheck & load village has rallypoint in Village>Build tab"); var result = await switchVillageCommand.HandleAsync(new(rallypointVillageId), cancellationToken); if (result.IsFailed) return result; diff --git a/MainCore/Commands/Navigate/SwitchVillageCommand.cs b/MainCore/Commands/Navigate/SwitchVillageCommand.cs index da3ce7d4..b22ce13f 100644 --- a/MainCore/Commands/Navigate/SwitchVillageCommand.cs +++ b/MainCore/Commands/Navigate/SwitchVillageCommand.cs @@ -14,7 +14,7 @@ CancellationToken cancellationToken var villageId = command.VillageId; var villageNode = VillagePanelParser.GetVillageNode(browser.Html, villageId); - if (villageNode is null) return Skip.VillageNotFound; + if (villageNode is null) return Skip.Error.WithError("Village not found"); if (VillagePanelParser.IsActive(villageNode)) return Result.Ok(); diff --git a/MainCore/Errors/Skip.cs b/MainCore/Errors/Skip.cs index 16b9be18..243f21bc 100644 --- a/MainCore/Errors/Skip.cs +++ b/MainCore/Errors/Skip.cs @@ -2,21 +2,10 @@ { public class Skip : Error { - public Skip() : base() + private Skip() : base() { } - private Skip(string message) : base(message) - { - } - - public static Skip VillageNotFound => new("Village not found"); - public static Skip AccountLogout => new("Account is logout. Re-login now"); - - public static Skip NoRallypoint => new("No rallypoint found. Recheck & load village has rallypoint in Village>Build tab"); - public static Skip NoActiveFarmlist => new("No farmlist is active"); - public static Skip NoAdventure => new("No adventure available"); - - public static Skip OverflowNPC => new("Overflow NPC resources. Bot won't npc to save gold"); + public static Result Error => new Skip(); } } \ No newline at end of file diff --git a/MainCore/Tasks/NPCTask.cs b/MainCore/Tasks/NPCTask.cs index de310f90..696c94be 100644 --- a/MainCore/Tasks/NPCTask.cs +++ b/MainCore/Tasks/NPCTask.cs @@ -58,7 +58,7 @@ private static async ValueTask HandleAsync( }; await saveVillageSettingCommand.HandleAsync(new(task.AccountId, task.VillageId, settings), cancellationToken); logger.Warning("Disable NPC for this village."); - return new Skip(); + return Skip.Error; } return result; } @@ -69,7 +69,7 @@ private static async ValueTask HandleAsync( if (result.HasError()) { task.ExecuteAt = DateTime.Now.AddHours(5); - return new Skip(); + return Skip.Error; } return result; } diff --git a/MainCore/Tasks/UpgradeBuildingTask.cs b/MainCore/Tasks/UpgradeBuildingTask.cs index 6fd61bea..89ce9e0c 100644 --- a/MainCore/Tasks/UpgradeBuildingTask.cs +++ b/MainCore/Tasks/UpgradeBuildingTask.cs @@ -42,7 +42,7 @@ private static async ValueTask HandleAsync( task.ExecuteAt = nextExecuteErrors.Select(x => x.NextExecute).Min(); } - return new Skip(); + return Skip.Error; } logger.Information("Build {Type} to level {Level} at location {Location}", plan.Type, plan.Level, plan.Location); @@ -67,7 +67,7 @@ private static async ValueTask HandleAsync( { var time = UpgradeParser.GetTimeWhenEnoughResource(browser.Html, plan.Type); task.ExecuteAt = DateTime.Now.Add(time); - return new Skip(); + return Skip.Error; } return result; From 56da187b550cf02fa445fbd821f7c861d263842a Mon Sep 17 00:00:00 2001 From: VINAGHOST Date: Thu, 25 Sep 2025 13:41:41 +0700 Subject: [PATCH 11/11] Refactor error handling for consistency Refactored the error handling mechanism across the codebase to use `WithError` and `WithErrors` methods for detailed and extensible error messages. - Replaced specific `Stop` error methods with a generic `Stop.Error` instance, allowing customization via `WithError`. - Made `Stop` constructor private to enforce the use of `Error`. - Updated `Skip` to include a default message and support `WithErrors`. - Replaced hardcoded error messages in various files with the new `WithError` and `WithErrors` methods for better clarity. - Removed redundant error methods like `JobNotAvailable` in `UpgradeBuildingError`. - Improved error propagation in `NPCTask` and `UpgradeBuildingTask`. These changes improve code maintainability and ensure consistent, user-friendly error messages throughout the application. --- .../Behaviors/ErrorLoggingBehaviorTest.cs | 1 - MainCore/Behaviors/AccountTaskBehavior.cs | 2 +- MainCore/Commands/Misc/GetValidAccessCommand.cs | 4 ++-- .../Commands/Update/UpdateBuildingCommand.cs | 2 +- MainCore/Errors/Skip.cs | 2 +- MainCore/Errors/Stop.cs | 16 +++------------- MainCore/Errors/UpgradeBuildingError.cs | 3 --- MainCore/Tasks/NPCTask.cs | 4 ++-- MainCore/Tasks/UpgradeBuildingTask.cs | 6 +++--- 9 files changed, 13 insertions(+), 27 deletions(-) diff --git a/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs b/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs index 31b2b600..f4ce2e55 100644 --- a/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs +++ b/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs @@ -66,7 +66,6 @@ public async Task ErrorLoggingBehaviorShouldLogCorrectErrorMessage(Result result new List { new object[] { Result.Fail(Cancel.Error), "Pause button is pressed" }, - new object[] { Result.Fail(Stop.EnglishRequired("abcxyz")), "Cannot parse abcxyz. Is language English ?. Bot must stop" }, }; } } \ No newline at end of file diff --git a/MainCore/Behaviors/AccountTaskBehavior.cs b/MainCore/Behaviors/AccountTaskBehavior.cs index f4ab3745..ceb1e258 100644 --- a/MainCore/Behaviors/AccountTaskBehavior.cs +++ b/MainCore/Behaviors/AccountTaskBehavior.cs @@ -30,7 +30,7 @@ public override async ValueTask HandleAsync(TRequest request, Cancell { if (!LoginParser.IsLoginPage(_browser.Html)) { - return (TResponse)Stop.NotTravianPage; + return (TResponse)Stop.Error.WithError("Travian is not ingame nor login page. Please check browser"); } if (request is not LoginTask.Task) diff --git a/MainCore/Commands/Misc/GetValidAccessCommand.cs b/MainCore/Commands/Misc/GetValidAccessCommand.cs index b5b17acb..a81f482f 100644 --- a/MainCore/Commands/Misc/GetValidAccessCommand.cs +++ b/MainCore/Commands/Misc/GetValidAccessCommand.cs @@ -48,14 +48,14 @@ AppDbContext context } var access = await GetValidAccess(accesses); - if (access is null) return Stop.AllAccessNotWorking; + if (access is null) return Stop.Error.WithError("All accesses not working"); if (accesses.Count == 1) return access; if (ignoreSleepTime) return access; var minSleep = context.ByName(accountId, AccountSettingEnums.SleepTimeMin); var timeValid = DateTime.Now.AddMinutes(-minSleep); - if (access.LastUsed > timeValid) return Stop.LackOfAccess; + if (access.LastUsed > timeValid) return Stop.Error.WithError("Last access is reused, it may get MH's attention"); return access; } diff --git a/MainCore/Commands/Update/UpdateBuildingCommand.cs b/MainCore/Commands/Update/UpdateBuildingCommand.cs index 0f57149d..ce0eb2dd 100644 --- a/MainCore/Commands/Update/UpdateBuildingCommand.cs +++ b/MainCore/Commands/Update/UpdateBuildingCommand.cs @@ -45,7 +45,7 @@ private static Result IsValidQueueBuilding(List dtos) foreach (var strType in dtos.Select(x => x.Type)) { var resultParse = Enum.TryParse(strType, false, out BuildingEnums _); - if (!resultParse) return Stop.EnglishRequired(strType); + if (!resultParse) return Stop.Error.WithError($"Cannot parse {strType}. Is language English ?"); } return Result.Ok(); } diff --git a/MainCore/Errors/Skip.cs b/MainCore/Errors/Skip.cs index 243f21bc..4e4147a2 100644 --- a/MainCore/Errors/Skip.cs +++ b/MainCore/Errors/Skip.cs @@ -2,7 +2,7 @@ { public class Skip : Error { - private Skip() : base() + private Skip() : base("Bot skip this task") { } diff --git a/MainCore/Errors/Stop.cs b/MainCore/Errors/Stop.cs index cfe3d047..b98ad677 100644 --- a/MainCore/Errors/Stop.cs +++ b/MainCore/Errors/Stop.cs @@ -2,21 +2,11 @@ { public class Stop : Error { - public Stop() : base() + private Stop() : base("Bot must stop") { } - private Stop(string message) : base($"{message}. Bot must stop") - { - } - - public static Stop EnglishRequired(string strType) => new($"Cannot parse {strType}. Is language English ?"); - - public static Stop NotTravianPage => new($"Travian is not ingame nor login page. Please check browser"); - - public static Stop AllAccessNotWorking => new("All accesses not working"); - public static Stop LackOfAccess => new("Last access is reused, it may get MH's attention"); - - public static Stop DriverNotReady => new("Driver is not ready."); + public static Result Error => new Stop(); + public static Result DriverNotReady => Error.WithError("Driver is not ready."); } } \ No newline at end of file diff --git a/MainCore/Errors/UpgradeBuildingError.cs b/MainCore/Errors/UpgradeBuildingError.cs index f76dfa7f..ac2224f3 100644 --- a/MainCore/Errors/UpgradeBuildingError.cs +++ b/MainCore/Errors/UpgradeBuildingError.cs @@ -12,9 +12,6 @@ public static UpgradeBuildingError BuildingJobQueueEmpty public static UpgradeBuildingError BuildingJobQueueBroken => new("Building job queue is broken. No building in construct but cannot choose job"); - public static UpgradeBuildingError JobNotAvailable(string type) - => new($"{type} job is not available"); - public static UpgradeBuildingError PrerequisiteBuildingMissing(BuildingEnums prerequisiteBuilding, int level) => new($"{prerequisiteBuilding} level {level} is missing"); } diff --git a/MainCore/Tasks/NPCTask.cs b/MainCore/Tasks/NPCTask.cs index 696c94be..10fa8727 100644 --- a/MainCore/Tasks/NPCTask.cs +++ b/MainCore/Tasks/NPCTask.cs @@ -58,7 +58,7 @@ private static async ValueTask HandleAsync( }; await saveVillageSettingCommand.HandleAsync(new(task.AccountId, task.VillageId, settings), cancellationToken); logger.Warning("Disable NPC for this village."); - return Skip.Error; + return Skip.Error.WithErrors(result.Errors); } return result; } @@ -69,7 +69,7 @@ private static async ValueTask HandleAsync( if (result.HasError()) { task.ExecuteAt = DateTime.Now.AddHours(5); - return Skip.Error; + return Skip.Error.WithErrors(result.Errors); } return result; } diff --git a/MainCore/Tasks/UpgradeBuildingTask.cs b/MainCore/Tasks/UpgradeBuildingTask.cs index 89ce9e0c..aad57cff 100644 --- a/MainCore/Tasks/UpgradeBuildingTask.cs +++ b/MainCore/Tasks/UpgradeBuildingTask.cs @@ -42,7 +42,7 @@ private static async ValueTask HandleAsync( task.ExecuteAt = nextExecuteErrors.Select(x => x.NextExecute).Min(); } - return Skip.Error; + return Skip.Error.WithErrors(errors); } logger.Information("Build {Type} to level {Level} at location {Location}", plan.Type, plan.Level, plan.Location); @@ -61,13 +61,13 @@ private static async ValueTask HandleAsync( if (result.HasError()) { - return new Stop(); + return Stop.Error.WithErrors(result.Errors); } if (result.HasError()) { var time = UpgradeParser.GetTimeWhenEnoughResource(browser.Html, plan.Type); task.ExecuteAt = DateTime.Now.Add(time); - return Skip.Error; + return Skip.Error.WithErrors(result.Errors); } return result;