diff --git a/src/Poe2Trade.Bot/BotOrchestrator.cs b/src/Poe2Trade.Bot/BotOrchestrator.cs index f932b6f..b25b12f 100644 --- a/src/Poe2Trade.Bot/BotOrchestrator.cs +++ b/src/Poe2Trade.Bot/BotOrchestrator.cs @@ -28,29 +28,37 @@ public class BotOrchestrator : IAsyncDisposable private readonly long _startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); private bool _started; - public LinkManager Links { get; } public ConfigStore Store { get; } - public AppConfig Config { get; } - - public GameController Game { get; private set; } = null!; - public ScreenReader Screen { get; private set; } = null!; - public ClientLogWatcher LogWatcher { get; private set; } = null!; - public TradeMonitor TradeMonitor { get; private set; } = null!; - public InventoryManager Inventory { get; private set; } = null!; - public TradeExecutor TradeExecutor { get; private set; } = null!; - public TradeQueue TradeQueue { get; private set; } = null!; + public SavedSettings Config => Store.Settings; + public LinkManager Links { get; } + public IGameController Game { get; } + public IScreenReader Screen { get; } + public IClientLogWatcher LogWatcher { get; } + public ITradeMonitor TradeMonitor { get; } + public IInventoryManager Inventory { get; } + public TradeExecutor TradeExecutor { get; } + public TradeQueue TradeQueue { get; } private readonly Dictionary _scrapExecutors = new(); // Events public event Action? StatusUpdated; public event Action? LogMessage; // level, message - public BotOrchestrator(ConfigStore store, AppConfig config) + public BotOrchestrator(ConfigStore store, IGameController game, IScreenReader screen, + IClientLogWatcher logWatcher, ITradeMonitor tradeMonitor, + IInventoryManager inventory, TradeExecutor tradeExecutor, + TradeQueue tradeQueue, LinkManager links) { Store = store; - Config = config; + Game = game; + Screen = screen; + LogWatcher = logWatcher; + TradeMonitor = tradeMonitor; + Inventory = inventory; + TradeExecutor = tradeExecutor; + TradeQueue = tradeQueue; + Links = links; _paused = store.Settings.Paused; - Links = new LinkManager(store); } public bool IsReady => _started; @@ -139,18 +147,12 @@ public class BotOrchestrator : IAsyncDisposable public async Task Start(IReadOnlyList cliUrls) { - Screen = new ScreenReader(); - Game = new GameController(Config); - LogWatcher = new ClientLogWatcher(Config.Poe2LogPath); LogWatcher.Start(); Emit("info", "Watching Client.txt for game events"); - TradeMonitor = new TradeMonitor(Config); await TradeMonitor.Start(); Emit("info", "Browser launched"); - Inventory = new InventoryManager(Game, Screen, LogWatcher, Config); - // Warmup OCR daemon var ocrWarmup = Screen.Warmup().ContinueWith(t => { @@ -183,10 +185,8 @@ public class BotOrchestrator : IAsyncDisposable await Inventory.ClearToStash(); Emit("info", "Inventory cleared"); - // Create executors - TradeExecutor = new TradeExecutor(Game, Screen, TradeMonitor, Inventory, Config); + // Wire executor events TradeExecutor.StateChanged += _ => UpdateExecutorState(); - TradeQueue = new TradeQueue(TradeExecutor, Config); TradeQueue.TradeCompleted += () => { _tradesCompleted++; StatusUpdated?.Invoke(); }; TradeQueue.TradeFailed += () => { _tradesFailed++; StatusUpdated?.Invoke(); }; diff --git a/src/Poe2Trade.Bot/ScrapExecutor.cs b/src/Poe2Trade.Bot/ScrapExecutor.cs index 0d0978c..ba5e853 100644 --- a/src/Poe2Trade.Bot/ScrapExecutor.cs +++ b/src/Poe2Trade.Bot/ScrapExecutor.cs @@ -15,18 +15,18 @@ public class ScrapExecutor private bool _stopped; private IPage? _activePage; private PostAction _postAction = PostAction.Salvage; - private readonly GameController _game; - private readonly ScreenReader _screen; - private readonly TradeMonitor _tradeMonitor; - private readonly InventoryManager _inventory; - private readonly AppConfig _config; + private readonly IGameController _game; + private readonly IScreenReader _screen; + private readonly ITradeMonitor _tradeMonitor; + private readonly IInventoryManager _inventory; + private readonly SavedSettings _config; public event Action? StateChanged; public event Action? ItemBought; public event Action? ItemFailed; - public ScrapExecutor(GameController game, ScreenReader screen, TradeMonitor tradeMonitor, - InventoryManager inventory, AppConfig config) + public ScrapExecutor(IGameController game, IScreenReader screen, ITradeMonitor tradeMonitor, + IInventoryManager inventory, SavedSettings config) { _game = game; _screen = screen; @@ -109,7 +109,7 @@ public class ScrapExecutor if (items.Count == 0) { Log.Information("No items after refresh, waiting..."); - await Helpers.Sleep(5000); + await Helpers.Sleep(Delays.EmptyRefreshWait); if (_stopped) break; items = await RefreshPage(page); } @@ -124,34 +124,8 @@ public class ScrapExecutor { try { - var alreadyAtSeller = !_inventory.IsAtOwnHideout - && !string.IsNullOrEmpty(item.Account) - && item.Account == _inventory.SellerAccount; - - if (alreadyAtSeller) - { - Log.Information("Already at seller hideout, skipping travel"); - } - else - { - SetState(ScrapState.Traveling); - var arrived = await _inventory.WaitForAreaTransition( - _config.TravelTimeoutMs, - async () => - { - if (!await _tradeMonitor.ClickTravelToHideout(page, item.Id)) - throw new Exception("Failed to click Travel to Hideout"); - }); - if (!arrived) - { - Log.Error("Timed out waiting for hideout arrival: {ItemId}", item.Id); - SetState(ScrapState.Failed); - return false; - } - _inventory.SetLocation(false, item.Account); - await _game.FocusGame(); - await Helpers.Sleep(1500); - } + if (!await TravelToSellerIfNeeded(page, item)) + return false; SetState(ScrapState.Buying); var sellerLayout = GridLayouts.Seller; @@ -176,6 +150,38 @@ public class ScrapExecutor } } + private async Task TravelToSellerIfNeeded(IPage page, TradeItem item) + { + var alreadyAtSeller = !_inventory.IsAtOwnHideout + && !string.IsNullOrEmpty(item.Account) + && item.Account == _inventory.SellerAccount; + + if (alreadyAtSeller) + { + Log.Information("Already at seller hideout, skipping travel"); + return true; + } + + SetState(ScrapState.Traveling); + var arrived = await _inventory.WaitForAreaTransition( + _config.TravelTimeoutMs, + async () => + { + if (!await _tradeMonitor.ClickTravelToHideout(page, item.Id)) + throw new Exception("Failed to click Travel to Hideout"); + }); + if (!arrived) + { + Log.Error("Timed out waiting for hideout arrival: {ItemId}", item.Id); + SetState(ScrapState.Failed); + return false; + } + _inventory.SetLocation(false, item.Account); + await _game.FocusGame(); + await Helpers.Sleep(Delays.PostTravel); + return true; + } + private async Task ProcessItems() { try @@ -210,12 +216,15 @@ public class ScrapExecutor items.Add(TradeMonitor.ParseTradeItem(r)); } } - catch { } + catch (Exception ex) + { + Log.Debug(ex, "Non-JSON trade response"); + } } page.Response += OnResponse; await page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.NetworkIdle }); - await Helpers.Sleep(2000); + await Helpers.Sleep(Delays.PageLoad); page.Response -= OnResponse; return items; } diff --git a/src/Poe2Trade.Bot/TradeExecutor.cs b/src/Poe2Trade.Bot/TradeExecutor.cs index e4fa788..b8b2991 100644 --- a/src/Poe2Trade.Bot/TradeExecutor.cs +++ b/src/Poe2Trade.Bot/TradeExecutor.cs @@ -11,16 +11,16 @@ namespace Poe2Trade.Bot; public class TradeExecutor { private TradeState _state = TradeState.Idle; - private readonly GameController _game; - private readonly ScreenReader _screen; - private readonly TradeMonitor _tradeMonitor; - private readonly InventoryManager _inventory; - private readonly AppConfig _config; + private readonly IGameController _game; + private readonly IScreenReader _screen; + private readonly ITradeMonitor _tradeMonitor; + private readonly IInventoryManager _inventory; + private readonly SavedSettings _config; public event Action? StateChanged; - public TradeExecutor(GameController game, ScreenReader screen, TradeMonitor tradeMonitor, - InventoryManager inventory, AppConfig config) + public TradeExecutor(IGameController game, IScreenReader screen, ITradeMonitor tradeMonitor, + IInventoryManager inventory, SavedSettings config) { _game = game; _screen = screen; @@ -48,71 +48,24 @@ public class TradeExecutor try { - // Step 1: Travel to seller hideout - SetState(TradeState.Traveling); - Log.Information("Clicking Travel to Hideout for {SearchId}...", trade.SearchId); - - var arrived = await _inventory.WaitForAreaTransition( - _config.TravelTimeoutMs, - async () => - { - if (!await _tradeMonitor.ClickTravelToHideout(page, trade.ItemIds[0])) - throw new Exception("Failed to click Travel to Hideout"); - }); - if (!arrived) - { - Log.Error("Timed out waiting for hideout arrival"); - SetState(TradeState.Failed); + if (!await TravelToSeller(page, trade)) return false; - } - SetState(TradeState.InSellersHideout); - _inventory.SetLocation(false); - Log.Information("Arrived at seller hideout"); - - // Step 2: Focus game and find stash - await _game.FocusGame(); - await Helpers.Sleep(1500); - - var angePos = await _inventory.FindAndClickNameplate("Ange"); - if (angePos == null) - Log.Warning("Could not find Ange nameplate, trying Stash directly"); - else - await Helpers.Sleep(1000); - - var stashPos = await _inventory.FindAndClickNameplate("Stash"); - if (stashPos == null) - { - Log.Error("Could not find Stash in seller hideout"); - SetState(TradeState.Failed); + if (!await FindSellerStash()) return false; - } - await Helpers.Sleep(1000); - // Step 3: Scan stash and buy SetState(TradeState.ScanningStash); await ScanAndBuyItems(); - // Step 4: Wait for more SetState(TradeState.WaitingForMore); Log.Information("Waiting {Ms}ms for more items...", _config.WaitForMoreItemsMs); await Helpers.Sleep(_config.WaitForMoreItemsMs); await ScanAndBuyItems(); - // Step 5: Go home - SetState(TradeState.GoingHome); - await _game.FocusGame(); - await Helpers.Sleep(300); + await ReturnHome(); - var home = await _inventory.WaitForAreaTransition( - _config.TravelTimeoutMs, () => _game.GoToHideout()); - if (!home) Log.Warning("Timed out going home"); - - _inventory.SetLocation(true); - - // Step 6: Store items SetState(TradeState.InHideout); - await Helpers.Sleep(1000); + await Helpers.Sleep(Delays.PostStashOpen); await _inventory.ProcessInventory(); SetState(TradeState.Idle); @@ -122,25 +75,90 @@ public class TradeExecutor { Log.Error(ex, "Trade execution failed"); SetState(TradeState.Failed); - - try - { - await _game.FocusGame(); - await _game.PressEscape(); - await Helpers.Sleep(500); - await _game.GoToHideout(); - } - catch { /* best-effort recovery */ } - + await RecoverFromError(); SetState(TradeState.Idle); return false; } } + private async Task TravelToSeller(IPage page, TradeInfo trade) + { + SetState(TradeState.Traveling); + Log.Information("Clicking Travel to Hideout for {SearchId}...", trade.SearchId); + + var arrived = await _inventory.WaitForAreaTransition( + _config.TravelTimeoutMs, + async () => + { + if (!await _tradeMonitor.ClickTravelToHideout(page, trade.ItemIds[0])) + throw new Exception("Failed to click Travel to Hideout"); + }); + if (!arrived) + { + Log.Error("Timed out waiting for hideout arrival"); + SetState(TradeState.Failed); + return false; + } + + SetState(TradeState.InSellersHideout); + _inventory.SetLocation(false); + Log.Information("Arrived at seller hideout"); + return true; + } + + private async Task FindSellerStash() + { + await _game.FocusGame(); + await Helpers.Sleep(Delays.PostTravel); + + var angePos = await _inventory.FindAndClickNameplate("Ange"); + if (angePos == null) + Log.Warning("Could not find Ange nameplate, trying Stash directly"); + else + await Helpers.Sleep(Delays.PostStashOpen); + + var stashPos = await _inventory.FindAndClickNameplate("Stash"); + if (stashPos == null) + { + Log.Error("Could not find Stash in seller hideout"); + SetState(TradeState.Failed); + return false; + } + await Helpers.Sleep(Delays.PostStashOpen); + return true; + } + + private async Task ReturnHome() + { + SetState(TradeState.GoingHome); + await _game.FocusGame(); + await Helpers.Sleep(Delays.PostFocus); + + var home = await _inventory.WaitForAreaTransition( + _config.TravelTimeoutMs, () => _game.GoToHideout()); + if (!home) Log.Warning("Timed out going home"); + + _inventory.SetLocation(true); + } + + private async Task RecoverFromError() + { + try + { + await _game.FocusGame(); + await _game.PressEscape(); + await Helpers.Sleep(Delays.PostEscape); + await _game.GoToHideout(); + } + catch (Exception ex) + { + Log.Debug(ex, "Recovery failed"); + } + } + private async Task ScanAndBuyItems() { - var stashRegion = new Region(20, 140, 630, 750); - var stashText = await _screen.ReadRegionText(stashRegion); + var stashText = await _screen.ReadRegionText(GridLayouts.SellerStashOcr); Log.Information("Stash OCR: {Text}", stashText.Length > 200 ? stashText[..200] : stashText); SetState(TradeState.Buying); } diff --git a/src/Poe2Trade.Bot/TradeQueue.cs b/src/Poe2Trade.Bot/TradeQueue.cs index 1f66d4a..134f3b0 100644 --- a/src/Poe2Trade.Bot/TradeQueue.cs +++ b/src/Poe2Trade.Bot/TradeQueue.cs @@ -7,10 +7,10 @@ public class TradeQueue { private readonly Queue _queue = new(); private readonly TradeExecutor _executor; - private readonly AppConfig _config; + private readonly SavedSettings _config; private bool _processing; - public TradeQueue(TradeExecutor executor, AppConfig config) + public TradeQueue(TradeExecutor executor, SavedSettings config) { _executor = executor; _config = config; diff --git a/src/Poe2Trade.Core/AppConfig.cs b/src/Poe2Trade.Core/AppConfig.cs deleted file mode 100644 index bfd881b..0000000 --- a/src/Poe2Trade.Core/AppConfig.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace Poe2Trade.Core; - -public class AppConfig -{ - public List TradeUrls { get; set; } = []; - public string Poe2LogPath { get; set; } = @"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\logs\Client.txt"; - public string Poe2WindowTitle { get; set; } = "Path of Exile 2"; - public string BrowserUserDataDir { get; set; } = "./browser-data"; - public int TravelTimeoutMs { get; set; } = 15000; - public int StashScanTimeoutMs { get; set; } = 10000; - public int WaitForMoreItemsMs { get; set; } = 20000; - public int BetweenTradesDelayMs { get; set; } = 5000; - - public static AppConfig Load(string? configPath = null) - { - var builder = new ConfigurationBuilder(); - var path = configPath ?? "appsettings.json"; - if (File.Exists(path)) - { - builder.AddJsonFile(path, optional: true); - } - var configuration = builder.Build(); - var config = new AppConfig(); - configuration.Bind(config); - return config; - } -} diff --git a/src/Poe2Trade.Core/Delays.cs b/src/Poe2Trade.Core/Delays.cs new file mode 100644 index 0000000..31632b2 --- /dev/null +++ b/src/Poe2Trade.Core/Delays.cs @@ -0,0 +1,12 @@ +namespace Poe2Trade.Core; + +public static class Delays +{ + public const int PostFocus = 300; + public const int PostTravel = 1500; + public const int PostStashOpen = 1000; + public const int ClickInterval = 150; + public const int PostEscape = 500; + public const int PageLoad = 2000; + public const int EmptyRefreshWait = 5000; +} diff --git a/src/Poe2Trade.Core/Poe2Trade.Core.csproj b/src/Poe2Trade.Core/Poe2Trade.Core.csproj index e8be0c4..5aff918 100644 --- a/src/Poe2Trade.Core/Poe2Trade.Core.csproj +++ b/src/Poe2Trade.Core/Poe2Trade.Core.csproj @@ -8,9 +8,6 @@ - - - - + diff --git a/src/Poe2Trade.Game/GameController.cs b/src/Poe2Trade.Game/GameController.cs index 8b338cb..a37cda8 100644 --- a/src/Poe2Trade.Game/GameController.cs +++ b/src/Poe2Trade.Game/GameController.cs @@ -3,12 +3,12 @@ using Serilog; namespace Poe2Trade.Game; -public class GameController +public class GameController : IGameController { private readonly WindowManager _windowManager; private readonly InputSender _input; - public GameController(AppConfig config) + public GameController(SavedSettings config) { _windowManager = new WindowManager(config.Poe2WindowTitle); _input = new InputSender(); diff --git a/src/Poe2Trade.Game/IGameController.cs b/src/Poe2Trade.Game/IGameController.cs new file mode 100644 index 0000000..b23fc94 --- /dev/null +++ b/src/Poe2Trade.Game/IGameController.cs @@ -0,0 +1,22 @@ +namespace Poe2Trade.Game; + +public interface IGameController +{ + Task FocusGame(); + bool IsGameFocused(); + RECT? GetWindowRect(); + Task SendChat(string message); + Task SendChatViaPaste(string message); + Task GoToHideout(); + Task CtrlLeftClickAt(int x, int y); + Task CtrlRightClickAt(int x, int y); + Task LeftClickAt(int x, int y); + Task RightClickAt(int x, int y); + Task MoveMouseTo(int x, int y); + void MoveMouseInstant(int x, int y); + Task MoveMouseFast(int x, int y); + Task PressEscape(); + Task OpenInventory(); + Task HoldCtrl(); + Task ReleaseCtrl(); +} diff --git a/src/Poe2Trade.Inventory/IInventoryManager.cs b/src/Poe2Trade.Inventory/IInventoryManager.cs new file mode 100644 index 0000000..dae69d5 --- /dev/null +++ b/src/Poe2Trade.Inventory/IInventoryManager.cs @@ -0,0 +1,20 @@ +using Poe2Trade.Core; + +namespace Poe2Trade.Inventory; + +public interface IInventoryManager +{ + InventoryTracker Tracker { get; } + bool IsAtOwnHideout { get; } + string SellerAccount { get; } + void SetLocation(bool atHome, string? seller = null); + Task ScanInventory(PostAction defaultAction = PostAction.Stash); + Task ClearToStash(); + Task EnsureAtOwnHideout(); + Task ProcessInventory(); + Task WaitForAreaTransition(int timeoutMs, Func? triggerAction = null); + Task<(int X, int Y)?> FindAndClickNameplate(string name, int maxRetries = 3, int retryDelayMs = 1000); + Task DepositItemsToStash(List items); + Task SalvageItems(List items); + (bool[,] Grid, List Items, int Free) GetInventoryState(); +} diff --git a/src/Poe2Trade.Inventory/InventoryManager.cs b/src/Poe2Trade.Inventory/InventoryManager.cs index de168ff..00a4372 100644 --- a/src/Poe2Trade.Inventory/InventoryManager.cs +++ b/src/Poe2Trade.Inventory/InventoryManager.cs @@ -6,7 +6,7 @@ using Serilog; namespace Poe2Trade.Inventory; -public class InventoryManager +public class InventoryManager : IInventoryManager { private static readonly string SalvageTemplate = Path.Combine("assets", "salvage.png"); @@ -14,15 +14,15 @@ public class InventoryManager private bool _atOwnHideout = true; private string _sellerAccount = ""; - private readonly GameController _game; - private readonly ScreenReader _screen; - private readonly ClientLogWatcher _logWatcher; - private readonly AppConfig _config; + private readonly IGameController _game; + private readonly IScreenReader _screen; + private readonly IClientLogWatcher _logWatcher; + private readonly SavedSettings _config; public bool IsAtOwnHideout => _atOwnHideout; public string SellerAccount => _sellerAccount; - public InventoryManager(GameController game, ScreenReader screen, ClientLogWatcher logWatcher, AppConfig config) + public InventoryManager(IGameController game, IScreenReader screen, IClientLogWatcher logWatcher, SavedSettings config) { _game = game; _screen = screen; @@ -40,7 +40,7 @@ public class InventoryManager { Log.Information("Scanning inventory..."); await _game.FocusGame(); - await Helpers.Sleep(300); + await Helpers.Sleep(Delays.PostFocus); await _game.OpenInventory(); var result = await _screen.Grid.Scan("inventory"); @@ -54,7 +54,7 @@ public class InventoryManager Tracker.InitFromScan(cells, result.Items, defaultAction); await _game.PressEscape(); - await Helpers.Sleep(300); + await Helpers.Sleep(Delays.PostFocus); } public async Task ClearToStash() @@ -83,7 +83,7 @@ public class InventoryManager } await _game.FocusGame(); - await Helpers.Sleep(300); + await Helpers.Sleep(Delays.PostFocus); var arrived = await WaitForAreaTransition(_config.TravelTimeoutMs, () => _game.GoToHideout()); if (!arrived) @@ -92,7 +92,7 @@ public class InventoryManager return false; } - await Helpers.Sleep(1500); + await Helpers.Sleep(Delays.PostTravel); _atOwnHideout = true; _sellerAccount = ""; return true; @@ -108,23 +108,13 @@ public class InventoryManager Log.Error("Could not find Stash nameplate"); return; } - await Helpers.Sleep(1000); + await Helpers.Sleep(Delays.PostStashOpen); - var inventoryLayout = GridLayouts.Inventory; Log.Information("Depositing {Count} items to stash", items.Count); - - await _game.HoldCtrl(); - foreach (var item in items) - { - var center = _screen.Grid.GetCellCenter(inventoryLayout, item.Row, item.Col); - await _game.LeftClickAt(center.X, center.Y); - await Helpers.Sleep(150); - } - await _game.ReleaseCtrl(); - await Helpers.Sleep(500); + await CtrlClickItems(items, GridLayouts.Inventory); await _game.PressEscape(); - await Helpers.Sleep(500); + await Helpers.Sleep(Delays.PostEscape); Log.Information("Items deposited to stash"); } @@ -138,35 +128,38 @@ public class InventoryManager Log.Error("Could not find Salvage nameplate"); return false; } - await Helpers.Sleep(1000); + await Helpers.Sleep(Delays.PostStashOpen); var salvageBtn = await _screen.TemplateMatch(SalvageTemplate); if (salvageBtn != null) { await _game.LeftClickAt(salvageBtn.X, salvageBtn.Y); - await Helpers.Sleep(500); + await Helpers.Sleep(Delays.PostEscape); } else { Log.Warning("Could not find salvage button via template match"); } - var inventoryLayout = GridLayouts.Inventory; Log.Information("Salvaging {Count} inventory items", items.Count); + await CtrlClickItems(items, GridLayouts.Inventory); + await _game.PressEscape(); + await Helpers.Sleep(Delays.PostEscape); + return true; + } + + private async Task CtrlClickItems(List items, GridLayout layout, int clickDelayMs = Delays.ClickInterval) + { await _game.HoldCtrl(); foreach (var item in items) { - var center = _screen.Grid.GetCellCenter(inventoryLayout, item.Row, item.Col); + var center = _screen.Grid.GetCellCenter(layout, item.Row, item.Col); await _game.LeftClickAt(center.X, center.Y); - await Helpers.Sleep(150); + await Helpers.Sleep(clickDelayMs); } await _game.ReleaseCtrl(); - await Helpers.Sleep(500); - - await _game.PressEscape(); - await Helpers.Sleep(500); - return true; + await Helpers.Sleep(Delays.PostEscape); } public async Task ProcessInventory() @@ -201,7 +194,7 @@ public class InventoryManager catch (Exception ex) { Log.Error(ex, "Inventory processing failed"); - try { await _game.PressEscape(); await Helpers.Sleep(300); } catch { } + try { await _game.PressEscape(); await Helpers.Sleep(Delays.PostFocus); } catch { } Tracker.Clear(); } } diff --git a/src/Poe2Trade.Screen/GridReader.cs b/src/Poe2Trade.Screen/GridReader.cs index a2b3885..8e508a2 100644 --- a/src/Poe2Trade.Screen/GridReader.cs +++ b/src/Poe2Trade.Screen/GridReader.cs @@ -68,6 +68,7 @@ public static class GridLayouts public static readonly GridLayout Inventory = All["inventory"]; public static readonly GridLayout Seller = All["seller"]; + public static readonly Region SellerStashOcr = new(20, 140, 630, 750); } public class GridReader diff --git a/src/Poe2Trade.Screen/IScreenReader.cs b/src/Poe2Trade.Screen/IScreenReader.cs new file mode 100644 index 0000000..3293991 --- /dev/null +++ b/src/Poe2Trade.Screen/IScreenReader.cs @@ -0,0 +1,22 @@ +using Poe2Trade.Core; + +namespace Poe2Trade.Screen; + +public interface IScreenReader : IDisposable +{ + GridReader Grid { get; } + Task Warmup(); + Task CaptureScreen(); + Task CaptureRegion(Region region); + Task Ocr(Region? region = null, string? preprocess = null); + Task<(int X, int Y)?> FindTextOnScreen(string searchText, bool fuzzy = false); + Task ReadFullScreen(); + Task<(int X, int Y)?> FindTextInRegion(Region region, string searchText); + Task ReadRegionText(Region region); + Task CheckForText(Region region, string searchText); + Task Snapshot(); + Task DiffOcr(string? savePath = null, Region? region = null); + Task TemplateMatch(string templatePath, Region? region = null); + Task SaveScreenshot(string path); + Task SaveRegion(Region region, string path); +} diff --git a/src/Poe2Trade.Screen/ScreenReader.cs b/src/Poe2Trade.Screen/ScreenReader.cs index d5f2f7f..3b72acc 100644 --- a/src/Poe2Trade.Screen/ScreenReader.cs +++ b/src/Poe2Trade.Screen/ScreenReader.cs @@ -4,7 +4,7 @@ using Serilog; namespace Poe2Trade.Screen; -public class ScreenReader : IDisposable +public class ScreenReader : IScreenReader { private readonly DiffCropHandler _diffCrop = new(); private readonly GridHandler _gridHandler = new(); diff --git a/src/Poe2Trade.Trade/ITradeMonitor.cs b/src/Poe2Trade.Trade/ITradeMonitor.cs new file mode 100644 index 0000000..68125e5 --- /dev/null +++ b/src/Poe2Trade.Trade/ITradeMonitor.cs @@ -0,0 +1,15 @@ +using Microsoft.Playwright; +using Poe2Trade.Core; + +namespace Poe2Trade.Trade; + +public interface ITradeMonitor : IAsyncDisposable +{ + event Action, IPage>? NewListings; + Task Start(string? dashboardUrl = null); + Task AddSearch(string tradeUrl); + Task PauseSearch(string searchId); + Task ClickTravelToHideout(IPage page, string? itemId = null); + Task<(IPage Page, List Items)> OpenScrapPage(string tradeUrl); + string ExtractSearchId(string url); +} diff --git a/src/Poe2Trade.Trade/TradeMonitor.cs b/src/Poe2Trade.Trade/TradeMonitor.cs index 696b20c..cea1234 100644 --- a/src/Poe2Trade.Trade/TradeMonitor.cs +++ b/src/Poe2Trade.Trade/TradeMonitor.cs @@ -5,12 +5,12 @@ using Serilog; namespace Poe2Trade.Trade; -public class TradeMonitor : IAsyncDisposable +public class TradeMonitor : ITradeMonitor { private IBrowserContext? _context; private readonly Dictionary _pages = new(); private readonly HashSet _pausedSearches = new(); - private readonly AppConfig _config; + private readonly SavedSettings _config; private const string StealthScript = """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); @@ -38,7 +38,7 @@ public class TradeMonitor : IAsyncDisposable public event Action, IPage>? NewListings; - public TradeMonitor(AppConfig config) + public TradeMonitor(SavedSettings config) { _config = config; } @@ -96,7 +96,7 @@ public class TradeMonitor : IAsyncDisposable _pages[searchId] = page; await page.GotoAsync(tradeUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); - await Helpers.Sleep(2000); + await Helpers.Sleep(Delays.PageLoad); page.WebSocket += (_, ws) => HandleWebSocket(ws, searchId, page); @@ -177,11 +177,11 @@ public class TradeMonitor : IAsyncDisposable items.Add(ParseTradeItem(r)); } } - catch { /* Response may not be JSON */ } + catch (Exception ex) { Log.Debug(ex, "Non-JSON trade response"); } }; await page.GotoAsync(tradeUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); - await Helpers.Sleep(2000); + await Helpers.Sleep(Delays.PageLoad); Log.Information("Scrap page opened: {Url} ({Count} items)", tradeUrl, items.Count); return (page, items); } @@ -259,7 +259,7 @@ public class TradeMonitor : IAsyncDisposable } } } - catch { /* Not all frames are JSON */ } + catch (Exception ex) { Log.Debug(ex, "Non-JSON WebSocket frame"); } }; ws.Close += (_, _) => Log.Warning("WebSocket closed: {SearchId}", searchId); diff --git a/src/Poe2Trade.Ui/App.axaml.cs b/src/Poe2Trade.Ui/App.axaml.cs index 793152b..bd065e3 100644 --- a/src/Poe2Trade.Ui/App.axaml.cs +++ b/src/Poe2Trade.Ui/App.axaml.cs @@ -1,8 +1,14 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; using Poe2Trade.Bot; using Poe2Trade.Core; +using Poe2Trade.Game; +using Poe2Trade.GameLog; +using Poe2Trade.Inventory; +using Poe2Trade.Screen; +using Poe2Trade.Trade; using Poe2Trade.Ui.ViewModels; using Poe2Trade.Ui.Views; @@ -19,15 +25,39 @@ public partial class App : Application { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var store = new ConfigStore(); - var config = AppConfig.Load(); - var bot = new BotOrchestrator(store, config); + var services = new ServiceCollection(); - var mainVm = new MainWindowViewModel(bot) - { - DebugVm = new DebugViewModel(bot), - SettingsVm = new SettingsViewModel(bot) - }; + // Config + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService().Settings); + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + new ClientLogWatcher(sp.GetRequiredService().Poe2LogPath)); + services.AddSingleton(); + services.AddSingleton(); + + // Bot + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // ViewModels + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + var provider = services.BuildServiceProvider(); + + var store = provider.GetRequiredService(); + var bot = provider.GetRequiredService(); + + var mainVm = provider.GetRequiredService(); + mainVm.DebugVm = provider.GetRequiredService(); + mainVm.SettingsVm = provider.GetRequiredService(); var window = new MainWindow { DataContext = mainVm }; window.SetConfigStore(store); diff --git a/src/Poe2Trade.Ui/Poe2Trade.Ui.csproj b/src/Poe2Trade.Ui/Poe2Trade.Ui.csproj index f7ebf31..bd6901d 100644 --- a/src/Poe2Trade.Ui/Poe2Trade.Ui.csproj +++ b/src/Poe2Trade.Ui/Poe2Trade.Ui.csproj @@ -11,9 +11,15 @@ + + + + + + diff --git a/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs b/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs index 1276f21..5e8dbeb 100644 --- a/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs +++ b/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs @@ -101,7 +101,7 @@ public partial class MainWindowViewModel : ObservableObject { try { - await _bot.Start(_bot.Config.TradeUrls); + await _bot.Start([]); IsStarted = true; } catch (Exception ex) diff --git a/src/Poe2Trade.Ui/ViewModels/SettingsViewModel.cs b/src/Poe2Trade.Ui/ViewModels/SettingsViewModel.cs index 4a6a67e..31d4b2d 100644 --- a/src/Poe2Trade.Ui/ViewModels/SettingsViewModel.cs +++ b/src/Poe2Trade.Ui/ViewModels/SettingsViewModel.cs @@ -46,13 +46,6 @@ public partial class SettingsViewModel : ObservableObject s.BetweenTradesDelayMs = (int)(BetweenTradesDelayMs ?? 5000); }); - _bot.Config.Poe2LogPath = Poe2LogPath; - _bot.Config.Poe2WindowTitle = WindowTitle; - _bot.Config.TravelTimeoutMs = (int)(TravelTimeoutMs ?? 15000); - _bot.Config.StashScanTimeoutMs = (int)(StashScanTimeoutMs ?? 10000); - _bot.Config.WaitForMoreItemsMs = (int)(WaitForMoreItemsMs ?? 20000); - _bot.Config.BetweenTradesDelayMs = (int)(BetweenTradesDelayMs ?? 5000); - IsSaved = true; }