refactoring
This commit is contained in:
parent
696fd07e86
commit
50d32abd49
20 changed files with 334 additions and 225 deletions
|
|
@ -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(); };
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue