feat(progression): snapshots, ports, in-memory adapters (slice 5)
flat snapshot record per aggregate - separate files mean saving a run doesn't rewrite the Profile, deleting a slot is deleting a file, and a corrupt slot doesn't cost achievements or best times. snapshots hold primitives so Restore re-runs the value-object factories; a serialiser building LevelId directly would bypass every guard. LoadResult has three outcomes: Absent != Corrupt. Collapsing them is the bug where a version bump made ContinueGame overwrite the save. Profile.Restore writes collections directly - replaying UnlockAchievement would re-fire popups and Steam pushes on every load. 44 tests.
This commit is contained in:
@@ -57,6 +57,40 @@ public sealed class Profile
|
||||
/// </summary>
|
||||
public IReadOnlyList<ProgressionEvent> DrainEvents() => _events.Drain();
|
||||
|
||||
public ProfileSnapshot Export() => new(
|
||||
_bestTimes.ToDictionary(e => e.Key.Value, e => e.Value.Seconds),
|
||||
_achievements.Select(a => a.Value).ToList(),
|
||||
_statistics.ToDictionary(e => e.Key.ToString(), e => e.Value));
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds a Profile from a snapshot, re-running every value object's factory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Writes the collections directly rather than replaying <see cref="UnlockAchievement"/>. Going
|
||||
/// through the operation would raise <see cref="AchievementUnlocked"/> for every achievement on
|
||||
/// every load, so a subscriber pushing to Steam or showing a popup would fire on startup.
|
||||
/// </remarks>
|
||||
public static Profile Restore(ProfileSnapshot snapshot)
|
||||
{
|
||||
var profile = new Profile();
|
||||
|
||||
foreach (var (level, seconds) in snapshot.BestTimes)
|
||||
profile._bestTimes[LevelId.Of(level)] = LevelTime.Of(seconds);
|
||||
|
||||
foreach (var achievement in snapshot.Achievements)
|
||||
profile._achievements.Add(AchievementId.Of(achievement));
|
||||
|
||||
foreach (var (stat, value) in snapshot.Statistics)
|
||||
{
|
||||
// A statistic this build no longer tracks is dropped. Losing one counter beats refusing
|
||||
// to load the Profile and with it every achievement and best time.
|
||||
if (Enum.TryParse<StatName>(stat, out var known))
|
||||
profile._statistics[known] = value;
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public LevelTime? BestTimeFor(LevelId level) =>
|
||||
_bestTimes.TryGetValue(level, out var time) ? time : null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user