switched to new way
This commit is contained in:
parent
f22d182c8f
commit
4a65c8e17b
96 changed files with 4991 additions and 10025 deletions
296
src/Poe2Trade.Bot/BotOrchestrator.cs
Normal file
296
src/Poe2Trade.Bot/BotOrchestrator.cs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
using Microsoft.Playwright;
|
||||
using Poe2Trade.Core;
|
||||
using Poe2Trade.Game;
|
||||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.GameLog;
|
||||
using Poe2Trade.Screen;
|
||||
using Poe2Trade.Trade;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Bot;
|
||||
|
||||
public class BotStatus
|
||||
{
|
||||
public bool Paused { get; set; }
|
||||
public string State { get; set; } = "Idle";
|
||||
public List<TradeLink> Links { get; set; } = [];
|
||||
public int TradesCompleted { get; set; }
|
||||
public int TradesFailed { get; set; }
|
||||
public long Uptime { get; set; }
|
||||
}
|
||||
|
||||
public class BotOrchestrator : IAsyncDisposable
|
||||
{
|
||||
private bool _paused;
|
||||
private string _state = "Idle";
|
||||
private int _tradesCompleted;
|
||||
private int _tradesFailed;
|
||||
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!;
|
||||
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)
|
||||
{
|
||||
Store = store;
|
||||
Config = config;
|
||||
_paused = store.Settings.Paused;
|
||||
Links = new LinkManager(store);
|
||||
}
|
||||
|
||||
public bool IsReady => _started;
|
||||
public bool IsPaused => _paused;
|
||||
|
||||
public string State
|
||||
{
|
||||
get => _state;
|
||||
set
|
||||
{
|
||||
if (_state == value) return;
|
||||
_state = value;
|
||||
StatusUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
_paused = true;
|
||||
Store.SetPaused(true);
|
||||
Log.Information("Bot paused");
|
||||
StatusUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
_paused = false;
|
||||
Store.SetPaused(false);
|
||||
Log.Information("Bot resumed");
|
||||
StatusUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public TradeLink AddLink(string url, string? name = null, LinkMode? mode = null, PostAction? postAction = null)
|
||||
{
|
||||
var link = Links.AddLink(url, name ?? "", mode, postAction);
|
||||
StatusUpdated?.Invoke();
|
||||
return link;
|
||||
}
|
||||
|
||||
public void RemoveLink(string id)
|
||||
{
|
||||
Links.RemoveLink(id);
|
||||
StatusUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void ToggleLink(string id, bool active)
|
||||
{
|
||||
var link = Links.ToggleLink(id, active);
|
||||
if (link == null) return;
|
||||
StatusUpdated?.Invoke();
|
||||
|
||||
if (active)
|
||||
_ = ActivateLink(link);
|
||||
else
|
||||
_ = DeactivateLink(id);
|
||||
}
|
||||
|
||||
public BotStatus GetStatus() => new()
|
||||
{
|
||||
Paused = _paused,
|
||||
State = _state,
|
||||
Links = Links.GetLinks(),
|
||||
TradesCompleted = _tradesCompleted,
|
||||
TradesFailed = _tradesFailed,
|
||||
Uptime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _startTime,
|
||||
};
|
||||
|
||||
public void UpdateExecutorState()
|
||||
{
|
||||
var execState = TradeExecutor.State;
|
||||
if (execState != TradeState.Idle)
|
||||
{
|
||||
State = execState.ToString();
|
||||
return;
|
||||
}
|
||||
foreach (var scrapExec in _scrapExecutors.Values)
|
||||
{
|
||||
if (scrapExec.State != ScrapState.Idle)
|
||||
{
|
||||
State = scrapExec.State.ToString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
State = "Idle";
|
||||
}
|
||||
|
||||
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 =>
|
||||
{
|
||||
if (t.IsFaulted) Log.Warning(t.Exception!, "OCR warmup failed");
|
||||
});
|
||||
|
||||
// Check if already in hideout
|
||||
var inHideout = LogWatcher.CurrentArea.Contains("hideout", StringComparison.OrdinalIgnoreCase);
|
||||
if (inHideout)
|
||||
{
|
||||
Log.Information("Already in hideout: {Area}", LogWatcher.CurrentArea);
|
||||
Inventory.SetLocation(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Emit("info", "Sending /hideout command...");
|
||||
await Game.FocusGame();
|
||||
var arrivedHome = await Inventory.WaitForAreaTransition(Config.TravelTimeoutMs, () => Game.GoToHideout());
|
||||
Inventory.SetLocation(true);
|
||||
if (!arrivedHome)
|
||||
Log.Warning("Timed out waiting for hideout transition on startup");
|
||||
}
|
||||
|
||||
State = "InHideout";
|
||||
Emit("info", "In hideout, ready to trade");
|
||||
|
||||
await ocrWarmup;
|
||||
|
||||
Emit("info", "Checking inventory for leftover items...");
|
||||
await Inventory.ClearToStash();
|
||||
Emit("info", "Inventory cleared");
|
||||
|
||||
// Create executors
|
||||
TradeExecutor = new TradeExecutor(Game, Screen, TradeMonitor, Inventory, Config);
|
||||
TradeExecutor.StateChanged += _ => UpdateExecutorState();
|
||||
TradeQueue = new TradeQueue(TradeExecutor, Config);
|
||||
TradeQueue.TradeCompleted += () => { _tradesCompleted++; StatusUpdated?.Invoke(); };
|
||||
TradeQueue.TradeFailed += () => { _tradesFailed++; StatusUpdated?.Invoke(); };
|
||||
|
||||
// Load links
|
||||
var allUrls = new HashSet<string>(cliUrls);
|
||||
foreach (var l in Store.Settings.Links)
|
||||
allUrls.Add(l.Url);
|
||||
|
||||
foreach (var url in allUrls)
|
||||
{
|
||||
var link = Links.AddLink(url);
|
||||
if (link.Active)
|
||||
await ActivateLink(link);
|
||||
else
|
||||
Emit("info", $"Loaded (inactive): {link.Name}");
|
||||
}
|
||||
|
||||
// Wire trade monitor events
|
||||
TradeMonitor.NewListings += OnNewListings;
|
||||
|
||||
_started = true;
|
||||
Emit("info", $"Loaded {allUrls.Count} trade link(s)");
|
||||
Log.Information("Bot started");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Log.Information("Shutting down bot...");
|
||||
foreach (var exec in _scrapExecutors.Values)
|
||||
await exec.Stop();
|
||||
Screen.Dispose();
|
||||
await TradeMonitor.DisposeAsync();
|
||||
LogWatcher.Dispose();
|
||||
}
|
||||
|
||||
private void OnNewListings(string searchId, List<string> itemIds, IPage page)
|
||||
{
|
||||
if (_paused)
|
||||
{
|
||||
Emit("warn", $"New listings ({itemIds.Count}) skipped - bot paused");
|
||||
return;
|
||||
}
|
||||
if (!Links.IsActive(searchId)) return;
|
||||
|
||||
Log.Information("New listings: {SearchId} ({Count} items)", searchId, itemIds.Count);
|
||||
Emit("info", $"New listings: {itemIds.Count} items from {searchId}");
|
||||
|
||||
TradeQueue.Enqueue(new TradeInfo(
|
||||
SearchId: searchId,
|
||||
ItemIds: itemIds,
|
||||
WhisperText: "",
|
||||
Timestamp: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
TradeUrl: "",
|
||||
Page: page
|
||||
));
|
||||
}
|
||||
|
||||
private async Task ActivateLink(TradeLink link)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (link.Mode == LinkMode.Scrap)
|
||||
{
|
||||
var scrapExec = new ScrapExecutor(Game, Screen, TradeMonitor, Inventory, Config);
|
||||
scrapExec.StateChanged += _ => UpdateExecutorState();
|
||||
scrapExec.ItemBought += () => { _tradesCompleted++; StatusUpdated?.Invoke(); };
|
||||
scrapExec.ItemFailed += () => { _tradesFailed++; StatusUpdated?.Invoke(); };
|
||||
_scrapExecutors[link.Id] = scrapExec;
|
||||
Emit("info", $"Scrap loop started: {link.Name}");
|
||||
StatusUpdated?.Invoke();
|
||||
|
||||
_ = scrapExec.RunScrapLoop(link.Url, link.PostAction).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Log.Error(t.Exception!, "Scrap loop error: {LinkId}", link.Id);
|
||||
Emit("error", $"Scrap loop failed: {link.Name}");
|
||||
_scrapExecutors.Remove(link.Id);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await TradeMonitor.AddSearch(link.Url);
|
||||
Emit("info", $"Monitoring: {link.Name}");
|
||||
StatusUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to activate link: {Url}", link.Url);
|
||||
Emit("error", $"Failed to activate: {link.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeactivateLink(string id)
|
||||
{
|
||||
if (_scrapExecutors.TryGetValue(id, out var scrapExec))
|
||||
{
|
||||
await scrapExec.Stop();
|
||||
_scrapExecutors.Remove(id);
|
||||
}
|
||||
await TradeMonitor.PauseSearch(id);
|
||||
}
|
||||
|
||||
private void Emit(string level, string message) => LogMessage?.Invoke(level, message);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue