using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Poe2Trade.Bot; using Poe2Trade.Core; using Poe2Trade.Inventory; using Serilog; namespace Poe2Trade.Ui.ViewModels; public partial class SettingsViewModel : ObservableObject { private readonly BotOrchestrator _bot; [ObservableProperty] private string _poe2LogPath = ""; [ObservableProperty] private string _windowTitle = ""; [ObservableProperty] private decimal? _travelTimeoutMs = 15000; [ObservableProperty] private decimal? _stashScanTimeoutMs = 10000; [ObservableProperty] private decimal? _waitForMoreItemsMs = 20000; [ObservableProperty] private decimal? _betweenTradesDelayMs = 5000; [ObservableProperty] private bool _headless = true; [ObservableProperty] private bool _showHudDebug; [ObservableProperty] private string _ocrEngine = "WinOCR"; [ObservableProperty] private bool _isSaved; [ObservableProperty] private string _calibrationStatus = ""; [ObservableProperty] private string _stashCalibratedAt = ""; [ObservableProperty] private string _shopCalibratedAt = ""; public static string[] OcrEngineOptions { get; } = ["WinOCR", "OneOCR", "EasyOCR"]; public ObservableCollection StashTabs { get; } = []; public ObservableCollection ShopTabs { get; } = []; public SettingsViewModel(BotOrchestrator bot) { _bot = bot; LoadFromConfig(); LoadTabs(); } private void LoadFromConfig() { var s = _bot.Store.Settings; Poe2LogPath = s.Poe2LogPath; WindowTitle = s.Poe2WindowTitle; TravelTimeoutMs = s.TravelTimeoutMs; StashScanTimeoutMs = s.StashScanTimeoutMs; WaitForMoreItemsMs = s.WaitForMoreItemsMs; BetweenTradesDelayMs = s.BetweenTradesDelayMs; Headless = s.Headless; ShowHudDebug = s.ShowHudDebug; OcrEngine = s.OcrEngine; } private void LoadTabs() { var s = _bot.Store.Settings; StashTabs.Clear(); if (s.StashCalibration != null) { foreach (var tab in s.StashCalibration.Tabs) StashTabs.Add(new StashTabViewModel(tab)); StashCalibratedAt = FormatTimestamp(s.StashCalibration.CalibratedAt); } else { StashCalibratedAt = "Not calibrated"; } ShopTabs.Clear(); if (s.ShopCalibration != null) { foreach (var tab in s.ShopCalibration.Tabs) ShopTabs.Add(new StashTabViewModel(tab)); ShopCalibratedAt = FormatTimestamp(s.ShopCalibration.CalibratedAt); } else { ShopCalibratedAt = "Not calibrated"; } } private static string FormatTimestamp(long unixMs) { if (unixMs == 0) return "Not calibrated"; var dt = DateTimeOffset.FromUnixTimeMilliseconds(unixMs).LocalDateTime; return dt.ToString("yyyy-MM-dd HH:mm"); } [RelayCommand] private void SaveSettings() { _bot.Store.UpdateSettings(s => { s.Poe2LogPath = Poe2LogPath; s.Poe2WindowTitle = WindowTitle; s.TravelTimeoutMs = (int)(TravelTimeoutMs ?? 15000); s.StashScanTimeoutMs = (int)(StashScanTimeoutMs ?? 10000); s.WaitForMoreItemsMs = (int)(WaitForMoreItemsMs ?? 20000); s.BetweenTradesDelayMs = (int)(BetweenTradesDelayMs ?? 5000); s.Headless = Headless; s.ShowHudDebug = ShowHudDebug; s.OcrEngine = OcrEngine; }); IsSaved = true; } [RelayCommand] private void SaveTabs() { _bot.Store.UpdateSettings(s => { // Models are already updated via write-through in StashTabViewModel // Just trigger a save if (s.StashCalibration != null) s.StashCalibration = s.StashCalibration; if (s.ShopCalibration != null) s.ShopCalibration = s.ShopCalibration; }); CalibrationStatus = "Tabs saved!"; } [RelayCommand] private async Task CalibrateStash() { try { var calibrator = new StashCalibrator(_bot.Screen, _bot.Game); CalibrationStatus = "Calibrating stash..."; await _bot.Game.FocusGame(); await Helpers.RandomDelay(150, 300); var pos = await _bot.Inventory.FindAndClickNameplate("STASH"); if (!pos.HasValue) { CalibrationStatus = "STASH not found. Stand near your stash."; return; } await Helpers.RandomDelay(300, 500); var cal = await calibrator.CalibrateOpenPanel(); await _bot.Game.PressEscape(); await Helpers.RandomDelay(200, 400); _bot.Store.UpdateSettings(s => s.StashCalibration = cal); LoadTabs(); CalibrationStatus = $"Stash calibrated — {cal.Tabs.Count} tabs found."; } catch (Exception ex) { CalibrationStatus = $"Stash calibration failed: {ex.Message}"; Log.Error(ex, "Stash calibration failed"); } } [RelayCommand] private async Task CalibrateShop() { try { var calibrator = new StashCalibrator(_bot.Screen, _bot.Game); CalibrationStatus = "Calibrating shop..."; await _bot.Game.FocusGame(); await Helpers.RandomDelay(150, 300); var pos = await _bot.Inventory.FindAndClickNameplate("ANGE"); if (!pos.HasValue) { CalibrationStatus = "ANGE not found. Stand near the vendor."; return; } await Helpers.RandomDelay(800, 1200); // ANGE opens a dialog — click "Manage Shop" var dialogRegion = new Region(pos.Value.X, pos.Value.Y, 460, 600); var managePos = await _bot.Screen.FindTextInRegion(dialogRegion, "Manage"); if (managePos.HasValue) { await _bot.Game.LeftClickAt(managePos.Value.X, managePos.Value.Y); await Helpers.RandomDelay(300, 500); } else { Log.Warning("'Manage Shop' not found in dialog region, saving debug capture"); await _bot.Screen.SaveRegion(dialogRegion, "debug/calibrate-dialog.png"); } var cal = await calibrator.CalibrateOpenPanel(firstFolderOnly: true); await _bot.Game.PressEscape(); await Helpers.RandomDelay(200, 400); _bot.Store.UpdateSettings(s => s.ShopCalibration = cal); LoadTabs(); CalibrationStatus = $"Shop calibrated — {cal.Tabs.Count} tabs found."; } catch (Exception ex) { CalibrationStatus = $"Shop calibration failed: {ex.Message}"; Log.Error(ex, "Shop calibration failed"); } } partial void OnPoe2LogPathChanged(string value) => IsSaved = false; partial void OnWindowTitleChanged(string value) => IsSaved = false; partial void OnTravelTimeoutMsChanged(decimal? value) => IsSaved = false; partial void OnStashScanTimeoutMsChanged(decimal? value) => IsSaved = false; partial void OnWaitForMoreItemsMsChanged(decimal? value) => IsSaved = false; partial void OnBetweenTradesDelayMsChanged(decimal? value) => IsSaved = false; partial void OnHeadlessChanged(bool value) => IsSaved = false; partial void OnShowHudDebugChanged(bool value) => IsSaved = false; partial void OnOcrEngineChanged(string value) => IsSaved = false; }