initial BFS movement
This commit is contained in:
parent
25257e1517
commit
490fb8bdba
7 changed files with 258 additions and 101 deletions
|
|
@ -236,6 +236,32 @@ public class BotOrchestrator : IAsyncDisposable
|
||||||
Log.Information("Bot started");
|
Log.Information("Bot started");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StartMapping()
|
||||||
|
{
|
||||||
|
LogWatcher.Start();
|
||||||
|
await Game.FocusGame();
|
||||||
|
|
||||||
|
Navigation.StateChanged += _ => UpdateExecutorState();
|
||||||
|
_started = true;
|
||||||
|
|
||||||
|
Emit("info", "Starting map exploration...");
|
||||||
|
State = "Exploring";
|
||||||
|
_ = Navigation.RunExploreLoop().ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsFaulted)
|
||||||
|
{
|
||||||
|
Log.Error(t.Exception!, "Explore loop failed");
|
||||||
|
Emit("error", $"Explore loop failed: {t.Exception?.InnerException?.Message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Emit("info", "Exploration finished");
|
||||||
|
}
|
||||||
|
State = "Idle";
|
||||||
|
StatusUpdated?.Invoke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
Log.Information("Shutting down bot...");
|
Log.Information("Shutting down bot...");
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,6 @@ public class GameController : IGameController
|
||||||
public Task HoldCtrl() => _input.KeyDown(InputSender.VK.CONTROL);
|
public Task HoldCtrl() => _input.KeyDown(InputSender.VK.CONTROL);
|
||||||
public Task ReleaseCtrl() => _input.KeyUp(InputSender.VK.CONTROL);
|
public Task ReleaseCtrl() => _input.KeyUp(InputSender.VK.CONTROL);
|
||||||
public Task ToggleMinimap() => _input.PressKey(InputSender.VK.TAB);
|
public Task ToggleMinimap() => _input.PressKey(InputSender.VK.TAB);
|
||||||
|
public Task KeyDown(int vkCode) => _input.KeyDown(vkCode);
|
||||||
|
public Task KeyUp(int vkCode) => _input.KeyUp(vkCode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,6 @@ public interface IGameController
|
||||||
Task HoldCtrl();
|
Task HoldCtrl();
|
||||||
Task ReleaseCtrl();
|
Task ReleaseCtrl();
|
||||||
Task ToggleMinimap();
|
Task ToggleMinimap();
|
||||||
|
Task KeyDown(int vkCode);
|
||||||
|
Task KeyUp(int vkCode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ public class InputSender
|
||||||
public const int A = 0x41;
|
public const int A = 0x41;
|
||||||
public const int C = 0x43;
|
public const int C = 0x43;
|
||||||
public const int I = 0x49;
|
public const int I = 0x49;
|
||||||
|
public const int W = 0x57;
|
||||||
|
public const int S = 0x53;
|
||||||
|
public const int D = 0x44;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PressKey(int vkCode)
|
public async Task PressKey(int vkCode)
|
||||||
|
|
|
||||||
|
|
@ -58,60 +58,61 @@ public class NavigationExecutor : IDisposable
|
||||||
_stopped = false;
|
_stopped = false;
|
||||||
Log.Information("Starting explore loop");
|
Log.Information("Starting explore loop");
|
||||||
|
|
||||||
// Open minimap overlay (Tab)
|
var lastMoveTime = 0L;
|
||||||
await _game.ToggleMinimap();
|
var lastClickTime = 0L;
|
||||||
await Helpers.Sleep(300);
|
var heldKeys = new HashSet<int>(); // currently held WASD keys
|
||||||
|
|
||||||
var lastMoveTime = long.MinValue;
|
try
|
||||||
|
|
||||||
while (!_stopped)
|
|
||||||
{
|
{
|
||||||
var frameStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
while (!_stopped)
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
// 1. Capture + track every frame (~30 fps)
|
var frameStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
SetState(NavigationState.Capturing);
|
|
||||||
using var frame = _capture.CaptureFrame();
|
try
|
||||||
if (frame == null)
|
|
||||||
{
|
{
|
||||||
Log.Warning("Failed to capture minimap frame");
|
// 1. Capture + track every frame
|
||||||
await Helpers.Sleep(_config.CaptureIntervalMs);
|
SetState(NavigationState.Capturing);
|
||||||
continue;
|
using var frame = _capture.CaptureFrame();
|
||||||
}
|
if (frame == null)
|
||||||
|
|
||||||
SetState(NavigationState.Processing);
|
|
||||||
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask);
|
|
||||||
if (_worldMap.LastMatchSucceeded)
|
|
||||||
_capture.CommitWallColors();
|
|
||||||
|
|
||||||
// Stuck detection: position hasn't moved enough over several frames
|
|
||||||
if (_lastPosition != null)
|
|
||||||
{
|
|
||||||
var dx = pos.X - _lastPosition.X;
|
|
||||||
var dy = pos.Y - _lastPosition.Y;
|
|
||||||
if (Math.Sqrt(dx * dx + dy * dy) < _config.StuckThreshold)
|
|
||||||
_stuckCounter++;
|
|
||||||
else
|
|
||||||
_stuckCounter = 0;
|
|
||||||
}
|
|
||||||
_lastPosition = pos;
|
|
||||||
|
|
||||||
// 2. Movement decisions at slower rate
|
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
||||||
if (now - lastMoveTime >= _config.MovementWaitMs)
|
|
||||||
{
|
|
||||||
lastMoveTime = now;
|
|
||||||
|
|
||||||
if (_stuckCounter >= _config.StuckFrameCount)
|
|
||||||
{
|
{
|
||||||
SetState(NavigationState.Stuck);
|
Log.Warning("Failed to capture minimap frame");
|
||||||
Log.Information("Stuck detected, clicking random direction");
|
await Helpers.Sleep(_config.CaptureIntervalMs);
|
||||||
await ClickRandomDirection();
|
continue;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
SetState(NavigationState.Processing);
|
||||||
|
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask);
|
||||||
|
if (_worldMap.LastMatchSucceeded)
|
||||||
|
_capture.CommitWallColors();
|
||||||
|
|
||||||
|
// Stuck detection
|
||||||
|
if (_lastPosition != null)
|
||||||
{
|
{
|
||||||
SetState(NavigationState.Planning);
|
var dx = pos.X - _lastPosition.X;
|
||||||
|
var dy = pos.Y - _lastPosition.Y;
|
||||||
|
if (Math.Sqrt(dx * dx + dy * dy) < _config.StuckThreshold)
|
||||||
|
_stuckCounter++;
|
||||||
|
else
|
||||||
|
_stuckCounter = 0;
|
||||||
|
}
|
||||||
|
_lastPosition = pos;
|
||||||
|
|
||||||
|
// 2. Movement decisions (faster re-evaluation when stuck)
|
||||||
|
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
var moveInterval = _stuckCounter >= _config.StuckFrameCount
|
||||||
|
? 200
|
||||||
|
: _config.MovementWaitMs;
|
||||||
|
|
||||||
|
if (now - lastMoveTime >= moveInterval)
|
||||||
|
{
|
||||||
|
lastMoveTime = now;
|
||||||
|
|
||||||
|
if (_stuckCounter >= _config.StuckFrameCount)
|
||||||
|
SetState(NavigationState.Stuck);
|
||||||
|
else
|
||||||
|
SetState(NavigationState.Planning);
|
||||||
|
|
||||||
|
// BFS finds path through explored cells to nearest frontier
|
||||||
var direction = _worldMap.FindNearestUnexplored(pos);
|
var direction = _worldMap.FindNearestUnexplored(pos);
|
||||||
|
|
||||||
if (direction == null)
|
if (direction == null)
|
||||||
|
|
@ -122,23 +123,45 @@ public class NavigationExecutor : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
SetState(NavigationState.Moving);
|
SetState(NavigationState.Moving);
|
||||||
await ClickToMove(direction.Value.dirX, direction.Value.dirY);
|
await UpdateWasdKeys(heldKeys, direction.Value.dirX, direction.Value.dirY);
|
||||||
|
|
||||||
|
if (_stuckCounter >= _config.StuckFrameCount)
|
||||||
|
_stuckCounter = 0; // reset after re-routing
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Occasional combat clicks near screen center
|
||||||
|
if (now - lastClickTime >= 1000 + Rng.Next(1000))
|
||||||
|
{
|
||||||
|
lastClickTime = now;
|
||||||
|
var cx = 1280 + Rng.Next(-150, 150);
|
||||||
|
var cy = 720 + Rng.Next(-150, 150);
|
||||||
|
await _game.LeftClickAt(cx, cy);
|
||||||
|
await Helpers.Sleep(100 + Rng.Next(100));
|
||||||
|
cx = 1280 + Rng.Next(-150, 150);
|
||||||
|
cy = 720 + Rng.Next(-150, 150);
|
||||||
|
await _game.RightClickAt(cx, cy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
Log.Error(ex, "Error in explore loop");
|
||||||
Log.Error(ex, "Error in explore loop");
|
SetState(NavigationState.Failed);
|
||||||
SetState(NavigationState.Failed);
|
await Helpers.Sleep(1000);
|
||||||
await Helpers.Sleep(1000);
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep remainder of frame interval
|
// Sleep remainder of frame interval
|
||||||
var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - frameStart;
|
var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - frameStart;
|
||||||
var sleepMs = _config.CaptureIntervalMs - (int)elapsed;
|
var sleepMs = _config.CaptureIntervalMs - (int)elapsed;
|
||||||
if (sleepMs > 0)
|
if (sleepMs > 0)
|
||||||
await Helpers.Sleep(sleepMs);
|
await Helpers.Sleep(sleepMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Always release held keys
|
||||||
|
foreach (var key in heldKeys)
|
||||||
|
await _game.KeyUp(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_state != NavigationState.Completed)
|
if (_state != NavigationState.Completed)
|
||||||
|
|
@ -147,6 +170,39 @@ public class NavigationExecutor : IDisposable
|
||||||
Log.Information("Explore loop ended");
|
Log.Information("Explore loop ended");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a direction vector to WASD key holds. Releases keys no longer needed,
|
||||||
|
/// presses new ones. Supports diagonal movement (two keys at once).
|
||||||
|
/// </summary>
|
||||||
|
private async Task UpdateWasdKeys(HashSet<int> held, double dirX, double dirY)
|
||||||
|
{
|
||||||
|
var wanted = new HashSet<int>();
|
||||||
|
|
||||||
|
// Threshold for diagonal: if both components are significant, hold both keys
|
||||||
|
const double threshold = 0.3;
|
||||||
|
if (dirY < -threshold) wanted.Add(InputSender.VK.W); // up
|
||||||
|
if (dirY > threshold) wanted.Add(InputSender.VK.S); // down
|
||||||
|
if (dirX < -threshold) wanted.Add(InputSender.VK.A); // left
|
||||||
|
if (dirX > threshold) wanted.Add(InputSender.VK.D); // right
|
||||||
|
|
||||||
|
// If direction is too weak, default to W
|
||||||
|
if (wanted.Count == 0) wanted.Add(InputSender.VK.W);
|
||||||
|
|
||||||
|
// Release keys no longer wanted
|
||||||
|
foreach (var key in held.Except(wanted).ToList())
|
||||||
|
{
|
||||||
|
await _game.KeyUp(key);
|
||||||
|
held.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press newly wanted keys
|
||||||
|
foreach (var key in wanted.Except(held).ToList())
|
||||||
|
{
|
||||||
|
await _game.KeyDown(key);
|
||||||
|
held.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ClickToMove(double dirX, double dirY)
|
private async Task ClickToMove(double dirX, double dirY)
|
||||||
{
|
{
|
||||||
// Player is at minimap center on screen; click offset from center
|
// Player is at minimap center on screen; click offset from center
|
||||||
|
|
@ -169,6 +225,7 @@ public class NavigationExecutor : IDisposable
|
||||||
await ClickToMove(Math.Cos(angle), Math.Sin(angle));
|
await ClickToMove(Math.Cos(angle), Math.Sin(angle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsExploring => _state != NavigationState.Idle && _state != NavigationState.Completed && _state != NavigationState.Failed;
|
||||||
public MapPosition Position => _worldMap.Position;
|
public MapPosition Position => _worldMap.Position;
|
||||||
public byte[] GetMapSnapshot() => _worldMap.GetMapSnapshot();
|
public byte[] GetMapSnapshot() => _worldMap.GetMapSnapshot();
|
||||||
public byte[] GetViewportSnapshot(int viewSize = 400) => _worldMap.GetViewportSnapshot(_worldMap.Position, viewSize);
|
public byte[] GetViewportSnapshot(int viewSize = 400) => _worldMap.GetViewportSnapshot(_worldMap.Position, viewSize);
|
||||||
|
|
|
||||||
|
|
@ -323,57 +323,119 @@ public class WorldMap : IDisposable
|
||||||
return totalBlocks > 0 ? (double)cleanBlocks / totalBlocks : 1.0;
|
return totalBlocks > 0 ? (double)cleanBlocks / totalBlocks : 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (double dirX, double dirY)? FindNearestUnexplored(MapPosition pos, int searchRadius = 200)
|
/// <summary>
|
||||||
|
/// BFS through walkable (Explored) cells to find the nearest frontier
|
||||||
|
/// (Explored cell adjacent to Unknown/Fog). Returns direction toward the
|
||||||
|
/// first step on the shortest path, respecting walls.
|
||||||
|
/// </summary>
|
||||||
|
public (double dirX, double dirY)? FindNearestUnexplored(MapPosition pos, int searchRadius = 400)
|
||||||
{
|
{
|
||||||
var cx = (int)Math.Round(pos.X);
|
var cx = (int)Math.Round(pos.X);
|
||||||
var cy = (int)Math.Round(pos.Y);
|
var cy = (int)Math.Round(pos.Y);
|
||||||
|
|
||||||
var bestAngle = double.NaN;
|
// BFS at half resolution for speed (step=2 → ~200x200 effective grid for r=400)
|
||||||
var bestScore = 0;
|
const int step = 2;
|
||||||
const int sectorCount = 16;
|
var size = _config.CanvasSize;
|
||||||
var fogRadius = _config.CaptureSize / 2;
|
var rr = searchRadius / step;
|
||||||
|
var gridW = 2 * rr + 1;
|
||||||
|
|
||||||
for (var sector = 0; sector < sectorCount; sector++)
|
// Visited grid + parent tracking (encode parent as single int)
|
||||||
|
var visited = new bool[gridW * gridW];
|
||||||
|
var parentX = new short[gridW * gridW];
|
||||||
|
var parentY = new short[gridW * gridW];
|
||||||
|
|
||||||
|
var queue = new Queue<(int gx, int gy)>(4096);
|
||||||
|
var startGx = rr;
|
||||||
|
var startGy = rr;
|
||||||
|
visited[startGy * gridW + startGx] = true;
|
||||||
|
parentX[startGy * gridW + startGx] = -1;
|
||||||
|
parentY[startGy * gridW + startGx] = -1;
|
||||||
|
queue.Enqueue((startGx, startGy));
|
||||||
|
|
||||||
|
// 8-connected neighbors
|
||||||
|
ReadOnlySpan<int> dxs = [-1, 0, 1, -1, 1, -1, 0, 1];
|
||||||
|
ReadOnlySpan<int> dys = [-1, -1, -1, 0, 0, 1, 1, 1];
|
||||||
|
|
||||||
|
int foundGx = -1, foundGy = -1;
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
{
|
{
|
||||||
var angle = 2 * Math.PI * sector / sectorCount;
|
var (gx, gy) = queue.Dequeue();
|
||||||
var score = 0;
|
|
||||||
|
|
||||||
for (var r = fogRadius - 20; r <= fogRadius + searchRadius; r += 5)
|
// Map grid coords back to canvas coords
|
||||||
|
var wx = cx + (gx - rr) * step;
|
||||||
|
var wy = cy + (gy - rr) * step;
|
||||||
|
|
||||||
|
// Check if this explored cell borders Unknown/Fog (= frontier)
|
||||||
|
if (gx != startGx || gy != startGy) // skip player cell
|
||||||
{
|
{
|
||||||
for (var spread = -15; spread <= 15; spread += 5)
|
for (var d = 0; d < 8; d++)
|
||||||
{
|
{
|
||||||
var sampleAngle = angle + spread * Math.PI / 180;
|
var nx = wx + dxs[d] * step;
|
||||||
var sx = cx + (int)(r * Math.Cos(sampleAngle));
|
var ny = wy + dys[d] * step;
|
||||||
var sy = cy + (int)(r * Math.Sin(sampleAngle));
|
if (nx < 0 || nx >= size || ny < 0 || ny >= size) continue;
|
||||||
|
var neighbor = _canvas.At<byte>(ny, nx);
|
||||||
if (sx < 0 || sx >= _config.CanvasSize || sy < 0 || sy >= _config.CanvasSize)
|
if (neighbor == (byte)MapCell.Unknown || neighbor == (byte)MapCell.Fog)
|
||||||
continue;
|
{
|
||||||
|
foundGx = gx;
|
||||||
var cell = _canvas.At<byte>(sy, sx);
|
foundGy = gy;
|
||||||
if (cell == (byte)MapCell.Fog)
|
goto Found;
|
||||||
score += 2; // prefer visible fog (we know there's unexplored area)
|
}
|
||||||
else if (cell == (byte)MapCell.Unknown)
|
|
||||||
score++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (score > bestScore)
|
// Expand to walkable neighbors
|
||||||
|
for (var d = 0; d < 8; d++)
|
||||||
{
|
{
|
||||||
bestScore = score;
|
var ngx = gx + dxs[d];
|
||||||
bestAngle = angle;
|
var ngy = gy + dys[d];
|
||||||
|
if (ngx < 0 || ngx >= gridW || ngy < 0 || ngy >= gridW) continue;
|
||||||
|
|
||||||
|
var idx = ngy * gridW + ngx;
|
||||||
|
if (visited[idx]) continue;
|
||||||
|
|
||||||
|
var nwx = cx + (ngx - rr) * step;
|
||||||
|
var nwy = cy + (ngy - rr) * step;
|
||||||
|
if (nwx < 0 || nwx >= size || nwy < 0 || nwy >= size) continue;
|
||||||
|
|
||||||
|
var cell = _canvas.At<byte>(nwy, nwx);
|
||||||
|
if (cell != (byte)MapCell.Explored) continue; // only walk through explored
|
||||||
|
|
||||||
|
visited[idx] = true;
|
||||||
|
parentX[idx] = (short)gx;
|
||||||
|
parentY[idx] = (short)gy;
|
||||||
|
queue.Enqueue((ngx, ngy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestScore == 0 || double.IsNaN(bestAngle))
|
Log.Information("BFS: no reachable frontier within {Radius}px", searchRadius);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Found:
|
||||||
|
// Trace back to first step from start
|
||||||
|
var traceX = foundGx;
|
||||||
|
var traceY = foundGy;
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
Log.Information("No unexplored area found within search radius");
|
var idx = traceY * gridW + traceX;
|
||||||
return null;
|
var px = parentX[idx];
|
||||||
|
var py = parentY[idx];
|
||||||
|
if (px == startGx && py == startGy)
|
||||||
|
break; // traceX/traceY is the first step
|
||||||
|
traceX = px;
|
||||||
|
traceY = py;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dirX = Math.Cos(bestAngle);
|
var dirX = (double)(traceX - startGx);
|
||||||
var dirY = Math.Sin(bestAngle);
|
var dirY = (double)(traceY - startGy);
|
||||||
Log.Debug("Best exploration direction: angle={Angle:F1}deg score={Score}",
|
var len = Math.Sqrt(dirX * dirX + dirY * dirY);
|
||||||
bestAngle * 180 / Math.PI, bestScore);
|
if (len < 0.001) return (1, 0); // shouldn't happen
|
||||||
|
|
||||||
|
dirX /= len;
|
||||||
|
dirY /= len;
|
||||||
|
|
||||||
|
var dist = Math.Sqrt((foundGx - startGx) * (foundGx - startGx) + (foundGy - startGy) * (foundGy - startGy)) * step;
|
||||||
|
Log.Debug("BFS: frontier at {Dist:F0}px, first step dir=({Dx:F2},{Dy:F2})", dist, dirX, dirY);
|
||||||
return (dirX, dirY);
|
return (dirX, dirY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
private static extern short GetAsyncKeyState(int vKey);
|
private static extern short GetAsyncKeyState(int vKey);
|
||||||
private const int VK_F12 = 0x7B;
|
private const int VK_END = 0x23;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _state = "Idle";
|
private string _state = "Idle";
|
||||||
|
|
@ -130,7 +130,10 @@ public partial class MainWindowViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _bot.Start([]);
|
if (_bot.Mode == BotMode.Mapping)
|
||||||
|
await _bot.StartMapping();
|
||||||
|
else
|
||||||
|
await _bot.Start([]);
|
||||||
IsStarted = true;
|
IsStarted = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -186,10 +189,10 @@ public partial class MainWindowViewModel : ObservableObject
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// F12 hotkey — edge-detect (trigger once per press)
|
// F12 hotkey — edge-detect (trigger once per press)
|
||||||
var f12Down = (GetAsyncKeyState(VK_F12) & 0x8000) != 0;
|
var endDown = (GetAsyncKeyState(VK_END) & 0x8000) != 0;
|
||||||
if (f12Down && !f12WasDown)
|
if (endDown && !f12WasDown)
|
||||||
{
|
{
|
||||||
Log.Information("F12 pressed — emergency stop");
|
Log.Information("END pressed — emergency stop");
|
||||||
await _bot.Navigation.Stop();
|
await _bot.Navigation.Stop();
|
||||||
_bot.Pause();
|
_bot.Pause();
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
|
|
@ -198,10 +201,12 @@ public partial class MainWindowViewModel : ObservableObject
|
||||||
State = "Stopped (F12)";
|
State = "Stopped (F12)";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
f12WasDown = f12Down;
|
f12WasDown = endDown;
|
||||||
|
|
||||||
// Minimap capture + display
|
// Minimap display: if explore loop owns capture, just render viewport
|
||||||
var bytes = _bot.Navigation.ProcessFrame(SelectedMinimapStage);
|
var bytes = _bot.Navigation.IsExploring
|
||||||
|
? _bot.Navigation.GetViewportSnapshot()
|
||||||
|
: _bot.Navigation.ProcessFrame(SelectedMinimapStage);
|
||||||
if (bytes != null)
|
if (bytes != null)
|
||||||
{
|
{
|
||||||
var bmp = new Bitmap(new MemoryStream(bytes));
|
var bmp = new Bitmap(new MemoryStream(bytes));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue