work on well of souls and yolo detection
This commit is contained in:
parent
3456e0d62a
commit
40d30115bf
41 changed files with 3031 additions and 148 deletions
529
src/Poe2Trade.Bot/BossRunExecutor.cs
Normal file
529
src/Poe2Trade.Bot/BossRunExecutor.cs
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
using System.Diagnostics;
|
||||
using Poe2Trade.Core;
|
||||
using Poe2Trade.Game;
|
||||
using Poe2Trade.GameLog;
|
||||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Bot;
|
||||
|
||||
public class BossRunExecutor
|
||||
{
|
||||
private static readonly string WellOfSoulsTemplate = Path.Combine("assets", "well-of-souls.png");
|
||||
private static readonly string BlackCathedralTemplate = Path.Combine("assets", "black-cathedral.png");
|
||||
private static readonly string InvitationTemplate = Path.Combine("assets", "invitation.png");
|
||||
|
||||
private BossRunState _state = BossRunState.Idle;
|
||||
private bool _stopped;
|
||||
private readonly IGameController _game;
|
||||
private readonly IScreenReader _screen;
|
||||
private readonly IInventoryManager _inventory;
|
||||
private readonly IClientLogWatcher _logWatcher;
|
||||
private readonly SavedSettings _config;
|
||||
private readonly BossDetector _bossDetector;
|
||||
|
||||
public event Action<BossRunState>? StateChanged;
|
||||
|
||||
public BossRunExecutor(IGameController game, IScreenReader screen,
|
||||
IInventoryManager inventory, IClientLogWatcher logWatcher, SavedSettings config,
|
||||
BossDetector bossDetector)
|
||||
{
|
||||
_game = game;
|
||||
_screen = screen;
|
||||
_inventory = inventory;
|
||||
_logWatcher = logWatcher;
|
||||
_config = config;
|
||||
_bossDetector = bossDetector;
|
||||
}
|
||||
|
||||
public BossRunState State => _state;
|
||||
|
||||
private void SetState(BossRunState s)
|
||||
{
|
||||
_state = s;
|
||||
StateChanged?.Invoke(s);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_stopped = true;
|
||||
Log.Information("Boss run executor stop requested");
|
||||
}
|
||||
|
||||
public async Task RunBossLoop()
|
||||
{
|
||||
_stopped = false;
|
||||
_bossDetector.SetBoss("kulemak");
|
||||
Log.Information("Starting boss run loop ({Count} invitations)", _config.Kulemak.InvitationCount);
|
||||
|
||||
if (!await Prepare())
|
||||
{
|
||||
SetState(BossRunState.Failed);
|
||||
await RecoverToHideout();
|
||||
SetState(BossRunState.Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
var completed = 0;
|
||||
for (var i = 0; i < _config.Kulemak.InvitationCount; i++)
|
||||
{
|
||||
if (_stopped) break;
|
||||
|
||||
Log.Information("=== Boss run {N}/{Total} ===", i + 1, _config.Kulemak.InvitationCount);
|
||||
|
||||
if (!await TravelToZone())
|
||||
{
|
||||
Log.Error("Failed to travel to zone");
|
||||
await RecoverToHideout();
|
||||
break;
|
||||
}
|
||||
if (_stopped) break;
|
||||
|
||||
var entrance = await WalkToEntrance();
|
||||
if (entrance == null)
|
||||
{
|
||||
Log.Error("Failed to find Black Cathedral entrance");
|
||||
await RecoverToHideout();
|
||||
break;
|
||||
}
|
||||
if (_stopped) break;
|
||||
|
||||
if (!await UseInvitation(entrance.X, entrance.Y))
|
||||
{
|
||||
Log.Error("Failed to use invitation");
|
||||
await RecoverToHideout();
|
||||
break;
|
||||
}
|
||||
if (_stopped) break;
|
||||
|
||||
await Fight();
|
||||
if (_stopped) break;
|
||||
|
||||
await Loot();
|
||||
if (_stopped) break;
|
||||
|
||||
if (!await ReturnHome())
|
||||
{
|
||||
Log.Error("Failed to return home");
|
||||
await RecoverToHideout();
|
||||
break;
|
||||
}
|
||||
if (_stopped) break;
|
||||
|
||||
await StoreLoot();
|
||||
completed++;
|
||||
|
||||
if (_stopped) break;
|
||||
}
|
||||
|
||||
Log.Information("Boss run loop finished: {Completed}/{Total} runs completed", completed, _config.Kulemak.InvitationCount);
|
||||
SetState(BossRunState.Complete);
|
||||
await Helpers.Sleep(1000);
|
||||
SetState(BossRunState.Idle);
|
||||
}
|
||||
|
||||
private async Task<bool> Prepare()
|
||||
{
|
||||
SetState(BossRunState.Preparing);
|
||||
Log.Information("Preparing: depositing inventory and grabbing invitations");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Helpers.Sleep(Delays.PostFocus);
|
||||
|
||||
// Open stash
|
||||
var stashPos = await _inventory.FindAndClickNameplate("Stash");
|
||||
if (stashPos == null)
|
||||
{
|
||||
Log.Error("Could not find Stash nameplate");
|
||||
return false;
|
||||
}
|
||||
await Helpers.Sleep(Delays.PostStashOpen);
|
||||
|
||||
// Click loot tab and deposit all inventory items
|
||||
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
||||
if (lootTab != null)
|
||||
{
|
||||
await _inventory.ClickStashTab(lootTab, lootFolder);
|
||||
|
||||
// Deposit all inventory items via ctrl+click
|
||||
var scanResult = await _screen.Grid.Scan("inventory");
|
||||
if (scanResult.Occupied.Count > 0)
|
||||
{
|
||||
Log.Information("Depositing {Count} inventory items to loot tab", scanResult.Occupied.Count);
|
||||
await _game.KeyDown(InputSender.VK.SHIFT);
|
||||
await _game.HoldCtrl();
|
||||
foreach (var cell in scanResult.Occupied)
|
||||
{
|
||||
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
|
||||
await _game.LeftClickAt(center.X, center.Y);
|
||||
await Helpers.Sleep(Delays.ClickInterval);
|
||||
}
|
||||
await _game.ReleaseCtrl();
|
||||
await _game.KeyUp(InputSender.VK.SHIFT);
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("Loot tab path not configured or not found, skipping deposit");
|
||||
}
|
||||
|
||||
// Click invitation tab and grab invitations
|
||||
var (invTab, invFolder) = ResolveTabPath(_config.Kulemak.InvitationTabPath);
|
||||
if (invTab != null)
|
||||
{
|
||||
await _inventory.ClickStashTab(invTab, invFolder);
|
||||
|
||||
// Determine layout name based on tab config
|
||||
var layoutName = (invTab.GridCols == 24, invFolder != null) switch
|
||||
{
|
||||
(true, true) => "stash24_folder",
|
||||
(true, false) => "stash24",
|
||||
(false, true) => "stash12_folder",
|
||||
(false, false) => "stash12",
|
||||
};
|
||||
|
||||
await _inventory.GrabItemsFromStash(layoutName, _config.Kulemak.InvitationCount, InvitationTemplate);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("Invitation tab path not configured or not found, skipping grab");
|
||||
}
|
||||
|
||||
// Close stash
|
||||
await _game.PressEscape();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
|
||||
Log.Information("Preparation complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> TravelToZone()
|
||||
{
|
||||
SetState(BossRunState.TravelingToZone);
|
||||
Log.Information("Traveling to Well of Souls via waypoint");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Helpers.Sleep(Delays.PostFocus);
|
||||
|
||||
// Find and click Waypoint
|
||||
var wpPos = await _inventory.FindAndClickNameplate("Waypoint");
|
||||
if (wpPos == null)
|
||||
{
|
||||
Log.Error("Could not find Waypoint nameplate");
|
||||
return false;
|
||||
}
|
||||
await Helpers.Sleep(1000);
|
||||
|
||||
// Template match well-of-souls.png and click
|
||||
var match = await _screen.TemplateMatch(WellOfSoulsTemplate);
|
||||
if (match == null)
|
||||
{
|
||||
Log.Error("Could not find Well of Souls on waypoint map");
|
||||
await _game.PressEscape();
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Information("Found Well of Souls at ({X},{Y}), clicking", match.X, match.Y);
|
||||
await _game.LeftClickAt(match.X, match.Y);
|
||||
|
||||
// Wait for area transition
|
||||
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
|
||||
if (!arrived)
|
||||
{
|
||||
Log.Error("Timed out waiting for Well of Souls transition");
|
||||
return false;
|
||||
}
|
||||
|
||||
await Helpers.Sleep(Delays.PostTravel);
|
||||
Log.Information("Arrived at Well of Souls");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<TemplateMatchResult?> WalkToEntrance()
|
||||
{
|
||||
SetState(BossRunState.WalkingToEntrance);
|
||||
Log.Information("Walking to Black Cathedral entrance (W+D)");
|
||||
|
||||
return await WalkAndMatch(BlackCathedralTemplate, InputSender.VK.W, InputSender.VK.D, 15000);
|
||||
}
|
||||
|
||||
private async Task<bool> UseInvitation(int x, int y)
|
||||
{
|
||||
SetState(BossRunState.UsingInvitation);
|
||||
Log.Information("Using invitation at ({X},{Y})", x, y);
|
||||
|
||||
// Hover first so the game registers the target, then use invitation
|
||||
await _game.MoveMouseTo(x, y);
|
||||
await Helpers.Sleep(500);
|
||||
await _game.CtrlLeftClickAt(x, y);
|
||||
await Helpers.Sleep(1000);
|
||||
|
||||
// Find "NEW" text — pick the leftmost instance
|
||||
var ocr = await _screen.Ocr();
|
||||
var newWords = ocr.Lines
|
||||
.SelectMany(l => l.Words)
|
||||
.Where(w => w.Text.Equals("NEW", StringComparison.OrdinalIgnoreCase)
|
||||
|| w.Text.Equals("New", StringComparison.Ordinal))
|
||||
.OrderBy(w => w.X)
|
||||
.ToList();
|
||||
|
||||
if (newWords.Count == 0)
|
||||
{
|
||||
Log.Error("Could not find 'NEW' text for instance selection");
|
||||
return false;
|
||||
}
|
||||
|
||||
var target = newWords[0];
|
||||
var clickX = target.X + target.Width / 2;
|
||||
var clickY = target.Y + target.Height / 2;
|
||||
Log.Information("Found {Count} 'NEW' matches, clicking leftmost at ({X},{Y})", newWords.Count, clickX, clickY);
|
||||
await _game.MoveMouseTo(clickX, clickY);
|
||||
await Helpers.Sleep(500);
|
||||
await _game.LeftClickAt(clickX, clickY);
|
||||
|
||||
// Wait for area transition into boss arena
|
||||
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
|
||||
if (!arrived)
|
||||
{
|
||||
Log.Error("Timed out waiting for boss arena transition");
|
||||
return false;
|
||||
}
|
||||
|
||||
await Helpers.Sleep(Delays.PostTravel);
|
||||
Log.Information("Entered boss arena");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task Fight()
|
||||
{
|
||||
SetState(BossRunState.Fighting);
|
||||
Log.Information("[PLACEHOLDER] Fight phase - waiting for manual combat");
|
||||
// Placeholder: user handles combat manually for now
|
||||
await Helpers.Sleep(1000);
|
||||
}
|
||||
|
||||
private async Task Loot()
|
||||
{
|
||||
SetState(BossRunState.Looting);
|
||||
Log.Information("[PLACEHOLDER] Loot phase - waiting for manual looting");
|
||||
// Placeholder: user handles looting manually for now
|
||||
await Helpers.Sleep(1000);
|
||||
}
|
||||
|
||||
private async Task<bool> ReturnHome()
|
||||
{
|
||||
SetState(BossRunState.Returning);
|
||||
Log.Information("Returning home");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Helpers.Sleep(Delays.PostFocus);
|
||||
|
||||
// Walk away from loot (hold S briefly)
|
||||
await _game.KeyDown(InputSender.VK.S);
|
||||
await Helpers.Sleep(1000);
|
||||
await _game.KeyUp(InputSender.VK.S);
|
||||
await Helpers.Sleep(300);
|
||||
|
||||
// Press + to open portal
|
||||
await _game.PressPlus();
|
||||
await Helpers.Sleep(1500);
|
||||
|
||||
// Find "The Ardura Caravan" and click it
|
||||
var caravanPos = await _inventory.FindAndClickNameplate("The Ardura Caravan", maxRetries: 5, retryDelayMs: 1500);
|
||||
if (caravanPos == null)
|
||||
{
|
||||
Log.Error("Could not find 'The Ardura Caravan' portal");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for area transition to caravan
|
||||
var arrivedCaravan = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
|
||||
if (!arrivedCaravan)
|
||||
{
|
||||
Log.Error("Timed out waiting for caravan transition");
|
||||
return false;
|
||||
}
|
||||
await Helpers.Sleep(Delays.PostTravel);
|
||||
|
||||
// /hideout to go home
|
||||
var arrivedHome = await _inventory.WaitForAreaTransition(
|
||||
_config.TravelTimeoutMs, () => _game.GoToHideout());
|
||||
if (!arrivedHome)
|
||||
{
|
||||
Log.Error("Timed out going to hideout");
|
||||
return false;
|
||||
}
|
||||
|
||||
await Helpers.Sleep(Delays.PostTravel);
|
||||
_inventory.SetLocation(true);
|
||||
Log.Information("Arrived at hideout");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task StoreLoot()
|
||||
{
|
||||
SetState(BossRunState.StoringLoot);
|
||||
Log.Information("Storing loot");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Helpers.Sleep(Delays.PostFocus);
|
||||
|
||||
// Open stash
|
||||
var stashPos = await _inventory.FindAndClickNameplate("Stash");
|
||||
if (stashPos == null)
|
||||
{
|
||||
Log.Warning("Could not find Stash, skipping loot storage");
|
||||
return;
|
||||
}
|
||||
await Helpers.Sleep(Delays.PostStashOpen);
|
||||
|
||||
// Click loot tab
|
||||
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
||||
if (lootTab != null)
|
||||
await _inventory.ClickStashTab(lootTab, lootFolder);
|
||||
|
||||
// Deposit all inventory items
|
||||
var scanResult = await _screen.Grid.Scan("inventory");
|
||||
if (scanResult.Occupied.Count > 0)
|
||||
{
|
||||
Log.Information("Depositing {Count} items to loot tab", scanResult.Occupied.Count);
|
||||
await _game.KeyDown(InputSender.VK.SHIFT);
|
||||
await _game.HoldCtrl();
|
||||
foreach (var cell in scanResult.Occupied)
|
||||
{
|
||||
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
|
||||
await _game.LeftClickAt(center.X, center.Y);
|
||||
await Helpers.Sleep(Delays.ClickInterval);
|
||||
}
|
||||
await _game.ReleaseCtrl();
|
||||
await _game.KeyUp(InputSender.VK.SHIFT);
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
}
|
||||
|
||||
// Close stash
|
||||
await _game.PressEscape();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
|
||||
Log.Information("Loot stored");
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
|
||||
private (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)
|
||||
{
|
||||
// Simple tab name
|
||||
var tab = _config.StashCalibration.Tabs.FirstOrDefault(t => t.Name == parts[0]);
|
||||
return (tab, null);
|
||||
}
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
// Folder/SubTab
|
||||
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);
|
||||
}
|
||||
|
||||
private 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue