using System.Text.Json; using System.Text.Json.Serialization; using Serilog; namespace Nexus.Core; public class SavedLink { public string Url { get; set; } = ""; public string Name { get; set; } = ""; public bool Active { get; set; } = true; public LinkMode Mode { get; set; } = LinkMode.Live; public PostAction PostAction { get; set; } = PostAction.Stash; public string AddedAt { get; set; } = DateTime.UtcNow.ToString("o"); } public class SavedSettings { public bool Paused { get; set; } public List Links { get; set; } = []; public string GameLogPath { get; set; } = @"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\logs\Client.txt"; public string GameWindowTitle { get; set; } = "Path of Exile 2"; public string BrowserUserDataDir { get; set; } = "./browser-data"; public int TravelTimeoutMs { get; set; } = 15000; public int StashScanTimeoutMs { get; set; } = 10000; public int WaitForMoreItemsMs { get; set; } = 20000; public int BetweenTradesDelayMs { get; set; } = 5000; public double? WindowX { get; set; } public double? WindowY { get; set; } public double? WindowWidth { get; set; } public double? WindowHeight { get; set; } public bool Headless { get; set; } = true; public BotMode Mode { get; set; } = BotMode.Trading; public MapType MapType { get; set; } = MapType.TrialOfChaos; public StashCalibration? StashCalibration { get; set; } public StashCalibration? ShopCalibration { get; set; } public bool ShowHudDebug { get; set; } public string OcrEngine { get; set; } = "WinOCR"; public KulemakSettings Kulemak { get; set; } = new(); public DiamondSettings Diamond { get; set; } = new(); public List Crafts { get; set; } = []; public List CurrencyPositions { get; set; } = []; public string SelectedLeague { get; set; } = ""; } public class DiamondPriceConfig { public string ItemName { get; set; } = ""; public string DisplayName { get; set; } = ""; public double MaxDivinePrice { get; set; } public bool Enabled { get; set; } = true; } public class DiamondSettings { public static readonly Dictionary KnownDiamonds = new() { ["SanctumJewel"] = "Time-Lost Diamond", ["SacredFlameJewel"] = "Prism of Belief", ["TrialmasterJewel"] = "The Adorned", ["DeliriumJewel"] = "Megalomaniac Diamond", ["ApostatesHeart"] = "Heart of the Well", }; public List Prices { get; set; } = DefaultPrices(); private static List DefaultPrices() => KnownDiamonds.Select(kv => new DiamondPriceConfig { ItemName = kv.Key, DisplayName = kv.Value, MaxDivinePrice = 0, Enabled = false, }).ToList(); /// Ensure all known diamonds exist in the list (adds missing ones as disabled). public void BackfillKnown() { // Remove blank entries Prices.RemoveAll(p => string.IsNullOrWhiteSpace(p.ItemName)); var existing = new HashSet(Prices.Select(p => p.ItemName), StringComparer.OrdinalIgnoreCase); foreach (var kv in KnownDiamonds) { if (existing.Contains(kv.Key)) continue; Prices.Add(new DiamondPriceConfig { ItemName = kv.Key, DisplayName = kv.Value, MaxDivinePrice = 0, Enabled = false, }); } } } public class KulemakSettings { public bool Enabled { get; set; } public string InvitationTabPath { get; set; } = ""; public string LootTabPath { get; set; } = ""; public int InvitationCount { get; set; } = 15; public int RunCount { get; set; } = 15; } public class ConfigStore { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; private readonly string _filePath; private SavedSettings _data; public ConfigStore(string? configPath = null) { _filePath = configPath ?? Path.GetFullPath("config.json"); _data = Load(); } public SavedSettings Settings => _data; public IReadOnlyList Links => _data.Links; public void AddLink(string url, string name = "", LinkMode mode = LinkMode.Live, PostAction? postAction = null) { url = StripLive(url); var existing = _data.Links.FirstOrDefault(l => l.Url == url); if (existing != null) { // Update mode/postAction/name if re-added with different settings existing.Mode = mode; existing.PostAction = postAction ?? (mode == LinkMode.Scrap ? PostAction.Salvage : PostAction.Stash); if (!string.IsNullOrEmpty(name)) existing.Name = name; Save(); return; } _data.Links.Add(new SavedLink { Url = url, Name = name, Active = true, Mode = mode, PostAction = postAction ?? (mode == LinkMode.Scrap ? PostAction.Salvage : PostAction.Stash), AddedAt = DateTime.UtcNow.ToString("o") }); Save(); } public void RemoveLink(string url) { _data.Links.RemoveAll(l => l.Url == url); Save(); } public void RemoveLinkById(string id) { _data.Links.RemoveAll(l => l.Url.Split('/').Last() == id); Save(); } public SavedLink? UpdateLinkById(string id, Action update) { var link = _data.Links.FirstOrDefault(l => l.Url.Split('/').Last() == id); if (link == null) return null; update(link); Save(); return link; } public void SetPaused(bool paused) { _data.Paused = paused; Save(); } public void UpdateSettings(Action update) { update(_data); Save(); } public void Save() { try { var json = JsonSerializer.Serialize(_data, JsonOptions); File.WriteAllText(_filePath, json); } catch (Exception ex) { Log.Error(ex, "Failed to save config.json to {Path}", _filePath); } } private SavedSettings Load() { if (!File.Exists(_filePath)) { Log.Information("No config.json found at {Path}, using defaults", _filePath); return new SavedSettings(); } try { var raw = File.ReadAllText(_filePath); // Migrate: BossRun was removed from BotMode, now it's MapType.Kulemak if (raw.Contains("\"bossRun\"") || raw.Contains("\"BossRun\"")) { const System.Text.RegularExpressions.RegexOptions ic = System.Text.RegularExpressions.RegexOptions.IgnoreCase; // Mode enum: bossRun → mapping using var doc = JsonDocument.Parse(raw); if (doc.RootElement.TryGetProperty("Mode", out var modeProp) && modeProp.GetString()?.Equals("bossRun", StringComparison.OrdinalIgnoreCase) == true) { raw = System.Text.RegularExpressions.Regex.Replace( raw, @"""Mode""\s*:\s*""bossRun""", @"""Mode"": ""mapping""", ic); raw = System.Text.RegularExpressions.Regex.Replace( raw, @"""MapType""\s*:\s*""[^""]*""", @"""MapType"": ""kulemak""", ic); Log.Information("Migrated config: Mode bossRun -> mapping + MapType kulemak"); } // MapType enum value: bossRun → kulemak raw = System.Text.RegularExpressions.Regex.Replace( raw, @"""MapType""\s*:\s*""bossRun""", @"""MapType"": ""kulemak""", ic); // Settings property name: BossRun → Kulemak raw = raw.Replace("\"BossRun\":", "\"Kulemak\":"); } var parsed = JsonSerializer.Deserialize(raw, JsonOptions); if (parsed == null) return new SavedSettings(); // Migrate links: strip /live from URLs foreach (var link in parsed.Links) { link.Url = StripLive(link.Url); } // Backfill known diamonds parsed.Diamond.BackfillKnown(); Log.Information("Loaded config.json from {Path} ({LinkCount} links)", _filePath, parsed.Links.Count); return parsed; } catch (Exception ex) { Log.Warning(ex, "Failed to read config.json at {Path}, using defaults", _filePath); return new SavedSettings(); } } private static string StripLive(string url) => System.Text.RegularExpressions.Regex.Replace(url, @"/live/?$", ""); }