refactoring

This commit is contained in:
Boki 2026-02-13 08:42:46 -05:00
parent 696fd07e86
commit 50d32abd49
20 changed files with 334 additions and 225 deletions

View file

@ -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<string, ScrapExecutor> _scrapExecutors = new();
// Events
public event Action? StatusUpdated;
public event Action<string, string>? 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<string> 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(); };

View file

@ -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<ScrapState>? 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);
if (!await TravelToSellerIfNeeded(page, item))
return false;
}
_inventory.SetLocation(false, item.Account);
await _game.FocusGame();
await Helpers.Sleep(1500);
}
SetState(ScrapState.Buying);
var sellerLayout = GridLayouts.Seller;
@ -176,6 +150,38 @@ public class ScrapExecutor
}
}
private async Task<bool> 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;
}

View file

@ -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<TradeState>? 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,7 +48,41 @@ public class TradeExecutor
try
{
// Step 1: Travel to seller hideout
if (!await TravelToSeller(page, trade))
return false;
if (!await FindSellerStash())
return false;
SetState(TradeState.ScanningStash);
await ScanAndBuyItems();
SetState(TradeState.WaitingForMore);
Log.Information("Waiting {Ms}ms for more items...", _config.WaitForMoreItemsMs);
await Helpers.Sleep(_config.WaitForMoreItemsMs);
await ScanAndBuyItems();
await ReturnHome();
SetState(TradeState.InHideout);
await Helpers.Sleep(Delays.PostStashOpen);
await _inventory.ProcessInventory();
SetState(TradeState.Idle);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Trade execution failed");
SetState(TradeState.Failed);
await RecoverFromError();
SetState(TradeState.Idle);
return false;
}
}
private async Task<bool> TravelToSeller(IPage page, TradeInfo trade)
{
SetState(TradeState.Traveling);
Log.Information("Clicking Travel to Hideout for {SearchId}...", trade.SearchId);
@ -69,16 +103,19 @@ public class TradeExecutor
SetState(TradeState.InSellersHideout);
_inventory.SetLocation(false);
Log.Information("Arrived at seller hideout");
return true;
}
// Step 2: Focus game and find stash
private async Task<bool> FindSellerStash()
{
await _game.FocusGame();
await Helpers.Sleep(1500);
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(1000);
await Helpers.Sleep(Delays.PostStashOpen);
var stashPos = await _inventory.FindAndClickNameplate("Stash");
if (stashPos == null)
@ -87,60 +124,41 @@ public class TradeExecutor
SetState(TradeState.Failed);
return false;
}
await Helpers.Sleep(1000);
await Helpers.Sleep(Delays.PostStashOpen);
return true;
}
// 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
private async Task ReturnHome()
{
SetState(TradeState.GoingHome);
await _game.FocusGame();
await Helpers.Sleep(300);
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);
// Step 6: Store items
SetState(TradeState.InHideout);
await Helpers.Sleep(1000);
await _inventory.ProcessInventory();
SetState(TradeState.Idle);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Trade execution failed");
SetState(TradeState.Failed);
private async Task RecoverFromError()
{
try
{
await _game.FocusGame();
await _game.PressEscape();
await Helpers.Sleep(500);
await Helpers.Sleep(Delays.PostEscape);
await _game.GoToHideout();
}
catch { /* best-effort recovery */ }
SetState(TradeState.Idle);
return false;
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);
}

View file

@ -7,10 +7,10 @@ public class TradeQueue
{
private readonly Queue<TradeInfo> _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;

View file

@ -1,29 +0,0 @@
using Microsoft.Extensions.Configuration;
namespace Poe2Trade.Core;
public class AppConfig
{
public List<string> 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;
}
}

View file

@ -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;
}

View file

@ -8,9 +8,6 @@
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>

View file

@ -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();

View file

@ -0,0 +1,22 @@
namespace Poe2Trade.Game;
public interface IGameController
{
Task<bool> 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();
}

View file

@ -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<bool> EnsureAtOwnHideout();
Task ProcessInventory();
Task<bool> WaitForAreaTransition(int timeoutMs, Func<Task>? triggerAction = null);
Task<(int X, int Y)?> FindAndClickNameplate(string name, int maxRetries = 3, int retryDelayMs = 1000);
Task DepositItemsToStash(List<PlacedItem> items);
Task<bool> SalvageItems(List<PlacedItem> items);
(bool[,] Grid, List<PlacedItem> Items, int Free) GetInventoryState();
}

View file

@ -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<PlacedItem> 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();
}
}

View file

@ -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

View file

@ -0,0 +1,22 @@
using Poe2Trade.Core;
namespace Poe2Trade.Screen;
public interface IScreenReader : IDisposable
{
GridReader Grid { get; }
Task Warmup();
Task<byte[]> CaptureScreen();
Task<byte[]> CaptureRegion(Region region);
Task<OcrResponse> Ocr(Region? region = null, string? preprocess = null);
Task<(int X, int Y)?> FindTextOnScreen(string searchText, bool fuzzy = false);
Task<string> ReadFullScreen();
Task<(int X, int Y)?> FindTextInRegion(Region region, string searchText);
Task<string> ReadRegionText(Region region);
Task<bool> CheckForText(Region region, string searchText);
Task Snapshot();
Task<DiffOcrResponse> DiffOcr(string? savePath = null, Region? region = null);
Task<TemplateMatchResult?> TemplateMatch(string templatePath, Region? region = null);
Task SaveScreenshot(string path);
Task SaveRegion(Region region, string path);
}

View file

@ -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();

View file

@ -0,0 +1,15 @@
using Microsoft.Playwright;
using Poe2Trade.Core;
namespace Poe2Trade.Trade;
public interface ITradeMonitor : IAsyncDisposable
{
event Action<string, List<string>, IPage>? NewListings;
Task Start(string? dashboardUrl = null);
Task AddSearch(string tradeUrl);
Task PauseSearch(string searchId);
Task<bool> ClickTravelToHideout(IPage page, string? itemId = null);
Task<(IPage Page, List<TradeItem> Items)> OpenScrapPage(string tradeUrl);
string ExtractSearchId(string url);
}

View file

@ -5,12 +5,12 @@ using Serilog;
namespace Poe2Trade.Trade;
public class TradeMonitor : IAsyncDisposable
public class TradeMonitor : ITradeMonitor
{
private IBrowserContext? _context;
private readonly Dictionary<string, IPage> _pages = new();
private readonly HashSet<string> _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<string, List<string>, 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);

View file

@ -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<ConfigStore>();
services.AddSingleton(sp => sp.GetRequiredService<ConfigStore>().Settings);
// Services
services.AddSingleton<IGameController, GameController>();
services.AddSingleton<IScreenReader, ScreenReader>();
services.AddSingleton<IClientLogWatcher>(sp =>
new ClientLogWatcher(sp.GetRequiredService<SavedSettings>().Poe2LogPath));
services.AddSingleton<ITradeMonitor, TradeMonitor>();
services.AddSingleton<IInventoryManager, InventoryManager>();
// Bot
services.AddSingleton<LinkManager>();
services.AddSingleton<TradeExecutor>();
services.AddSingleton<TradeQueue>();
services.AddSingleton<BotOrchestrator>();
// ViewModels
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<DebugViewModel>();
services.AddSingleton<SettingsViewModel>();
var provider = services.BuildServiceProvider();
var store = provider.GetRequiredService<ConfigStore>();
var bot = provider.GetRequiredService<BotOrchestrator>();
var mainVm = provider.GetRequiredService<MainWindowViewModel>();
mainVm.DebugVm = provider.GetRequiredService<DebugViewModel>();
mainVm.SettingsVm = provider.GetRequiredService<SettingsViewModel>();
var window = new MainWindow { DataContext = mainVm };
window.SetConfigStore(store);

View file

@ -11,9 +11,15 @@
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" />
<ProjectReference Include="..\Poe2Trade.Bot\Poe2Trade.Bot.csproj" />
<ProjectReference Include="..\Poe2Trade.Game\Poe2Trade.Game.csproj" />
<ProjectReference Include="..\Poe2Trade.Screen\Poe2Trade.Screen.csproj" />
<ProjectReference Include="..\Poe2Trade.Trade\Poe2Trade.Trade.csproj" />
<ProjectReference Include="..\Poe2Trade.Log\Poe2Trade.Log.csproj" />
<ProjectReference Include="..\Poe2Trade.Inventory\Poe2Trade.Inventory.csproj" />
</ItemGroup>
</Project>

View file

@ -101,7 +101,7 @@ public partial class MainWindowViewModel : ObservableObject
{
try
{
await _bot.Start(_bot.Config.TradeUrls);
await _bot.Start([]);
IsStarted = true;
}
catch (Exception ex)

View file

@ -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;
}