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);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
12
src/Poe2Trade.Core/Delays.cs
Normal file
12
src/Poe2Trade.Core/Delays.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
22
src/Poe2Trade.Game/IGameController.cs
Normal file
22
src/Poe2Trade.Game/IGameController.cs
Normal 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();
|
||||
}
|
||||
20
src/Poe2Trade.Inventory/IInventoryManager.cs
Normal file
20
src/Poe2Trade.Inventory/IInventoryManager.cs
Normal 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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
src/Poe2Trade.Screen/IScreenReader.cs
Normal file
22
src/Poe2Trade.Screen/IScreenReader.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
15
src/Poe2Trade.Trade/ITradeMonitor.cs
Normal file
15
src/Poe2Trade.Trade/ITradeMonitor.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
{
|
||||
try
|
||||
{
|
||||
await _bot.Start(_bot.Config.TradeUrls);
|
||||
await _bot.Start([]);
|
||||
IsStarted = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue