94 lines
3.4 KiB
C#
94 lines
3.4 KiB
C#
|
|
using Theriapolis.Core;
|
||
|
|
using Theriapolis.Core.Persistence;
|
||
|
|
using Theriapolis.Core.Tactical;
|
||
|
|
using Theriapolis.Core.Util;
|
||
|
|
using Theriapolis.Core.World;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace Theriapolis.Tests.Persistence;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// End-to-end M5 smoke test:
|
||
|
|
/// 1. Generate a chunk via the streamer.
|
||
|
|
/// 2. Mutate a tile (chop a tree → set Deco=None).
|
||
|
|
/// 3. Flush → save → load.
|
||
|
|
/// 4. Re-stream the chunk and verify the mutation is still applied.
|
||
|
|
///
|
||
|
|
/// This is the single most important save-correctness test — exercises the
|
||
|
|
/// streamer/delta-store/codec/restore loop in one shot.
|
||
|
|
/// </summary>
|
||
|
|
public sealed class DeltaPersistenceTests : IClassFixture<WorldCache>
|
||
|
|
{
|
||
|
|
private const ulong TestSeed = 0xCAFEBABEUL;
|
||
|
|
private readonly WorldCache _cache;
|
||
|
|
public DeltaPersistenceTests(WorldCache c) => _cache = c;
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void ChopTree_PersistsAcrossSaveLoad()
|
||
|
|
{
|
||
|
|
var w = _cache.Get(TestSeed).World;
|
||
|
|
|
||
|
|
// Find a chunk + tile that the baseline generator put a tree on.
|
||
|
|
var (cc, lx, ly) = FindTreeTile(w);
|
||
|
|
|
||
|
|
var deltasA = new InMemoryChunkDeltaStore();
|
||
|
|
var streamerA = new ChunkStreamer(TestSeed, w, deltasA);
|
||
|
|
var chunk = streamerA.Get(cc);
|
||
|
|
Assert.Equal(TacticalDeco.Tree, chunk.Tiles[lx, ly].Deco);
|
||
|
|
|
||
|
|
// Chop it.
|
||
|
|
chunk.Tiles[lx, ly].Deco = TacticalDeco.None;
|
||
|
|
chunk.HasDelta = true;
|
||
|
|
|
||
|
|
// Flush + save.
|
||
|
|
streamerA.FlushAll();
|
||
|
|
var header = new SaveHeader { WorldSeedHex = $"0x{TestSeed:X}" };
|
||
|
|
var body = new SaveBody { Clock = new() { InGameSeconds = 0 } };
|
||
|
|
body.Player = new() { Name = "Tester" };
|
||
|
|
foreach (var kv in deltasA.All) body.ModifiedChunks[kv.Key] = kv.Value;
|
||
|
|
var bytes = SaveCodec.Serialize(header, body);
|
||
|
|
|
||
|
|
// ── Load on a fresh streamer ──
|
||
|
|
var (_, rb) = SaveCodec.Deserialize(bytes);
|
||
|
|
var deltasB = new InMemoryChunkDeltaStore();
|
||
|
|
foreach (var kv in rb.ModifiedChunks) deltasB.Put(kv.Key, kv.Value);
|
||
|
|
var streamerB = new ChunkStreamer(TestSeed, w, deltasB);
|
||
|
|
var reloaded = streamerB.Get(cc);
|
||
|
|
|
||
|
|
Assert.Equal(TacticalDeco.None, reloaded.Tiles[lx, ly].Deco);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void UnmodifiedChunk_HasNoDelta()
|
||
|
|
{
|
||
|
|
var w = _cache.Get(TestSeed).World;
|
||
|
|
var deltas = new InMemoryChunkDeltaStore();
|
||
|
|
var streamer = new ChunkStreamer(TestSeed, w, deltas);
|
||
|
|
|
||
|
|
// Touch a chunk without modifying it.
|
||
|
|
var cc = new ChunkCoord(20, 20);
|
||
|
|
streamer.Get(cc);
|
||
|
|
streamer.FlushAll();
|
||
|
|
Assert.Null(deltas.Get(cc));
|
||
|
|
}
|
||
|
|
|
||
|
|
private static (ChunkCoord cc, int lx, int ly) FindTreeTile(WorldState w)
|
||
|
|
{
|
||
|
|
// Walk a band of chunks centred near a known land settlement so we
|
||
|
|
// don't waste time scanning ocean.
|
||
|
|
var anchor = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
|
||
|
|
var centre = ChunkCoord.ForWorldTile(anchor.TileX, anchor.TileY);
|
||
|
|
for (int dy = -3; dy <= 3; dy++)
|
||
|
|
for (int dx = -3; dx <= 3; dx++)
|
||
|
|
{
|
||
|
|
var cc = new ChunkCoord(centre.X + dx, centre.Y + dy);
|
||
|
|
var chunk = TacticalChunkGen.Generate(0xCAFEBABEUL, cc, w);
|
||
|
|
for (int ly = 0; ly < C.TACTICAL_CHUNK_SIZE; ly++)
|
||
|
|
for (int lx = 0; lx < C.TACTICAL_CHUNK_SIZE; lx++)
|
||
|
|
if (chunk.Tiles[lx, ly].Deco == TacticalDeco.Tree)
|
||
|
|
return (cc, lx, ly);
|
||
|
|
}
|
||
|
|
throw new Xunit.Sdk.XunitException("no tree near settlement to chop in test");
|
||
|
|
}
|
||
|
|
}
|