using System.Text.Json; namespace Mr.BrickAdventures.Progression.Adapters; /// /// Reading and writing versioned save files. Shared by the Playthrough and Profile stores, which /// differ only in what they hold and where they live. /// internal static class JsonFiles { internal static readonly JsonSerializerOptions Options = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, }; /// /// Writes to a temporary file and then renames it over the destination. A crash mid-write leaves /// the previous file intact rather than a half-written one — which matters more once there are /// several slots to lose. /// internal static void WriteAtomic(string path, int version, T data) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); var temp = path + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(new SaveEnvelope(version, data), Options)); File.Move(temp, path, overwrite: true); } /// /// Reads a save file. Absent when there is no file; Corrupt when there is one but it cannot be /// turned into — unreadable JSON, an unknown version, or a /// snapshot the domain rejects. Never conflates the two: that conflation is what let a version /// bump silently overwrite a player's save. /// internal static LoadResult Read( string path, int currentVersion, Func restore) where TSnapshot : class where TAggregate : class { if (!File.Exists(path)) return LoadResult.Absent(); SaveEnvelope? envelope; try { envelope = JsonSerializer.Deserialize>(File.ReadAllText(path), Options); } catch (JsonException e) { return LoadResult.Corrupt($"unreadable json: {e.Message}"); } if (envelope?.Data is null) return LoadResult.Corrupt("file held no save data"); if (envelope.Version > currentVersion) return LoadResult.Corrupt( $"saved by a newer build (version {envelope.Version}, this build reads {currentVersion})"); if (envelope.Version < currentVersion) return LoadResult.Corrupt( $"version {envelope.Version} has no migration to {currentVersion}"); try { return LoadResult.Loaded(restore(envelope.Data)); } catch (Exception e) { return LoadResult.Corrupt($"snapshot rejected by the domain: {e.Message}"); } } }