diff --git a/assets/toc_finish.png b/assets/toc_finish.png new file mode 100644 index 0000000..e7ef7f3 Binary files /dev/null and b/assets/toc_finish.png differ diff --git a/src/Poe2Trade.Navigation/MinimapCapture.cs b/src/Poe2Trade.Navigation/MinimapCapture.cs index af25b12..9f7d444 100644 --- a/src/Poe2Trade.Navigation/MinimapCapture.cs +++ b/src/Poe2Trade.Navigation/MinimapCapture.cs @@ -13,6 +13,8 @@ public class MinimapCapture : IFrameConsumer, IDisposable private readonly IScreenCapture _backend; // kept for debug capture paths private int _modeCheckCounter = 9; // trigger mode detection on first frame private MinimapMode _detectedMode = MinimapMode.Overlay; + private int _pendingModeCount; // consecutive detections of a different mode + private MinimapMode _pendingMode; private MinimapFrame? _lastFrame; public MinimapMode DetectedMode => _detectedMode; @@ -31,23 +33,48 @@ public class MinimapCapture : IFrameConsumer, IDisposable /// public void Process(ScreenFrame screen) { - // Auto-detect minimap mode every 10th frame via single pixel probe + // Auto-detect minimap mode by checking orange player dot at both positions. + // null = not in gameplay (loading/menu) → skip frame entirely. + // Require 3 consecutive detections of a new mode to avoid transient flips. if (++_modeCheckCounter >= 10) { _modeCheckCounter = 0; var detected = DetectMinimapMode(screen); + if (detected == null) + { + _pendingModeCount = 0; + _lastFrame = null; + return; // not in gameplay — skip frame + } + if (detected != _detectedMode) { - var oldMode = _detectedMode; - var oldRegion = _detectedMode == MinimapMode.Overlay ? _config.OverlayRegion : _config.CornerRegion; - _detectedMode = detected; - var newRegion = _detectedMode == MinimapMode.Overlay ? _config.OverlayRegion : _config.CornerRegion; - Log.Information("MODE SWITCH: {Old} → {New} | oldRegion=({OX},{OY},{OW}x{OH}) newRegion=({NX},{NY},{NW}x{NH})", - oldMode, _detectedMode, - oldRegion.X, oldRegion.Y, oldRegion.Width, oldRegion.Height, - newRegion.X, newRegion.Y, newRegion.Width, newRegion.Height); - ResetAdaptation(); - ModeChanged?.Invoke(_detectedMode); + if (detected == _pendingMode) + _pendingModeCount++; + else + { + _pendingMode = detected.Value; + _pendingModeCount = 1; + } + + if (_pendingModeCount >= 3) + { + var oldMode = _detectedMode; + var oldRegion = _detectedMode == MinimapMode.Overlay ? _config.OverlayRegion : _config.CornerRegion; + _detectedMode = detected.Value; + var newRegion = _detectedMode == MinimapMode.Overlay ? _config.OverlayRegion : _config.CornerRegion; + Log.Information("MODE SWITCH: {Old} → {New} | oldRegion=({OX},{OY},{OW}x{OH}) newRegion=({NX},{NY},{NW}x{NH})", + oldMode, _detectedMode, + oldRegion.X, oldRegion.Y, oldRegion.Width, oldRegion.Height, + newRegion.X, newRegion.Y, newRegion.Width, newRegion.Height); + ResetAdaptation(); + _pendingModeCount = 0; + ModeChanged?.Invoke(_detectedMode); + } + } + else + { + _pendingModeCount = 0; } } @@ -137,19 +164,24 @@ public class MinimapCapture : IFrameConsumer, IDisposable } /// - /// Detect minimap mode by sampling pixels at the corner minimap center. - /// If the pixel is close to #DE581B (orange player dot), corner minimap is active. + /// Detect minimap mode by sampling the orange player dot (#DE581B) at both + /// the overlay center and corner center. Returns null if neither is found + /// (loading screen, menu, map transition). /// - private MinimapMode DetectMinimapMode(ScreenFrame screen) + private MinimapMode? DetectMinimapMode(ScreenFrame screen) { - var cx = _config.CornerCenterX; - var cy = _config.CornerCenterY; + if (IsOrangeDot(screen, _config.OverlayCenterX, _config.OverlayCenterY)) + return MinimapMode.Overlay; + if (IsOrangeDot(screen, _config.CornerCenterX, _config.CornerCenterY)) + return MinimapMode.Corner; + return null; + } - // Bounds check + private static bool IsOrangeDot(ScreenFrame screen, int cx, int cy) + { if (cx < 2 || cy < 2 || cx + 2 >= screen.Width || cy + 2 >= screen.Height) - return _detectedMode; + return false; - // Average a 5x5 patch worth of pixels double bSum = 0, gSum = 0, rSum = 0; var count = 0; for (var dy = -2; dy <= 2; dy++) @@ -162,16 +194,13 @@ public class MinimapCapture : IFrameConsumer, IDisposable count++; } - var b = bSum / count; - var g = gSum / count; var r = rSum / count; + var g = gSum / count; + var b = bSum / count; // #DE581B → R=222, G=88, B=27 const int tol = 60; - if (Math.Abs(r - 222) < tol && Math.Abs(g - 88) < tol && Math.Abs(b - 27) < tol) - return MinimapMode.Corner; - - return MinimapMode.Overlay; + return Math.Abs(r - 222) < tol && Math.Abs(g - 88) < tol && Math.Abs(b - 27) < tol; } private Mat BuildWallMask(Mat hsv, Mat playerMask, bool sample = false) diff --git a/src/Poe2Trade.Navigation/WorldMap.cs b/src/Poe2Trade.Navigation/WorldMap.cs index 3768cfb..1fe21ec 100644 --- a/src/Poe2Trade.Navigation/WorldMap.cs +++ b/src/Poe2Trade.Navigation/WorldMap.cs @@ -93,8 +93,17 @@ public class WorldMap : IDisposable // Warmup / re-bootstrap: stitch at current position to seed the canvas if (needsBootstrap) { + // Don't consume warmup slots on empty frames (game still loading minimap) + if (wallCountAfter < 50) + { + _frameCount--; + Log.Information("Warmup waiting for minimap ({Ms:F1}ms)", sw.Elapsed.TotalMilliseconds); + return _position; + } + StitchWithConfidence(classifiedMat, _position, boosted: true, mode: mode); PaintExploredCircle(_position); + LastMatchSucceeded = true; // signal caller to update viewport if (_consecutiveMatchFails >= 30) { Log.Information("Re-bootstrap: mode={Mode} pos=({X:F1},{Y:F1}) frameSize={FS} walls={W} stitch={Ms:F1}ms", @@ -471,6 +480,7 @@ public class WorldMap : IDisposable _position = new MapPosition(_canvasSize / 2.0, _canvasSize / 2.0); _frameCount = 0; _consecutiveMatchFails = 0; + LastMatchSucceeded = false; } /// @@ -520,7 +530,7 @@ public class WorldMap : IDisposable _prevWallMask?.Dispose(); _prevWallMask = null; - // Don't force re-bootstrap — let the match find the correct position first + _frameCount = 0; // force re-warmup with new mode's data } public void Dispose()