Files
Project-M/Assets/_Project/Tests/EditMode/SavePersistenceTests.cs
T
kronic b34945c2d2 LANTERN purge B3+B5: delete the cycle/core/win-lose spine + onboarding; save epoch v7
Deletes CyclePhaseSystem, GoalReachedSystem, CoreDamage/CoreRestore, ThreatDirector,
CoreIntegrity/GoalProgress/RunPhase/RunOutcome/ThreatState components,
CoreVisualFeedbackSystem, and the whole Client/Onboarding slice (+6 test files).

Keepers reworked: RunDirectorSystem (UpdateBefore attr + launch guard + goal/threat
bank removed; sole SaveRequest raiser now), CycleDirectorSpawnSystem (ledger/meta
host only), WaveSystem UNGATED (waves run wherever a WaveDirector is baked),
EnemyAISystem core-fallback stripped, AmbientAudioSystem reworked (bed + run cues;
no CycleState gate), MusicSystem RunInfo-only, HudSystem big trim (goal meter, core
bar, siege banner, terminal banner, outcome flash, onboarding hook all gone),
MetaShop/ClassPrep/AimReticle siege gates dropped, DebugOverlay/ops re-meant
(SpawnWave=force next wave, EndSiege=quiet arena; SetCalm/AdvanceGoal/SetHeat
retired, bytes reserved), TuningConfig Core knobs retired (ids 20-23 reserved),
StorageMath.DrainFraction deleted, HowToPlay copy rewritten.

Save epoch v7 (fresh epoch, operator-approved): SaveData drops goal/core/outcome +
conveyor/machine-IO fields; MinLoadableVersion=7; PendingSave/PendingStructure
trimmed; RollTerminalCampaignForward deleted; SaveStructureScan signature slimmed.

390 tests green; Play world-creation clean (player + waves live, no exceptions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 15:27:12 -07:00

161 lines
6.8 KiB
C#

using NUnit.Framework;
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Tests
{
/// <summary>
/// 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
/// (<see cref="SaveApply.WriteLedger"/>) the server spawn system uses to overwrite a director's StorageEntry
/// buffer from a staged PendingSave.
/// </summary>
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<SaveData>(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<SaveData>("{}"));
var empty = new SaveData();
var back = JsonUtility.FromJson<SaveData>(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<SaveData>("{\"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<PendingSaveLedgerRow>(e);
em.AddBuffer<StorageEntry>(e);
var src = em.GetBuffer<PendingSaveLedgerRow>(e);
src.Add(new PendingSaveLedgerRow { ItemId = 3, Count = 7 });
src.Add(new PendingSaveLedgerRow { ItemId = 5, Count = 12 });
var dest = em.GetBuffer<StorageEntry>(e);
dest.Add(new StorageEntry { ItemId = 99, Count = 1 }); // pre-existing junk that must be cleared
SaveApply.WriteLedger(em.GetBuffer<PendingSaveLedgerRow>(e), em.GetBuffer<StorageEntry>(e));
var result = em.GetBuffer<StorageEntry>(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<PendingSaveLedgerRow>(e);
em.AddBuffer<StorageEntry>(e);
var dest = em.GetBuffer<StorageEntry>(e);
dest.Add(new StorageEntry { ItemId = 1, Count = 1 });
SaveApply.WriteLedger(em.GetBuffer<PendingSaveLedgerRow>(e), em.GetBuffer<StorageEntry>(e));
Assert.AreEqual(0, em.GetBuffer<StorageEntry>(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<SaveData>(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<SaveData>(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).");
}
}
}