diff --git a/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs b/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs index ebd8d0d4..f4ce2e55 100644 --- a/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs +++ b/MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs @@ -66,8 +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" }, }; } } \ No newline at end of file diff --git a/MainCore/Behaviors/AccountTaskBehavior.cs b/MainCore/Behaviors/AccountTaskBehavior.cs index 984ee7c0..ceb1e258 100644 --- a/MainCore/Behaviors/AccountTaskBehavior.cs +++ b/MainCore/Behaviors/AccountTaskBehavior.cs @@ -30,14 +30,14 @@ 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) { _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/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..b6bf3908 100644 --- a/MainCore/Commands/Features/LoginCommand.cs +++ b/MainCore/Commands/Features/LoginCommand.cs @@ -13,22 +13,25 @@ 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); + + 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.Input(By.XPath(passwordNode.XPath), password, 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.Click(By.XPath(buttonNode.XPath), 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; + result = await browser.WaitPageChanged("dorf", cancellationToken); if (result.IsFailed) return result; 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..85750aac 100644 --- a/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs +++ b/MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs @@ -13,12 +13,19 @@ 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.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).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(); @@ -26,13 +33,8 @@ 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; - return Result.Ok(); } } 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..b169ada7 100644 --- a/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs +++ b/MainCore/Commands/Features/StartFarmList/StartActiveFarmListCommand.cs @@ -18,14 +18,14 @@ 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) { - 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/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/Features/TrainTroop/TrainTroopCommand.cs b/MainCore/Commands/Features/TrainTroop/TrainTroopCommand.cs index 589154a5..e09d3368 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"); + (_, isFailed, element, errors) = await browser.GetElement(doc => TrainTroopParser.GetTrainButton(doc), cancellationToken); + if (isFailed) return Result.Fail(errors); - 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..f4904a8d 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,32 @@ 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(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"); - result = await browser.Click(By.XPath(watchButton.XPath), cancellationToken); + (_, 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"); + + 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).WithError("Failed to find [Play ads video] button"); - result = await browser.Click(By.XPath(node.XPath), cancellationToken); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; driver.SwitchTo().DefaultContent(); @@ -184,12 +190,16 @@ 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).WithError("Failed to find [Play ads video] button"); + + 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; @@ -198,12 +208,16 @@ static bool videoFeatureShown(IWebDriver driver) var dontShowThisAgain = browser.Html.GetElementbyId("dontShowThisAgain"); if (dontShowThisAgain is not null) { - result = await browser.Click(By.XPath(dontShowThisAgain.XPath), cancellationToken); + (_, 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"); - result = await browser.Click(By.XPath(okButton.XPath), cancellationToken); + (_, 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; } @@ -214,10 +228,10 @@ 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 (_, isFailed, element, errors) = await browser.GetElement(doc => UpgradeParser.GetUpgradeButton(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.WaitPageChanged("dorf", cancellationToken); @@ -232,15 +246,14 @@ 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/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/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/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/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; diff --git a/MainCore/Commands/Navigate/SwitchTabCommand.cs b/MainCore/Commands/Navigate/SwitchTabCommand.cs index b6223061..aa4d24a0 100644 --- a/MainCore/Commands/Navigate/SwitchTabCommand.cs +++ b/MainCore/Commands/Navigate/SwitchTabCommand.cs @@ -20,26 +20,27 @@ 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).WithError($"Failed to find tab element [{tab.XPath}]"); + + Result result; + result = await browser.Click(element, cancellationToken); + if (result.IsFailed) return result; + 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; } - 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..b22ce13f 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.Error.WithError("Village not found"); - 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..53933694 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).WithError($"Failed to find [building at #{location}]"); + + 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; } } @@ -47,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); @@ -59,12 +62,14 @@ 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; } + + 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 e04a3c8d..cde5acf4 100644 --- a/MainCore/Commands/Navigate/ToDorfCommand.cs +++ b/MainCore/Commands/Navigate/ToDorfCommand.cs @@ -26,14 +26,16 @@ 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); + result = await browser.Click(element, cancellationToken); if (result.IsFailed) return result; - result = await browser.WaitPageChanged($"dorf{dorf}", cancellationToken); + + result = await browser.WaitPageChanged($"dorf{dorf}.php", cancellationToken); if (result.IsFailed) return result; + return Result.Ok(); } 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/Retry.cs b/MainCore/Errors/Retry.cs index 43effb8e..b57412f5 100644 --- a/MainCore/Errors/Retry.cs +++ b/MainCore/Errors/Retry.cs @@ -2,22 +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 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/Errors/Skip.cs b/MainCore/Errors/Skip.cs index 16b9be18..4e4147a2 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("Bot skip this task") { } - 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/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/Services/ChromeBrowser.cs b/MainCore/Services/ChromeBrowser.cs index e54bc61f..0f304bc2 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,60 @@ IWebElement wait() } catch (WebDriverTimeoutException ex) { - return Retry.BrowserTimeout(ex.Message); + var error = Retry.Error.WithError(ex.Message); + if (expression is not null) return error.WithError(expression); + return error; } } - public async Task Click(By by, CancellationToken cancellationToken) + 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) + { + var error = Retry.Error.WithError(ex.Message); + if (expression is not null) return error.WithError(expression); + return error; + } + } + + 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); @@ -192,7 +223,22 @@ public async Task ExecuteJsScript(string javascript) return Result.Ok(); } - public async Task Wait(Predicate condition, CancellationToken cancellationToken) + public async Task WaitPageChanged(string url, CancellationToken cancellationToken) + { + 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 => + { + 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() { @@ -209,26 +255,13 @@ void wait() } catch (WebDriverTimeoutException ex) { - return Retry.BrowserTimeout(ex.Message); + var error = Retry.Error.WithError(ex.Message); + if (expression is not null) return error.WithError(expression); + return error; } 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..daa41067 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,17 @@ 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); @@ -27,12 +32,8 @@ 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 Wait(Predicate condition, CancellationToken cancellationToken, [CallerArgumentExpression("condition")] string? expression = null); - Task WaitPageLoaded(CancellationToken cancellationToken); + Task WaitPageChanged(string url, CancellationToken cancellationToken); } } \ 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(); diff --git a/MainCore/Tasks/NPCTask.cs b/MainCore/Tasks/NPCTask.cs index de310f90..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 new Skip(); + 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 new Skip(); + return Skip.Error.WithErrors(result.Errors); } return result; } diff --git a/MainCore/Tasks/UpgradeBuildingTask.cs b/MainCore/Tasks/UpgradeBuildingTask.cs index 6fd61bea..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 new Skip(); + 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 new Skip(); + return Skip.Error.WithErrors(result.Errors); } return result; 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)