using System.Diagnostics; using Poe2Trade.Core; using Poe2Trade.Game; using Poe2Trade.Inventory; using Poe2Trade.Screen; using Serilog; namespace Poe2Trade.Bot; /// /// Base class for game executors that interact with the game world. /// Provides shared utilities: loot pickup, recovery, walking, stash tab resolution. /// public abstract class GameExecutor { protected static readonly Random Rng = new(); protected readonly IGameController _game; protected readonly IScreenReader _screen; protected readonly IInventoryManager _inventory; protected readonly SavedSettings _config; protected volatile bool _stopped; protected CancellationTokenSource _stopCts = new(); /// Cancellation token that fires when Stop() is called. protected CancellationToken StopToken => _stopCts.Token; protected GameExecutor(IGameController game, IScreenReader screen, IInventoryManager inventory, SavedSettings config) { _game = game; _screen = screen; _inventory = inventory; _config = config; } public virtual void Stop() { _stopped = true; _stopCts.Cancel(); } /// Reset stopped state for a new run. protected void ResetStop() { _stopped = false; _stopCts.Dispose(); _stopCts = new CancellationTokenSource(); } /// Cancellable sleep that throws OperationCanceledException when stopped. protected Task Sleep(int ms) => Helpers.Sleep(ms, _stopCts.Token); // ------ Loot pickup ------ // Tiers to skip (noise, low-value, or hidden by filter) private static readonly HashSet SkipTiers = ["gold"]; public async Task Loot() { Log.Information("Starting loot pickup"); const int maxRounds = 5; var totalPicked = 0; for (var round = 0; round < maxRounds; round++) { if (_stopped) return; // Move mouse out of the way so it doesn't cover labels _game.MoveMouseInstant(0, 1440); await Sleep(100); // Hold Alt, capture, detect await _game.KeyDown(InputSender.VK.MENU); await Sleep(250); using var capture = _screen.CaptureRawBitmap(); var labels = _screen.DetectLootLabels(capture, capture); var pickups = labels.Where(l => !SkipTiers.Contains(l.Tier)).ToList(); if (pickups.Count == 0) { await _game.KeyUp(InputSender.VK.MENU); Log.Information("No loot labels in round {Round} (picked {Total} total)", round + 1, totalPicked); break; } Log.Information("Round {Round}: {Count} loot labels ({Skipped} skipped)", round + 1, pickups.Count, labels.Count - pickups.Count); // Click all detected labels (positions are stable) foreach (var label in pickups) { if (_stopped) break; Log.Information("Picking up: tier={Tier} color=({R},{G},{B}) at ({X},{Y})", label.Tier, label.AvgR, label.AvgG, label.AvgB, label.CenterX, label.CenterY); await _game.LeftClickAt(label.CenterX, label.CenterY); totalPicked++; await Sleep(200); } // Quick check: capture small region around each clicked label to see if // new labels appeared underneath. If none changed, we're done. await Sleep(300); _game.MoveMouseInstant(0, 1440); await Sleep(100); using var recheck = _screen.CaptureRawBitmap(); var newLabels = _screen.DetectLootLabels(recheck, recheck); var newPickups = newLabels.Where(l => !SkipTiers.Contains(l.Tier)).ToList(); await _game.KeyUp(InputSender.VK.MENU); if (newPickups.Count == 0) { Log.Information("Quick recheck: no new labels, done (picked {Total} total)", totalPicked); break; } Log.Information("Quick recheck: {Count} new labels appeared, continuing", newPickups.Count); await Sleep(300); } Log.Information("Loot pickup complete ({Count} items)", totalPicked); } // ------ Recovery ------ protected async Task RecoverToHideout() { try { Log.Information("Recovering: escaping and going to hideout"); await _game.FocusGame(); // await _game.PressEscape(); // await Sleep(Delays.PostEscape); // await _game.PressEscape(); // await Sleep(Delays.PostEscape); var arrived = await _inventory.WaitForAreaTransition( _config.TravelTimeoutMs, () => _game.GoToHideout()); if (arrived) { _inventory.SetLocation(true); Log.Information("Recovery: arrived at hideout"); } else { Log.Warning("Recovery: timed out going to hideout"); } } catch (Exception ex) { Log.Error(ex, "Recovery failed"); } } // ------ Walk + template match ------ protected async Task WalkAndMatch(string templatePath, int vk1, int vk2, int timeoutMs = 15000, int closeRadius = 350) { const int screenCx = 2560 / 2; const int screenCy = 1440 / 2; await _game.KeyDown(vk1); await _game.KeyDown(vk2); try { var sw = Stopwatch.StartNew(); bool spotted = false; while (sw.ElapsedMilliseconds < timeoutMs) { if (_stopped) return null; var match = await _screen.TemplateMatch(templatePath); if (match == null) { await Sleep(500); continue; } var dx = match.X - screenCx; var dy = match.Y - screenCy; var dist = Math.Sqrt(dx * dx + dy * dy); if (!spotted) { Log.Information("Template spotted at ({X},{Y}), dist={Dist:F0}px from center, approaching...", match.X, match.Y, dist); spotted = true; } if (dist <= closeRadius) { Log.Information("Close enough at ({X},{Y}), dist={Dist:F0}px, stopping", match.X, match.Y, dist); // Stop, settle, re-match for accurate position await _game.KeyUp(vk2); await _game.KeyUp(vk1); await Sleep(300); var fresh = await _screen.TemplateMatch(templatePath); if (fresh != null) { Log.Information("Final position at ({X},{Y})", fresh.X, fresh.Y); return fresh; } Log.Warning("Re-match failed, using last known position"); return match; } await Sleep(200); } Log.Error("WalkAndMatch timed out after {Ms}ms (spotted={Spotted})", timeoutMs, spotted); return null; } finally { await _game.KeyUp(vk2); await _game.KeyUp(vk1); } } // ------ Stash tab resolution ------ protected (StashTabInfo? Tab, StashTabInfo? Folder) ResolveTabPath(string tabPath) { if (string.IsNullOrEmpty(tabPath) || _config.StashCalibration == null) return (null, null); var parts = tabPath.Split('/'); if (parts.Length == 1) { var tab = _config.StashCalibration.Tabs.FirstOrDefault(t => t.Name == parts[0]); return (tab, null); } if (parts.Length == 2) { var folder = _config.StashCalibration.Tabs.FirstOrDefault(t => t.Name == parts[0] && t.IsFolder); if (folder == null) return (null, null); var subTab = folder.SubTabs.FirstOrDefault(t => t.Name == parts[1]); return (subTab, folder); } return (null, null); } }