diff --git a/debug_loot_capture.png b/debug_loot_capture.png index 1fb1a3e..abea196 100644 Binary files a/debug_loot_capture.png and b/debug_loot_capture.png differ diff --git a/debug_loot_detected.png b/debug_loot_detected.png index 3199c9e..68e75b2 100644 Binary files a/debug_loot_detected.png and b/debug_loot_detected.png differ diff --git a/debug_loot_edges.png b/debug_loot_edges.png index cc4da60..07b4b1f 100644 Binary files a/debug_loot_edges.png and b/debug_loot_edges.png differ diff --git a/src/Poe2Trade.Bot/BossRunExecutor.cs b/src/Poe2Trade.Bot/BossRunExecutor.cs index c78b804..c706344 100644 --- a/src/Poe2Trade.Bot/BossRunExecutor.cs +++ b/src/Poe2Trade.Bot/BossRunExecutor.cs @@ -481,22 +481,70 @@ public class BossRunExecutor : GameExecutor /// /// Wait for the boss healthbar to appear (boss spawns/becomes active). /// + // -- Async combat: combat runs in background, checks run on main thread -- + + private volatile int _combatTargetX = 1280; + private volatile int _combatTargetY = 720; + private volatile int _combatJitter = 30; + + /// + /// Background combat loop — calls Tick continuously until cancelled. + /// Target position is updated via volatile fields from the main thread. + /// + private async Task RunCombatLoop(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + await _combat.Tick(_combatTargetX, _combatTargetY, _combatJitter); + } + catch (OperationCanceledException) { } + } + + /// + /// Start background combat loop, returning the task and CTS. + /// Caller must cancel + await + ReleaseAll when done. + /// + private (Task combatTask, CancellationTokenSource cts) StartCombatLoop(int x = 1280, int y = 720, int jitter = 30) + { + _combatTargetX = x; + _combatTargetY = y; + _combatJitter = jitter; + _combat.Reset().GetAwaiter().GetResult(); + var cts = new CancellationTokenSource(); + var task = Task.Run(() => RunCombatLoop(cts.Token)); + return (task, cts); + } + + private async Task StopCombatLoop(Task combatTask, CancellationTokenSource cts) + { + cts.Cancel(); + try { await combatTask; } catch { /* expected */ } + await _combat.ReleaseAll(); + cts.Dispose(); + } + private async Task WaitForBossSpawn(int timeoutMs = 30_000) { Log.Information("Waiting for boss healthbar to appear..."); var sw = Stopwatch.StartNew(); - var atkX = 1280; - var atkY = 720; - var lastCheckMs = 0L; - while (sw.ElapsedMilliseconds < timeoutMs) + var (combatTask, cts) = StartCombatLoop(); + try { - if (_stopped) return false; - - // Only check healthbar + death every ~500ms to avoid blocking combat - if (sw.ElapsedMilliseconds - lastCheckMs > 500) + while (sw.ElapsedMilliseconds < timeoutMs) { - lastCheckMs = sw.ElapsedMilliseconds; + if (_stopped) return false; + + // Update target from YOLO (fast, no capture) + var snapshot = _bossDetector.Latest; + if (snapshot.Bosses.Count > 0) + { + _combatTargetX = snapshot.Bosses[0].Cx; + _combatTargetY = snapshot.Bosses[0].Cy; + } + + // Check death + healthbar (blocks main thread, combat continues in background) if (await CheckDeath()) continue; if (await IsBossAlive()) @@ -506,18 +554,13 @@ public class BossRunExecutor : GameExecutor } } - // Attack toward YOLO-detected boss if available, otherwise last known position - var snapshot = _bossDetector.Latest; - if (snapshot.Bosses.Count > 0) - { - atkX = snapshot.Bosses[0].Cx; - atkY = snapshot.Bosses[0].Cy; - } - await _combat.Tick(atkX, atkY); + Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs); + return false; + } + finally + { + await StopCombatLoop(combatTask, cts); } - - Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs); - return false; } /// @@ -532,60 +575,41 @@ public class BossRunExecutor : GameExecutor const int screenCx = 1280; const int screenCy = 720; - - // Attack target — defaults to screen center, updated by YOLO - var atkX = screenCx; - var atkY = screenCy; (double X, double Y)? lastBossWorldPos = null; - await _game.MoveMouseFast(atkX, atkY); - await Helpers.Sleep(200); - - var sw = Stopwatch.StartNew(); - var lastCheckMs = 0L; - - await _combat.Reset(); Log.Information("Boss is alive, engaging"); + var (combatTask, cts) = StartCombatLoop(screenCx, screenCy); try { + var sw = Stopwatch.StartNew(); while (sw.ElapsedMilliseconds < timeoutMs) { if (_stopped) return lastBossWorldPos; - // Update attack target from YOLO when available + // Update attack target from YOLO (fast, no capture) var snapshot = _bossDetector.Latest; if (snapshot.Bosses.Count > 0) { var boss = snapshot.Bosses[0]; - atkX = boss.Cx; - atkY = boss.Cy; + _combatTargetX = boss.Cx; + _combatTargetY = boss.Cy; - // Convert boss screen offset to world position - // Scale: sqrt(72²+65²)/835 ≈ 0.116 minimap px per screen px const double screenToWorld = 97.0 / 835.0; var wp = _nav.WorldPosition; - var bossWorldX = wp.X + (boss.Cx - screenCx) * screenToWorld; - var bossWorldY = wp.Y + (boss.Cy - screenCy) * screenToWorld; - lastBossWorldPos = (bossWorldX, bossWorldY); + lastBossWorldPos = ( + wp.X + (boss.Cx - screenCx) * screenToWorld, + wp.Y + (boss.Cy - screenCy) * screenToWorld); } - // Re-check healthbar every ~0.5s, first miss = phase over - if (sw.ElapsedMilliseconds - lastCheckMs > 500) + // Check death + healthbar (combat keeps running in background) + if (await CheckDeath()) continue; + + if (!await IsBossAlive()) { - if (await CheckDeath()) continue; - - var bossAlive = await IsBossAlive(); - lastCheckMs = sw.ElapsedMilliseconds; - - if (!bossAlive) - { - Log.Information("Healthbar not found, boss phase over after {Ms}ms", sw.ElapsedMilliseconds); - return lastBossWorldPos; - } + Log.Information("Healthbar not found, boss phase over after {Ms}ms", sw.ElapsedMilliseconds); + return lastBossWorldPos; } - - await _combat.Tick(atkX, atkY); } Log.Warning("AttackBossUntilGone timed out after {Ms}ms", timeoutMs); @@ -593,26 +617,26 @@ public class BossRunExecutor : GameExecutor } finally { - await _combat.ReleaseAll(); + await StopCombatLoop(combatTask, cts); } } private async Task AttackAtPosition(int x, int y, int durationMs) { - await _combat.Reset(); - var sw = Stopwatch.StartNew(); + var (combatTask, cts) = StartCombatLoop(x, y, jitter: 20); try { + var sw = Stopwatch.StartNew(); while (sw.ElapsedMilliseconds < durationMs) { if (_stopped) return; if (await CheckDeath()) continue; - await _combat.Tick(x, y, jitter: 20); + await Helpers.Sleep(500); } } finally { - await _combat.ReleaseAll(); + await StopCombatLoop(combatTask, cts); } } diff --git a/src/Poe2Trade.Screen/ScreenReader.cs b/src/Poe2Trade.Screen/ScreenReader.cs index b4c64ef..200542c 100644 --- a/src/Poe2Trade.Screen/ScreenReader.cs +++ b/src/Poe2Trade.Screen/ScreenReader.cs @@ -171,18 +171,24 @@ public class ScreenReader : IScreenReader public Task TemplateMatch(string templatePath, Region? region = null) { - var result = _templateMatch.Match(templatePath, region); - if (result != null) - Log.Information("Template match found: ({X},{Y}) confidence={Conf:F3}", result.X, result.Y, result.Confidence); - return Task.FromResult(result); + return Task.Run(() => + { + var result = _templateMatch.Match(templatePath, region); + if (result != null) + Log.Information("Template match found: ({X},{Y}) confidence={Conf:F3}", result.X, result.Y, result.Confidence); + return result; + }); } public Task> TemplateMatchAll(string templatePath, Region? region = null, double threshold = 0.7, bool silent = false) { - var results = _templateMatch.MatchAll(templatePath, region, threshold); - if (!silent) - Log.Information("TemplateMatchAll: {Count} matches for {Template}", results.Count, Path.GetFileName(templatePath)); - return Task.FromResult(results); + return Task.Run(() => + { + var results = _templateMatch.MatchAll(templatePath, region, threshold); + if (!silent) + Log.Information("TemplateMatchAll: {Count} matches for {Template}", results.Count, Path.GetFileName(templatePath)); + return results; + }); } // -- Save --