rwork on kulemak bot and cleanup
This commit is contained in:
parent
c75b2b27f0
commit
053a016c8b
15 changed files with 727 additions and 160 deletions
222
src/Poe2Trade.Bot/GameExecutor.cs
Normal file
222
src/Poe2Trade.Bot/GameExecutor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue