rwork on kulemak bot and cleanup

This commit is contained in:
Boki 2026-02-21 09:18:10 -05:00
parent c75b2b27f0
commit 053a016c8b
15 changed files with 727 additions and 160 deletions

View file

@ -0,0 +1,222 @@
using System.Diagnostics;
using Poe2Trade.Core;
using Poe2Trade.Game;
using Poe2Trade.Inventory;
using Poe2Trade.Screen;
using Serilog;
namespace Poe2Trade.Bot;
/// <summary>
/// Base class for game executors that interact with the game world.
/// Provides shared utilities: loot pickup, recovery, walking, stash tab resolution.
/// </summary>
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 GameExecutor(IGameController game, IScreenReader screen,
IInventoryManager inventory, SavedSettings config)
{
_game = game;
_screen = screen;
_inventory = inventory;
_config = config;
}
public virtual void Stop()
{
_stopped = true;
}
// ------ Loot pickup ------
// Tiers to skip (noise, low-value, or hidden by filter)
private static readonly HashSet<string> SkipTiers = ["unknown", "gold"];
public async Task Loot()
{
Log.Information("Starting loot pickup");
const int maxRounds = 5;
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 Helpers.Sleep(100);
// Hold Alt to ensure all labels are visible, then capture
await _game.KeyDown(InputSender.VK.MENU);
await Helpers.Sleep(250);
var capture = _screen.CaptureRawBitmap();
// Detect magenta-bordered labels directly (no diff needed)
var labels = _screen.DetectLootLabels(capture, capture);
capture.Dispose();
// Filter out noise and unwanted tiers
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} (total detected: {Total}, filtered: {Filtered})",
round + 1, labels.Count, labels.Count - pickups.Count);
break;
}
Log.Information("Round {Round}: {Count} loot labels ({Skipped} skipped)",
round + 1, pickups.Count, labels.Count - pickups.Count);
foreach (var skip in labels.Where(l => SkipTiers.Contains(l.Tier)))
Log.Debug("Skipped: tier={Tier} color=({R},{G},{B}) at ({X},{Y})",
skip.Tier, skip.AvgR, skip.AvgG, skip.AvgB, skip.CenterX, skip.CenterY);
// Click each label center (Alt still held so labels visible)
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);
await Helpers.Sleep(300);
}
await _game.KeyUp(InputSender.VK.MENU);
await Helpers.Sleep(500);
}
Log.Information("Loot pickup complete");
}
// ------ Recovery ------
protected async Task RecoverToHideout()
{
try
{
Log.Information("Recovering: escaping and going to hideout");
await _game.FocusGame();
await _game.PressEscape();
await Helpers.Sleep(Delays.PostEscape);
await _game.PressEscape();
await Helpers.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<TemplateMatchResult?> 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 Helpers.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 Helpers.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 Helpers.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);
}
}