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}");
}
}
}