Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3f9bb66346
commit
95e164adce
5 changed files with 328 additions and 8 deletions
|
|
@ -3,7 +3,6 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -49,12 +48,36 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
if (_cultCache != null) return _cultCache;
|
||||
|
||||
_cultCache = AetheriaStores.Open(Path.Combine(GameDataDirectory.FullName, "Aetheria.cc"));
|
||||
|
||||
// All three stores attach once and stay attached until the process exits. The run lifecycle is record-level
|
||||
// inside this cache; reopening would replace the catalog instances the galaxy and live entities hold.
|
||||
_cultCache = AetheriaStores.Open(
|
||||
Path.Combine(GameDataDirectory.FullName, "Aetheria.cc"),
|
||||
runPath: Path.Combine(GameDataDirectory.FullName, "run.cc"),
|
||||
playerPath: Path.Combine(GameDataDirectory.FullName, "player.cc"));
|
||||
|
||||
return _cultCache;
|
||||
}
|
||||
}
|
||||
|
||||
// The only creator of PlayerSettings: an absent global means first launch, and the defaults are committed.
|
||||
public static PlayerSettings PlayerSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
var settings = CultCache.GetGlobal<PlayerSettings>();
|
||||
if (settings != null) return settings;
|
||||
settings = GetDefaultPlayerSettings();
|
||||
CultCache.Commit(batch => batch.Upsert(settings));
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SavePlayerSettings()
|
||||
{
|
||||
var settings = PlayerSettings;
|
||||
CultCache.Commit(batch => batch.Upsert(settings));
|
||||
}
|
||||
|
||||
private static PlayerSettings GetDefaultPlayerSettings()
|
||||
{
|
||||
var settings = new PlayerSettings();
|
||||
|
|
@ -205,6 +228,23 @@ public class ActionGameManager : MonoBehaviour
|
|||
get => MessagePackSerializer.Deserialize<EntitySettings>(MessagePackSerializer.Serialize(Settings.GameplaySettings.DefaultEntitySettings));
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
SaveRun();
|
||||
SavePlayerSettings();
|
||||
}
|
||||
|
||||
// Wormhole arrival and quit save the run; a dead run has no galaxy, so it is not saved.
|
||||
public void SaveRun()
|
||||
{
|
||||
if (CurrentGalaxy != null)
|
||||
{
|
||||
var (game, zones) = RunSave.Capture(CultCache, CurrentGalaxy, Zone, DockedEntity ?? CurrentEntity, IsTutorial,
|
||||
_actionBarSlots.Select(s => s.Save()).ToArray());
|
||||
RunSave.Commit(CultCache, game, zones);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Input.Dispose();
|
||||
|
|
@ -561,7 +601,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
ship.ExitWormhole(ZoneRenderer.WormholeInstances.Keys.First(w => w.Target == oldZone.GalaxyZone).Position,
|
||||
Settings.GameplaySettings.WormholeExitVelocity * ItemManager.Random.NextFloat2Direction());
|
||||
CurrentEntity.Zone = Zone;
|
||||
SaveState();
|
||||
SaveRun();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -655,7 +695,8 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
if (CurrentGalaxy != null)
|
||||
{
|
||||
if (PlayerSettings.SavedRun == null)
|
||||
var saved = CultCache.GetGlobal<SavedGame>();
|
||||
if (saved == null)
|
||||
{
|
||||
SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance));
|
||||
PopulateLevel(CurrentGalaxy.Entrance);
|
||||
|
|
@ -678,8 +719,8 @@ public class ActionGameManager : MonoBehaviour
|
|||
foreach(var group in CurrentGalaxy.DiscoveredZones
|
||||
.GroupBy(dz=>dz.Distance[CurrentGalaxy.Entrance]))
|
||||
SectorMap.QueueZoneReveal(group);
|
||||
PopulateLevel(CurrentGalaxy.Zones[PlayerSettings.SavedRun.CurrentZone]);
|
||||
var targetEntity = Zone.Entities[PlayerSettings.SavedRun.CurrentZoneEntity];
|
||||
PopulateLevel(CurrentGalaxy.Zones[saved.CurrentZone]);
|
||||
var targetEntity = Zone.Entities[saved.CurrentZoneEntity];
|
||||
if (targetEntity is OrbitalEntity orbitalEntity)
|
||||
{
|
||||
CurrentEntity = targetEntity.Children.First(c => c is Ship {IsPlayerShip: true});
|
||||
|
|
@ -693,7 +734,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
|
||||
for (var i = 0; i < _actionBarSlots.Count; i++)
|
||||
{
|
||||
_actionBarSlots[i].Restore(PlayerSettings.SavedRun.ActionBarBindings[i], CurrentEntity);
|
||||
_actionBarSlots[i].Restore(saved.ActionBarBindings[i], CurrentEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1015,6 +1056,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
MainMenu.gameObject.SetActive(true);
|
||||
Menu.gameObject.SetActive(false);
|
||||
CurrentGalaxy = null;
|
||||
RunSave.Clear(CultCache);
|
||||
SavePlayerSettings();
|
||||
Observable.EveryUpdate()
|
||||
.Where(_ => Time.time - deathTime < DeathPostTransitionTime)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,84 @@ public class SavedGame
|
|||
|
||||
[Key(12)]
|
||||
public FactionRelationship[] Relationships;
|
||||
}
|
||||
|
||||
// The run's lifecycle over the run store. Only Commit creates SavedGame and SavedZone records.
|
||||
public static class RunSave
|
||||
{
|
||||
// The live run as plain documents. Writes nothing.
|
||||
public static (SavedGame Game, SavedZone[] Zones) Capture(CultCache cache, Galaxy galaxy, Zone currentZone,
|
||||
Entity currentEntity, bool isTutorial, SavedActionBarBinding[] actionBar)
|
||||
{
|
||||
var factions = galaxy.HomeZones.Keys.ToArray();
|
||||
var game = new SavedGame
|
||||
{
|
||||
DiscoveredZones = galaxy.DiscoveredZones.Select(dz => Array.IndexOf(galaxy.Zones, dz)).ToArray(),
|
||||
Background = galaxy.Background,
|
||||
Factions = factions.Select(f => cache.RefOf(f)).ToArray(),
|
||||
Relationships = galaxy.Factions.Select(f => galaxy.FactionRelationships[f]).ToArray(),
|
||||
HomeZones = galaxy.HomeZones.ToDictionary(
|
||||
x => Array.IndexOf(factions, x.Key),
|
||||
x => Array.IndexOf(galaxy.Zones, x.Value)),
|
||||
BossZones = galaxy.BossZones.ToDictionary(
|
||||
x => Array.IndexOf(factions, x.Key),
|
||||
x => Array.IndexOf(galaxy.Zones, x.Value)),
|
||||
CurrentZone = Array.FindIndex(galaxy.Zones, zone => zone.Contents == currentZone),
|
||||
CurrentZoneEntity = currentZone.Entities.IndexOf(currentEntity),
|
||||
Entrance = Array.IndexOf(galaxy.Zones, galaxy.Entrance),
|
||||
Exit = Array.IndexOf(galaxy.Zones, galaxy.Exit),
|
||||
IsTutorial = isTutorial,
|
||||
ActionBarBindings = actionBar
|
||||
};
|
||||
|
||||
// A zone generated earlier but not loaded this session has no live Contents; its packed contents carry over.
|
||||
var zones = galaxy.Zones.Select(zone => new SavedZone
|
||||
{
|
||||
Name = zone.Name,
|
||||
Position = zone.Position,
|
||||
AdjacentZones = zone.AdjacentZones.Select(az => Array.IndexOf(galaxy.Zones, az)).ToArray(),
|
||||
Factions = zone.Factions.Select(f => Array.IndexOf(factions, f)).ToArray(),
|
||||
Contents = zone.Contents?.PackZone() ?? zone.PackedContents,
|
||||
Owner = zone.Owner == null ? -1 : Array.IndexOf(factions, zone.Owner)
|
||||
}).ToArray();
|
||||
return (game, zones);
|
||||
}
|
||||
|
||||
// The only writer of SavedGame and SavedZone: one Commit to the run store. Zone i lands at savedzone-{i}, stable
|
||||
// because a run's zone array is fixed at generation, and every stored SavedZone outside that set is removed. The
|
||||
// run store's single-file commit writes its whole view, so orbits and bodies staged by zone generation land too.
|
||||
public static void Commit(CultCache cache, SavedGame game, IReadOnlyList<SavedZone> zones)
|
||||
{
|
||||
var keys = zones.Select((_, i) => new CultRecordKey($"savedzone-{i}")).ToArray();
|
||||
var kept = new HashSet<CultRecordKey>(keys);
|
||||
var stale = cache.AllStoredDocuments
|
||||
.Where(stored => stored.Document is SavedZone && !kept.Contains(stored.Key))
|
||||
.Select(stored => stored.Key)
|
||||
.ToArray();
|
||||
game.Zones = keys.Select(key => new CultRecordRef<SavedZone>(key)).ToArray();
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
for (var i = 0; i < keys.Length; i++) batch.Upsert(typeof(SavedZone), zones[i], keys[i]);
|
||||
batch.Upsert(game);
|
||||
foreach (var key in stale) batch.Remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
// Removes every run record (SavedGame, SavedZone, OrbitData, BodyData) in one Commit.
|
||||
public static void Clear(CultCache cache)
|
||||
{
|
||||
var run = cache.AllStoredDocuments
|
||||
.Where(stored => IsRunRecord(stored.Descriptor.DocumentType))
|
||||
.Select(stored => stored.Key)
|
||||
.ToArray();
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
foreach (var key in run) batch.Remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsRunRecord(Type documentType) =>
|
||||
AetheriaStores.RunTypes.Any(home => home.IsAssignableFrom(documentType));
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.savedzone", "1"), MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
|
|
@ -81,6 +82,8 @@ public class InputDisplayLayout : MonoBehaviour
|
|||
void Start()
|
||||
{
|
||||
_canvas = transform.root.GetComponent<Canvas>();
|
||||
_inputLayout = ActionGameManager.CultCache.Get<InputLayout>(new CultRecordKey(LayoutFile.name))
|
||||
?? throw new InvalidOperationException($"catalog has no InputLayout '{LayoutFile.name}'");
|
||||
DisplayLayout(_inputLayout);
|
||||
|
||||
_buttonMappings.Add(MapMouseButton(MouseLeft, "<Mouse>/leftButton"));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
|
|
@ -91,9 +92,24 @@ public class MainMenu : MonoBehaviour
|
|||
{
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus");
|
||||
if (!InGame)
|
||||
{
|
||||
// A run exists only as the run store's SavedGame global.
|
||||
var cache = ActionGameManager.CultCache;
|
||||
var saved = cache.GetGlobal<SavedGame>();
|
||||
if (saved == null && cache.AllStoredDocuments.Any(stored => RunSave.IsRunRecord(stored.Descriptor.DocumentType)))
|
||||
Debug.Log("run store has no SavedGame; Continue disabled");
|
||||
_nextMenu.panel.AddButton("Continue", saved == null ? (Action) null : () =>
|
||||
{
|
||||
ActionGameManager.IsTutorial = saved.IsTutorial;
|
||||
ActionGameManager.CurrentGalaxy = new Galaxy(cache, saved, Debug.Log);
|
||||
SceneManager.LoadScene("ARPG");
|
||||
});
|
||||
}
|
||||
_nextMenu.panel.AddButton("New Game",
|
||||
() =>
|
||||
{
|
||||
RunSave.Clear(ActionGameManager.CultCache);
|
||||
var generatorState = "Loading Database Contents";
|
||||
Action<string> setState = s => generatorState = s;
|
||||
|
||||
|
|
|
|||
182
tests/Aetheria.Shared.Tests/RunSaveTests.cs
Normal file
182
tests/Aetheria.Shared.Tests/RunSaveTests.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using Xunit;
|
||||
|
||||
// The run lifecycle over all three stores: a save is one commit with stable zone keys, and a clear removes every run record.
|
||||
public sealed class RunSaveTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-runsave-" + Guid.NewGuid().ToString("N"));
|
||||
private string Catalog => Path.Combine(_root, "Aetheria.cc");
|
||||
private string Run => Path.Combine(_root, "run.cc");
|
||||
private string Player => Path.Combine(_root, "player.cc");
|
||||
|
||||
public RunSaveTests()
|
||||
{
|
||||
Directory.CreateDirectory(_root);
|
||||
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
batch.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
batch.Upsert(new Faction { Name = "Adrasteia", ShortName = "ADR" });
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() => Directory.Delete(_root, true);
|
||||
|
||||
private CultCache Open() => AetheriaStores.Open(Catalog, Run, Player);
|
||||
|
||||
[Fact]
|
||||
public void RepeatedSavesKeepRecordCountConstant()
|
||||
{
|
||||
ZonePack contents = null;
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
using (var cache = Open())
|
||||
{
|
||||
contents ??= StageZoneContents(cache);
|
||||
RunSave.Commit(cache, Game(cache), Zones(3, contents, 0));
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Equal(7, RunRecordCount(cache));
|
||||
Assert.Equal(7, RecordsIn(Run));
|
||||
Assert.Equal(new[] { "savedzone-0", "savedzone-1", "savedzone-2" }, SavedZoneKeys(cache));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveRemovesZonesNoLongerInTheRun()
|
||||
{
|
||||
using (var cache = Open())
|
||||
{
|
||||
RunSave.Commit(cache, Game(cache), Zones(3, null, -1));
|
||||
RunSave.Commit(cache, Game(cache), Zones(2, null, -1));
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
Assert.Equal(new[] { "savedzone-0", "savedzone-1" }, SavedZoneKeys(cache));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveWritesOnlyTheRunStore()
|
||||
{
|
||||
using (var cache = Open())
|
||||
cache.Commit(batch => batch.Upsert(new PlayerSettings()));
|
||||
var before = new[] { Catalog, Player }.Select(Hash).ToArray();
|
||||
|
||||
using (var cache = Open())
|
||||
RunSave.Commit(cache, Game(cache), Zones(3, StageZoneContents(cache), 0));
|
||||
|
||||
Assert.Equal(before, new[] { Catalog, Player }.Select(Hash).ToArray());
|
||||
Assert.Equal(7, RecordsIn(Run));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResumeThenSaveKeepsUnloadedZoneContents()
|
||||
{
|
||||
using (var cache = Open())
|
||||
RunSave.Commit(cache, Game(cache), Zones(2, StageZoneContents(cache), 1));
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
|
||||
var itemManager = new ItemManager(cache, TestSettings(), _ => { });
|
||||
galaxy.Zones[0].Contents = new Zone(itemManager, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy);
|
||||
var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, false, new SavedActionBarBinding[0]);
|
||||
RunSave.Commit(cache, game, zones);
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
var unloaded = cache.Get(cache.GetGlobal<SavedGame>().Zones[1]);
|
||||
Assert.NotNull(unloaded.Contents);
|
||||
Assert.NotNull(cache.Get(unloaded.Contents.Orbits[1]));
|
||||
Assert.NotNull(cache.Get(unloaded.Contents.Planets[0]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearRemovesEveryRunRecord()
|
||||
{
|
||||
using (var cache = Open())
|
||||
{
|
||||
cache.Commit(batch => batch.Upsert(new PlayerSettings()));
|
||||
RunSave.Commit(cache, Game(cache), Zones(3, StageZoneContents(cache), 0));
|
||||
}
|
||||
var before = new[] { Catalog, Player }.Select(Hash).ToArray();
|
||||
|
||||
using (var cache = Open())
|
||||
RunSave.Clear(cache);
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Equal(0, RunRecordCount(cache));
|
||||
Assert.Null(cache.GetGlobal<SavedGame>());
|
||||
}
|
||||
Assert.Equal(0, RecordsIn(Run));
|
||||
Assert.Equal(before, new[] { Catalog, Player }.Select(Hash).ToArray());
|
||||
}
|
||||
|
||||
// As in AetherDb's loadout command: the settings an ItemManager needs to build units, and nothing authored.
|
||||
internal static GameplaySettings TestSettings() => new GameplaySettings
|
||||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp()
|
||||
};
|
||||
|
||||
// Two orbits and a body upserted into the run store and left staged, as zone generation leaves them.
|
||||
private static ZonePack StageZoneContents(CultCache cache)
|
||||
{
|
||||
var root = cache.Upsert(new OrbitData());
|
||||
var moon = cache.Upsert(new OrbitData { Parent = root, Distance = 5 });
|
||||
var body = cache.Upsert<BodyData>(new PlanetData { Name = "Rock", Orbit = moon });
|
||||
return new ZonePack { Orbits = { root, moon }, Planets = { body } };
|
||||
}
|
||||
|
||||
private static SavedGame Game(CultCache cache) => new SavedGame
|
||||
{
|
||||
Factions = new[] { cache.RefOf(cache.GetAll<Faction>().Single()) },
|
||||
Relationships = new[] { FactionRelationship.Neutral },
|
||||
HomeZones = new Dictionary<int, int> { { 0, 0 } },
|
||||
BossZones = new Dictionary<int, int>(),
|
||||
DiscoveredZones = new[] { 0 },
|
||||
ActionBarBindings = new SavedActionBarBinding[0],
|
||||
Exit = -1
|
||||
};
|
||||
|
||||
// Fresh instances every call; zone contentsIndex holds the given contents, every other zone was never visited.
|
||||
private static SavedZone[] Zones(int count, ZonePack contents, int contentsIndex) => Enumerable.Range(0, count)
|
||||
.Select(i => new SavedZone
|
||||
{
|
||||
Name = $"Zone {i}",
|
||||
Position = new float2(i * 10, 0),
|
||||
AdjacentZones = Enumerable.Range(0, count).Where(j => j != i).ToArray(),
|
||||
Factions = new[] { 0 },
|
||||
Owner = 0,
|
||||
Contents = i == contentsIndex ? contents : null
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
private static int RunRecordCount(CultCache cache) =>
|
||||
cache.AllStoredDocuments.Count(stored => RunSave.IsRunRecord(stored.Descriptor.DocumentType));
|
||||
|
||||
private static string[] SavedZoneKeys(CultCache cache) => cache.AllStoredDocuments
|
||||
.Where(stored => stored.Document is SavedZone)
|
||||
.Select(stored => stored.Key.Value)
|
||||
.OrderBy(key => key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private static int RecordsIn(string path) =>
|
||||
CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records.Count();
|
||||
|
||||
private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
|
||||
}
|
||||
Loading…
Reference in a new issue