using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace StS2Sim; /// /// Reads the vanilla game's current_run.save (or current_run_mp.save if newer) /// and pulls out the active deck. Pure file IO + JSON; no mod, no game running. /// internal static class SaveFileReader { public sealed record Deck { public required string SourcePath { get; init; } public required DateTime Modified { get; init; } public required string CharacterId { get; init; } public required IReadOnlyList Cards { get; init; } public int CurrentHp { get; init; } public int MaxHp { get; init; } public int Gold { get; init; } public int Floor { get; init; } public IReadOnlyList Relics { get; init; } = Array.Empty(); } public sealed record DeckCard(string Id, int FloorAdded, int UpgradeLevel = 0); /// /// Find the freshest current_run*.save across both modded and unmodded profiles, /// across SP and MP. Returns null if none exists. /// public static Deck? ReadFreshest() { var roots = EnumerateSaveDirs().ToList(); var candidates = new List(); foreach (var dir in roots) { foreach (var name in new[] { "current_run.save", "current_run_mp.save" }) { var path = Path.Combine(dir, name); if (File.Exists(path)) candidates.Add(new FileInfo(path)); } } if (candidates.Count == 0) return null; // Newest by mtime wins — same heuristic the dev server uses. var newest = candidates.OrderByDescending(f => f.LastWriteTimeUtc).First(); return ReadFile(newest.FullName); } public static Deck? ReadFile(string path) { if (!File.Exists(path)) return null; SaveJson? save; try { var text = File.ReadAllText(path); save = JsonSerializer.Deserialize(text, JsonOpts); } catch (Exception ex) { Console.Error.WriteLine($"[SaveFileReader] Failed to parse {path}: {ex.Message}"); return null; } if (save?.Players == null || save.Players.Count == 0) return null; var p = save.Players[0]; return new Deck { SourcePath = path, Modified = File.GetLastWriteTime(path), CharacterId = p.CharacterId ?? "", Cards = (p.Deck ?? new()).Select(c => new DeckCard(c.Id ?? "", c.FloorAddedToDeck, c.CurrentUpgradeLevel)).ToList(), CurrentHp = p.CurrentHp, MaxHp = p.MaxHp, Gold = p.Gold, Floor = save.CurrentActIndex, // best proxy without parsing acts Relics = (p.Relics ?? new()).Select(r => r.Id ?? "").ToList(), }; } private static IEnumerable EnumerateSaveDirs() { var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); var steamRoot = Path.Combine(appData, "SlayTheSpire2", "steam"); if (!Directory.Exists(steamRoot)) yield break; foreach (var steamId in Directory.EnumerateDirectories(steamRoot)) { // Both modded/profile1/saves and profile1/saves can exist. var moddedSaves = Path.Combine(steamId, "modded", "profile1", "saves"); var unmoddedSaves = Path.Combine(steamId, "profile1", "saves"); if (Directory.Exists(moddedSaves)) yield return moddedSaves; if (Directory.Exists(unmoddedSaves)) yield return unmoddedSaves; } } private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; private sealed class SaveJson { [JsonPropertyName("players")] public List? Players { get; set; } [JsonPropertyName("current_act_index")] public int CurrentActIndex { get; set; } } private sealed class PlayerJson { [JsonPropertyName("character_id")] public string? CharacterId { get; set; } [JsonPropertyName("deck")] public List? Deck { get; set; } [JsonPropertyName("relics")] public List? Relics { get; set; } [JsonPropertyName("current_hp")] public int CurrentHp { get; set; } [JsonPropertyName("max_hp")] public int MaxHp { get; set; } [JsonPropertyName("gold")] public int Gold { get; set; } } private sealed class CardJson { [JsonPropertyName("id")] public string? Id { get; set; } [JsonPropertyName("floor_added_to_deck")] public int FloorAddedToDeck { get; set; } [JsonPropertyName("current_upgrade_level")] public int CurrentUpgradeLevel { get; set; } } private sealed class RelicJson { [JsonPropertyName("id")] public string? Id { get; set; } } }