adding stash calibration
This commit is contained in:
parent
23c581cff9
commit
3ae65d0e64
17 changed files with 848 additions and 111 deletions
|
|
@ -9,6 +9,7 @@ using Poe2Trade.GameLog;
|
|||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.Screen;
|
||||
using Poe2Trade.Trade;
|
||||
using Poe2Trade.Ui.Overlay;
|
||||
using Poe2Trade.Ui.ViewModels;
|
||||
using Poe2Trade.Ui.Views;
|
||||
|
||||
|
|
@ -66,8 +67,12 @@ public partial class App : Application
|
|||
window.SetConfigStore(store);
|
||||
desktop.MainWindow = window;
|
||||
|
||||
var overlay = new OverlayWindow(bot);
|
||||
overlay.Show();
|
||||
|
||||
desktop.ShutdownRequested += async (_, _) =>
|
||||
{
|
||||
overlay.Close();
|
||||
mainVm.Shutdown();
|
||||
await bot.DisposeAsync();
|
||||
};
|
||||
|
|
|
|||
19
src/Poe2Trade.Ui/Overlay/IOverlayLayer.cs
Normal file
19
src/Poe2Trade.Ui/Overlay/IOverlayLayer.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Avalonia.Media;
|
||||
using Poe2Trade.Navigation;
|
||||
using Poe2Trade.Screen;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay;
|
||||
|
||||
public record OverlayState(
|
||||
IReadOnlyList<DetectedEnemy> Enemies,
|
||||
float InferenceMs,
|
||||
HudSnapshot? Hud,
|
||||
NavigationState NavState,
|
||||
MapPosition NavPosition,
|
||||
bool IsExploring,
|
||||
double Fps);
|
||||
|
||||
public interface IOverlayLayer
|
||||
{
|
||||
void Draw(DrawingContext dc, OverlayState state);
|
||||
}
|
||||
60
src/Poe2Trade.Ui/Overlay/Layers/DebugTextLayer.cs
Normal file
60
src/Poe2Trade.Ui/Overlay/Layers/DebugTextLayer.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay.Layers;
|
||||
|
||||
public class DebugTextLayer : IOverlayLayer
|
||||
{
|
||||
private static readonly Typeface MonoTypeface = new("Consolas");
|
||||
private static readonly IBrush TextBrush = new SolidColorBrush(Color.FromRgb(80, 255, 80));
|
||||
private static readonly IBrush Background = new SolidColorBrush(Color.FromArgb(160, 0, 0, 0));
|
||||
|
||||
private const double PadX = 8;
|
||||
private const double PadY = 4;
|
||||
private const double StartX = 10;
|
||||
private const double StartY = 10;
|
||||
private const double FontSize = 13;
|
||||
|
||||
public void Draw(DrawingContext dc, OverlayState state)
|
||||
{
|
||||
var lines = new List<string>(8)
|
||||
{
|
||||
$"FPS: {state.Fps:F0}",
|
||||
$"Nav: {state.NavState}{(state.IsExploring ? " [exploring]" : "")}",
|
||||
$"Pos: ({state.NavPosition.X:F0}, {state.NavPosition.Y:F0})",
|
||||
$"Enemies: {state.Enemies.Count} YOLO: {state.InferenceMs:F1}ms"
|
||||
};
|
||||
|
||||
if (state.Hud is { Timestamp: > 0 } hud)
|
||||
{
|
||||
lines.Add($"HP: {hud.LifePct:P0} MP: {hud.ManaPct:P0}");
|
||||
}
|
||||
|
||||
// Measure max width for background
|
||||
double maxWidth = 0;
|
||||
double totalHeight = 0;
|
||||
var formatted = new List<FormattedText>(lines.Count);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var ft = new FormattedText(line, System.Globalization.CultureInfo.InvariantCulture,
|
||||
FlowDirection.LeftToRight, MonoTypeface, FontSize, TextBrush);
|
||||
formatted.Add(ft);
|
||||
if (ft.Width > maxWidth) maxWidth = ft.Width;
|
||||
totalHeight += ft.Height;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
dc.DrawRectangle(Background, null,
|
||||
new Rect(StartX - PadX, StartY - PadY,
|
||||
maxWidth + PadX * 2, totalHeight + PadY * 2));
|
||||
|
||||
// Draw text lines
|
||||
var y = StartY;
|
||||
foreach (var ft in formatted)
|
||||
{
|
||||
dc.DrawText(ft, new Point(StartX, y));
|
||||
y += ft.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Poe2Trade.Ui/Overlay/Layers/EnemyBoxLayer.cs
Normal file
36
src/Poe2Trade.Ui/Overlay/Layers/EnemyBoxLayer.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay.Layers;
|
||||
|
||||
public class EnemyBoxLayer : IOverlayLayer
|
||||
{
|
||||
// Pre-allocated pens — zero allocation per frame
|
||||
private static readonly IPen ConfirmedPen = new Pen(Brushes.Red, 2);
|
||||
private static readonly IPen UnconfirmedPen = new Pen(Brushes.Yellow, 2);
|
||||
private static readonly Typeface LabelTypeface = new("Consolas");
|
||||
private static readonly IBrush LabelBackground = new SolidColorBrush(Color.FromArgb(160, 0, 0, 0));
|
||||
|
||||
public void Draw(DrawingContext dc, OverlayState state)
|
||||
{
|
||||
foreach (var enemy in state.Enemies)
|
||||
{
|
||||
var pen = enemy.HealthBarConfirmed ? ConfirmedPen : UnconfirmedPen;
|
||||
var rect = new Rect(enemy.X, enemy.Y, enemy.Width, enemy.Height);
|
||||
dc.DrawRectangle(null, pen, rect);
|
||||
|
||||
// Confidence label above the box
|
||||
var label = $"{enemy.Confidence:P0}";
|
||||
var text = new FormattedText(label, System.Globalization.CultureInfo.InvariantCulture,
|
||||
FlowDirection.LeftToRight, LabelTypeface, 12, pen.Brush);
|
||||
|
||||
var labelX = enemy.X;
|
||||
var labelY = enemy.Y - text.Height - 2;
|
||||
|
||||
// Background for readability
|
||||
dc.DrawRectangle(LabelBackground, null,
|
||||
new Rect(labelX - 1, labelY - 1, text.Width + 2, text.Height + 2));
|
||||
dc.DrawText(text, new Point(labelX, labelY));
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Poe2Trade.Ui/Overlay/Layers/HudInfoLayer.cs
Normal file
47
src/Poe2Trade.Ui/Overlay/Layers/HudInfoLayer.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay.Layers;
|
||||
|
||||
public class HudInfoLayer : IOverlayLayer
|
||||
{
|
||||
private static readonly IBrush LifeBrush = new SolidColorBrush(Color.FromRgb(200, 40, 40));
|
||||
private static readonly IBrush ManaBrush = new SolidColorBrush(Color.FromRgb(40, 80, 200));
|
||||
private static readonly IBrush BarBackground = new SolidColorBrush(Color.FromArgb(140, 20, 20, 20));
|
||||
private static readonly IPen BarBorder = new Pen(Brushes.Gray, 1);
|
||||
private static readonly Typeface ValueTypeface = new("Consolas");
|
||||
|
||||
// Bar dimensions — positioned bottom-center above globe area
|
||||
private const double BarWidth = 200;
|
||||
private const double BarHeight = 16;
|
||||
private const double BarY = 1300; // above the globe at 2560x1440
|
||||
private const double LifeBarX = 1130; // left of center
|
||||
private const double ManaBarX = 1230; // right of center
|
||||
|
||||
public void Draw(DrawingContext dc, OverlayState state)
|
||||
{
|
||||
if (state.Hud == null || state.Hud.Timestamp == 0) return;
|
||||
|
||||
DrawBar(dc, LifeBarX, BarY, state.Hud.LifePct, LifeBrush, state.Hud.Life);
|
||||
DrawBar(dc, ManaBarX, BarY, state.Hud.ManaPct, ManaBrush, state.Hud.Mana);
|
||||
}
|
||||
|
||||
private static void DrawBar(DrawingContext dc, double x, double y, float pct,
|
||||
IBrush fillBrush, Screen.HudValues? values)
|
||||
{
|
||||
var outer = new Rect(x, y, BarWidth, BarHeight);
|
||||
dc.DrawRectangle(BarBackground, BarBorder, outer);
|
||||
|
||||
var fillWidth = BarWidth * Math.Clamp(pct, 0, 1);
|
||||
if (fillWidth > 0)
|
||||
dc.DrawRectangle(fillBrush, null, new Rect(x, y, fillWidth, BarHeight));
|
||||
|
||||
if (values != null)
|
||||
{
|
||||
var label = $"{values.Current}/{values.Max}";
|
||||
var text = new FormattedText(label, System.Globalization.CultureInfo.InvariantCulture,
|
||||
FlowDirection.LeftToRight, ValueTypeface, 11, Brushes.White);
|
||||
dc.DrawText(text, new Point(x + (BarWidth - text.Width) / 2, y + (BarHeight - text.Height) / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Poe2Trade.Ui/Overlay/OverlayCanvas.cs
Normal file
104
src/Poe2Trade.Ui/Overlay/OverlayCanvas.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using System.Diagnostics;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using Poe2Trade.Bot;
|
||||
using Poe2Trade.Navigation;
|
||||
using Poe2Trade.Ui.Overlay.Layers;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay;
|
||||
|
||||
public class OverlayCanvas : Control
|
||||
{
|
||||
private readonly List<IOverlayLayer> _layers = [];
|
||||
private BotOrchestrator? _bot;
|
||||
private DispatcherTimer? _timer;
|
||||
private nint _hwnd;
|
||||
private bool _shown;
|
||||
|
||||
// FPS tracking
|
||||
private readonly Stopwatch _fpsWatch = new();
|
||||
private int _frameCount;
|
||||
private double _fps;
|
||||
|
||||
public void Initialize(BotOrchestrator bot)
|
||||
{
|
||||
_bot = bot;
|
||||
|
||||
_layers.Add(new EnemyBoxLayer());
|
||||
_layers.Add(new HudInfoLayer());
|
||||
_layers.Add(new DebugTextLayer());
|
||||
|
||||
_fpsWatch.Start();
|
||||
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) }; // ~30fps
|
||||
_timer.Tick += OnTick;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void OnTick(object? sender, EventArgs e)
|
||||
{
|
||||
if (_bot == null) return;
|
||||
|
||||
// Lazily grab the HWND once the window is realized
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
var handle = ((Window?)VisualRoot)?.TryGetPlatformHandle();
|
||||
if (handle != null) _hwnd = handle.Handle;
|
||||
}
|
||||
|
||||
// Show/hide overlay based on game focus — use native Win32 calls
|
||||
// to avoid Avalonia's Show() which activates the window and steals focus
|
||||
if (_hwnd != 0)
|
||||
{
|
||||
var focused = _bot.Game.IsGameFocused();
|
||||
if (focused && !_shown)
|
||||
{
|
||||
OverlayNativeMethods.ShowNoActivate(_hwnd);
|
||||
_shown = true;
|
||||
}
|
||||
else if (!focused && _shown)
|
||||
{
|
||||
OverlayNativeMethods.HideWindow(_hwnd);
|
||||
_shown = false;
|
||||
}
|
||||
}
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext dc)
|
||||
{
|
||||
if (_bot == null) return;
|
||||
|
||||
// Update FPS
|
||||
_frameCount++;
|
||||
var elapsed = _fpsWatch.Elapsed.TotalSeconds;
|
||||
if (elapsed >= 1.0)
|
||||
{
|
||||
_fps = _frameCount / elapsed;
|
||||
_frameCount = 0;
|
||||
_fpsWatch.Restart();
|
||||
}
|
||||
|
||||
// Build state snapshot from volatile sources
|
||||
var detection = _bot.EnemyDetector.Latest;
|
||||
var state = new OverlayState(
|
||||
Enemies: detection.Enemies,
|
||||
InferenceMs: detection.InferenceMs,
|
||||
Hud: _bot.HudReader.Current,
|
||||
NavState: _bot.Navigation.State,
|
||||
NavPosition: _bot.Navigation.Position,
|
||||
IsExploring: _bot.Navigation.IsExploring,
|
||||
Fps: _fps);
|
||||
|
||||
foreach (var layer in _layers)
|
||||
layer.Draw(dc, state);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_timer?.Stop();
|
||||
_fpsWatch.Stop();
|
||||
}
|
||||
}
|
||||
36
src/Poe2Trade.Ui/Overlay/OverlayNativeMethods.cs
Normal file
36
src/Poe2Trade.Ui/Overlay/OverlayNativeMethods.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay;
|
||||
|
||||
internal static partial class OverlayNativeMethods
|
||||
{
|
||||
private const int GWL_EXSTYLE = -20;
|
||||
|
||||
internal const int WS_EX_TRANSPARENT = 0x00000020;
|
||||
internal const int WS_EX_LAYERED = 0x00080000;
|
||||
internal const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
internal const int WS_EX_NOACTIVATE = 0x08000000;
|
||||
|
||||
private const int SW_SHOWNOACTIVATE = 4;
|
||||
private const int SW_HIDE = 0;
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
internal static void MakeClickThrough(nint hwnd)
|
||||
{
|
||||
var style = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
|
||||
SetWindowLongPtr(hwnd, GWL_EXSTYLE,
|
||||
style | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE);
|
||||
}
|
||||
|
||||
internal static void ShowNoActivate(nint hwnd) => ShowWindow(hwnd, SW_SHOWNOACTIVATE);
|
||||
internal static void HideWindow(nint hwnd) => ShowWindow(hwnd, SW_HIDE);
|
||||
}
|
||||
13
src/Poe2Trade.Ui/Overlay/OverlayWindow.axaml
Normal file
13
src/Poe2Trade.Ui/Overlay/OverlayWindow.axaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:overlay="using:Poe2Trade.Ui.Overlay"
|
||||
x:Class="Poe2Trade.Ui.Overlay.OverlayWindow"
|
||||
SystemDecorations="None"
|
||||
Background="Transparent"
|
||||
TransparencyLevelHint="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
Width="2560" Height="1440"
|
||||
CanResize="False">
|
||||
<overlay:OverlayCanvas x:Name="Canvas" />
|
||||
</Window>
|
||||
38
src/Poe2Trade.Ui/Overlay/OverlayWindow.axaml.cs
Normal file
38
src/Poe2Trade.Ui/Overlay/OverlayWindow.axaml.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Avalonia.Controls;
|
||||
using Poe2Trade.Bot;
|
||||
|
||||
namespace Poe2Trade.Ui.Overlay;
|
||||
|
||||
public partial class OverlayWindow : Window
|
||||
{
|
||||
private readonly BotOrchestrator _bot = null!;
|
||||
|
||||
// Designer/XAML loader requires parameterless constructor
|
||||
public OverlayWindow() => InitializeComponent();
|
||||
|
||||
public OverlayWindow(BotOrchestrator bot)
|
||||
{
|
||||
_bot = bot;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
|
||||
// Position at top-left corner
|
||||
Position = new Avalonia.PixelPoint(0, 0);
|
||||
|
||||
// Apply Win32 click-through extended styles
|
||||
if (TryGetPlatformHandle() is { } handle)
|
||||
OverlayNativeMethods.MakeClickThrough(handle.Handle);
|
||||
|
||||
Canvas.Initialize(_bot);
|
||||
}
|
||||
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
Canvas.Shutdown();
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.2.3" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Poe2Trade.Bot;
|
||||
using Poe2Trade.Core;
|
||||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.Screen;
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -27,17 +30,9 @@ public partial class DebugViewModel : ObservableObject
|
|||
_bot = bot;
|
||||
}
|
||||
|
||||
private bool EnsureReady()
|
||||
{
|
||||
if (_bot.IsReady) return true;
|
||||
DebugResult = "Bot not started yet. Press Start first.";
|
||||
return false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task TakeScreenshot()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
var path = Path.Combine("debug", $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png");
|
||||
|
|
@ -55,7 +50,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task RunOcr()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
var text = await _bot.Screen.ReadFullScreen();
|
||||
|
|
@ -71,7 +65,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task GoHideout()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
await _bot.Game.FocusGame();
|
||||
|
|
@ -88,7 +81,7 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task FindTextOnScreen()
|
||||
{
|
||||
if (!EnsureReady() || string.IsNullOrWhiteSpace(FindText)) return;
|
||||
if (string.IsNullOrWhiteSpace(FindText)) return;
|
||||
try
|
||||
{
|
||||
var pos = await _bot.Screen.FindTextOnScreen(FindText, fuzzy: true);
|
||||
|
|
@ -106,7 +99,7 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task FindAndClick()
|
||||
{
|
||||
if (!EnsureReady() || string.IsNullOrWhiteSpace(FindText)) return;
|
||||
if (string.IsNullOrWhiteSpace(FindText)) return;
|
||||
try
|
||||
{
|
||||
await _bot.Game.FocusGame();
|
||||
|
|
@ -125,7 +118,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task ClickAt()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
var x = (int)(ClickX ?? 0);
|
||||
var y = (int)(ClickY ?? 0);
|
||||
try
|
||||
|
|
@ -144,7 +136,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task ScanGrid()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
var result = await _bot.Screen.Grid.Scan(SelectedGridLayout);
|
||||
|
|
@ -178,7 +169,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task ClickAnge()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
await _bot.Game.FocusGame();
|
||||
|
|
@ -191,7 +181,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task ClickStash()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
await _bot.Game.FocusGame();
|
||||
|
|
@ -204,7 +193,6 @@ public partial class DebugViewModel : ObservableObject
|
|||
[RelayCommand]
|
||||
private async Task ClickSalvage()
|
||||
{
|
||||
if (!EnsureReady()) return;
|
||||
try
|
||||
{
|
||||
await _bot.Game.FocusGame();
|
||||
|
|
@ -213,4 +201,99 @@ public partial class DebugViewModel : ObservableObject
|
|||
}
|
||||
catch (Exception ex) { DebugResult = $"Failed: {ex.Message}"; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CalibrateStash()
|
||||
{
|
||||
try
|
||||
{
|
||||
var calibrator = new StashCalibrator(_bot.Screen, _bot.Game);
|
||||
DebugResult = "Calibrating stash tabs...";
|
||||
|
||||
// Focus game and open stash
|
||||
await _bot.Game.FocusGame();
|
||||
await Helpers.Sleep(Delays.PostFocus);
|
||||
|
||||
var stashPos = await _bot.Inventory.FindAndClickNameplate("STASH");
|
||||
if (!stashPos.HasValue)
|
||||
{
|
||||
DebugResult = "STASH nameplate not found. Stand near your stash.";
|
||||
return;
|
||||
}
|
||||
await Helpers.Sleep(Delays.PostStashOpen);
|
||||
|
||||
// Calibrate stash
|
||||
var stashCal = await calibrator.CalibrateOpenPanel();
|
||||
|
||||
// Close stash, try shop
|
||||
await _bot.Game.PressEscape();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
|
||||
StashCalibration? shopCal = null;
|
||||
var angePos = await _bot.Inventory.FindAndClickNameplate("ANGE");
|
||||
if (angePos.HasValue)
|
||||
{
|
||||
await Helpers.Sleep(Delays.PostStashOpen);
|
||||
// ANGE opens a dialog — click "Manage Shop" to open shop tabs
|
||||
var managePos = await _bot.Screen.FindTextOnScreen("Manage Shop", fuzzy: true);
|
||||
if (managePos.HasValue)
|
||||
{
|
||||
await _bot.Game.LeftClickAt(managePos.Value.X, managePos.Value.Y);
|
||||
await Helpers.Sleep(Delays.PostStashOpen);
|
||||
}
|
||||
shopCal = await calibrator.CalibrateOpenPanel();
|
||||
await _bot.Game.PressEscape();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
}
|
||||
|
||||
// Save
|
||||
_bot.Store.UpdateSettings(s =>
|
||||
{
|
||||
s.StashCalibration = stashCal;
|
||||
s.ShopCalibration = shopCal;
|
||||
});
|
||||
|
||||
// Format results
|
||||
DebugResult = FormatCalibration(stashCal, shopCal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugResult = $"Calibration failed: {ex.Message}";
|
||||
Log.Error(ex, "Stash calibration failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatCalibration(StashCalibration stash, StashCalibration? shop)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== STASH CALIBRATION ===");
|
||||
FormatTabs(sb, stash.Tabs, indent: "");
|
||||
|
||||
if (shop != null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("=== SHOP CALIBRATION ===");
|
||||
FormatTabs(sb, shop.Tabs, indent: "");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("(Shop: ANGE not found, skipped)");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void FormatTabs(StringBuilder sb, List<StashTabInfo> tabs, string indent)
|
||||
{
|
||||
foreach (var tab in tabs)
|
||||
{
|
||||
var folder = tab.IsFolder ? " [FOLDER]" : "";
|
||||
sb.AppendLine($"{indent}#{tab.Index} \"{tab.Name}\" @ ({tab.ClickX},{tab.ClickY}) grid={tab.GridCols}col{folder}");
|
||||
if (tab.IsFolder)
|
||||
{
|
||||
FormatTabs(sb, tab.SubTabs, indent + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,6 +298,7 @@
|
|||
<Button Content="ANGE" Command="{Binding ClickAngeCommand}" />
|
||||
<Button Content="STASH" Command="{Binding ClickStashCommand}" />
|
||||
<Button Content="SALVAGE" Command="{Binding ClickSalvageCommand}" />
|
||||
<Button Content="Calibrate Stash" Command="{Binding CalibrateStashCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue