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()