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);
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<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,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<bool> 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<bool> 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);
}

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;