using NUnit.Framework; using ProjectM.Simulation; using Unity.Entities; using UnityEngine; namespace ProjectM.Tests { /// /// Pure tests for the save FOUNDATION: the JSON schema round-trips (JsonUtility), version handling is safe /// (v7 is a FRESH EPOCH — older saves are rejected at MinLoadableVersion), and the born-correct ledger apply /// () the server spawn system uses to overwrite a director's StorageEntry /// buffer from a staged PendingSave. /// public class SavePersistenceTests { [Test] public void SaveData_Json_RoundTrip_PreservesFields() { var data = new SaveData { RunsCompleted = 7, MaxDepthReached = 9, Ledger = new[] { new LedgerRow { ItemId = 1, Count = 5 }, new LedgerRow { ItemId = 2, Count = 9 }, }, SavedAtMs = 1234567890123L, }; var json = JsonUtility.ToJson(data); var back = JsonUtility.FromJson(json); Assert.AreEqual(SaveData.CurrentVersion, back.Version); Assert.AreEqual(7, back.RunsCompleted); Assert.AreEqual(9, back.MaxDepthReached); Assert.AreEqual(2, back.Ledger.Length); Assert.AreEqual(1, back.Ledger[0].ItemId); Assert.AreEqual(5, back.Ledger[0].Count); Assert.AreEqual(2, back.Ledger[1].ItemId); Assert.AreEqual(9, back.Ledger[1].Count); Assert.AreEqual(1234567890123L, back.SavedAtMs); } [Test] public void SaveData_EmptyJson_DoesNotThrow_And_EmptyLedgerRoundTrips() { Assert.DoesNotThrow(() => JsonUtility.FromJson("{}")); var empty = new SaveData(); var back = JsonUtility.FromJson(JsonUtility.ToJson(empty)); Assert.IsNotNull(back.Ledger); Assert.AreEqual(0, back.Ledger.Length); } [Test] public void SaveData_Is_At_V7_Fresh_Epoch() { Assert.AreEqual(7, SaveData.CurrentVersion, "SaveData is at v7 (the LANTERN fresh epoch)."); Assert.AreEqual(7, SaveData.MinLoadableVersion, "v7 is a fresh epoch: no older save loads."); } [Test] public void Pre_V7_Save_Is_Below_The_Load_Floor() { // A v6 (co-op-Hades era) save round-trips with its Version intact and sits BELOW MinLoadableVersion, // so SaveService.Load rejects it (-> New Game). Fresh epoch, operator-approved (LANTERN purge). var back = JsonUtility.FromJson("{\"Version\":6,\"RunsCompleted\":3}"); Assert.AreEqual(6, back.Version); Assert.Less(back.Version, SaveData.MinLoadableVersion, "a v6 save is rejected under the v7 fresh epoch."); } [Test] public void WriteLedger_Overwrites_Destination_From_Staged_Rows() { using var world = new World("SaveApplyTest"); var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); em.AddBuffer(e); var src = em.GetBuffer(e); src.Add(new PendingSaveLedgerRow { ItemId = 3, Count = 7 }); src.Add(new PendingSaveLedgerRow { ItemId = 5, Count = 12 }); var dest = em.GetBuffer(e); dest.Add(new StorageEntry { ItemId = 99, Count = 1 }); // pre-existing junk that must be cleared SaveApply.WriteLedger(em.GetBuffer(e), em.GetBuffer(e)); var result = em.GetBuffer(e); Assert.AreEqual(2, result.Length); Assert.AreEqual(3, result[0].ItemId); Assert.AreEqual(7, result[0].Count); Assert.AreEqual(5, result[1].ItemId); Assert.AreEqual(12, result[1].Count); } [Test] public void WriteLedger_EmptySource_ClearsDestination() { using var world = new World("SaveApplyEmptyTest"); var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); em.AddBuffer(e); var dest = em.GetBuffer(e); dest.Add(new StorageEntry { ItemId = 1, Count = 1 }); SaveApply.WriteLedger(em.GetBuffer(e), em.GetBuffer(e)); Assert.AreEqual(0, em.GetBuffer(e).Length); } [Test] public void StructureSave_HP_RoundTrips() { var data = new SaveData { Structures = new[] { new StructureSave { Type = 1, CellX = 1, CellZ = 2, HP = 37f } } }; var back = JsonUtility.FromJson(JsonUtility.ToJson(data)); Assert.AreEqual(SaveData.CurrentVersion, back.Version, "new saves write the current version."); Assert.AreEqual(1, back.Structures.Length); Assert.AreEqual(37f, back.Structures[0].HP, 1e-4f, "the wounded HP round-trips through JSON."); } [Test] public void ToPending_Maps_All_Fields_Including_The_Wounded_HP() { // The WorldLauncher save->stage copy: omitting any field here silently restores at full HP (review-caught). var s = new StructureSave { Type = 1, CellX = 3, CellZ = -2, HP = 37f }; var p = SaveApply.ToPending(s); Assert.AreEqual(1, p.Type); Assert.AreEqual(3, p.CellX); Assert.AreEqual(-2, p.CellZ); Assert.AreEqual(37f, p.HP, 1e-4f, "the wounded HP survives the save->staging copy."); } [Test] public void MetaUpgrades_And_Counters_RoundTrip() { var data = new SaveData { RunsCompleted = 7, MaxDepthReached = 9, MetaUpgrades = new[] { new MetaUpgradeSave { ClassId = 2, UpgradeId = 1, Tier = 3 }, new MetaUpgradeSave { ClassId = 3, UpgradeId = 200, Tier = 1 }, // unknown id persists verbatim }, }; var back = JsonUtility.FromJson(JsonUtility.ToJson(data)); Assert.AreEqual(7, back.RunsCompleted); Assert.AreEqual(9, back.MaxDepthReached); Assert.AreEqual(2, back.MetaUpgrades.Length, "meta tier rows round-trip through JSON."); Assert.AreEqual(2, back.MetaUpgrades[0].ClassId); Assert.AreEqual(1, back.MetaUpgrades[0].UpgradeId); Assert.AreEqual(3, back.MetaUpgrades[0].Tier); Assert.AreEqual(200, back.MetaUpgrades[1].UpgradeId, "an unknown upgrade id is preserved on disk, not clamped at save time (preserve-don't-crash)."); } } }