153 lines
5.7 KiB
C#
153 lines
5.7 KiB
C#
|
|
using Theriapolis.Core;
|
||
|
|
using Theriapolis.Core.Entities;
|
||
|
|
using Theriapolis.Core.Persistence;
|
||
|
|
using Theriapolis.Core.Tactical;
|
||
|
|
using Theriapolis.Core.Time;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace Theriapolis.Tests.Persistence;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// SaveCodec round-trip: every field we write should reappear after Serialize
|
||
|
|
/// → Deserialize. The Phase-4 reserved fields (Flags, Factions, etc.) are
|
||
|
|
/// covered with at least one entry each so future schema bumps notice if a
|
||
|
|
/// section gets accidentally dropped.
|
||
|
|
/// </summary>
|
||
|
|
public sealed class SaveCodecRoundTripTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void Roundtrip_PreservesPlayerAndClock()
|
||
|
|
{
|
||
|
|
var header = new SaveHeader
|
||
|
|
{
|
||
|
|
WorldSeedHex = "0xDEADBEEF",
|
||
|
|
PlayerName = "Grev",
|
||
|
|
PlayerTier = 2,
|
||
|
|
InGameSeconds = 12345,
|
||
|
|
SavedAtUtc = "2026-04-21T09:00:00Z",
|
||
|
|
};
|
||
|
|
header.StageHashes["ElevationGen"] = "0xABCDEF01";
|
||
|
|
header.StageHashes["BiomeAssign"] = "0x11223344";
|
||
|
|
|
||
|
|
var body = new SaveBody
|
||
|
|
{
|
||
|
|
Player = new PlayerActorState
|
||
|
|
{
|
||
|
|
Id = 7,
|
||
|
|
Name = "Grev",
|
||
|
|
PositionX = 1234.5f, PositionY = 678.9f,
|
||
|
|
FacingAngleRad = MathF.PI * 0.5f,
|
||
|
|
SpeedWorldPxPerSec = 80f,
|
||
|
|
HighestTierReached = 2,
|
||
|
|
DiscoveredPoiIds = new[] { 3, 14, 159 },
|
||
|
|
},
|
||
|
|
Clock = new WorldClockState { InGameSeconds = 12345 },
|
||
|
|
};
|
||
|
|
body.Flags["acts:1:complete"] = 1;
|
||
|
|
|
||
|
|
var bytes = SaveCodec.Serialize(header, body);
|
||
|
|
var (rh, rb) = SaveCodec.Deserialize(bytes);
|
||
|
|
|
||
|
|
Assert.Equal(header.WorldSeedHex, rh.WorldSeedHex);
|
||
|
|
Assert.Equal(header.PlayerName, rh.PlayerName);
|
||
|
|
Assert.Equal(2, rh.StageHashes.Count);
|
||
|
|
Assert.Equal("0xABCDEF01", rh.StageHashes["ElevationGen"]);
|
||
|
|
|
||
|
|
Assert.Equal(7, rb.Player.Id);
|
||
|
|
Assert.Equal("Grev", rb.Player.Name);
|
||
|
|
Assert.Equal(1234.5f, rb.Player.PositionX);
|
||
|
|
Assert.Equal(MathF.PI * 0.5f, rb.Player.FacingAngleRad);
|
||
|
|
Assert.Equal(2, rb.Player.HighestTierReached);
|
||
|
|
Assert.Equal(new[] { 3, 14, 159 }, rb.Player.DiscoveredPoiIds);
|
||
|
|
Assert.Equal(12345L, rb.Clock.InGameSeconds);
|
||
|
|
Assert.Equal(1, rb.Flags["acts:1:complete"]);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Roundtrip_PreservesChunkDeltas()
|
||
|
|
{
|
||
|
|
var header = new SaveHeader { WorldSeedHex = "0x1" };
|
||
|
|
var body = new SaveBody
|
||
|
|
{
|
||
|
|
Player = new PlayerActorState { Name = "X" },
|
||
|
|
Clock = new WorldClockState { InGameSeconds = 0 },
|
||
|
|
};
|
||
|
|
var d = new ChunkDelta { SpawnsConsumed = true };
|
||
|
|
d.TileMods.Add(new TileMod(5, 7, TacticalSurface.Cobble, TacticalDeco.None, (byte)TacticalFlags.Road));
|
||
|
|
d.TileMods.Add(new TileMod(8, 8, TacticalSurface.Mud, TacticalDeco.Boulder, 0));
|
||
|
|
body.ModifiedChunks[new ChunkCoord(3, 4)] = d;
|
||
|
|
|
||
|
|
var bytes = SaveCodec.Serialize(header, body);
|
||
|
|
var (_, rb) = SaveCodec.Deserialize(bytes);
|
||
|
|
|
||
|
|
Assert.True(rb.ModifiedChunks.ContainsKey(new ChunkCoord(3, 4)));
|
||
|
|
var rd = rb.ModifiedChunks[new ChunkCoord(3, 4)];
|
||
|
|
Assert.True(rd.SpawnsConsumed);
|
||
|
|
Assert.Equal(2, rd.TileMods.Count);
|
||
|
|
Assert.Equal(TacticalSurface.Cobble, rd.TileMods[0].Surface);
|
||
|
|
Assert.Equal(TacticalDeco.Boulder, rd.TileMods[1].Deco);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Roundtrip_PreservesWorldTileDeltas()
|
||
|
|
{
|
||
|
|
var header = new SaveHeader { WorldSeedHex = "0x2" };
|
||
|
|
var body = new SaveBody
|
||
|
|
{
|
||
|
|
Player = new PlayerActorState { Name = "Y" },
|
||
|
|
Clock = new WorldClockState { InGameSeconds = 0 },
|
||
|
|
ModifiedWorldTiles = { new WorldTileDelta(50, 80, 7, 0xAB) },
|
||
|
|
};
|
||
|
|
|
||
|
|
var bytes = SaveCodec.Serialize(header, body);
|
||
|
|
var (_, rb) = SaveCodec.Deserialize(bytes);
|
||
|
|
|
||
|
|
Assert.Single(rb.ModifiedWorldTiles);
|
||
|
|
Assert.Equal(50, rb.ModifiedWorldTiles[0].X);
|
||
|
|
Assert.Equal(80, rb.ModifiedWorldTiles[0].Y);
|
||
|
|
Assert.Equal(7, rb.ModifiedWorldTiles[0].NewBiome);
|
||
|
|
Assert.Equal(0xAB, rb.ModifiedWorldTiles[0].NewFeatures);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void DeserializeHeaderOnly_DoesNotTouchBody()
|
||
|
|
{
|
||
|
|
var header = new SaveHeader { WorldSeedHex = "0xCAFE", PlayerName = "Solo" };
|
||
|
|
var body = new SaveBody
|
||
|
|
{
|
||
|
|
Player = new PlayerActorState { Id = 99, Name = "Solo" },
|
||
|
|
Clock = new WorldClockState { InGameSeconds = 1 },
|
||
|
|
};
|
||
|
|
var bytes = SaveCodec.Serialize(header, body);
|
||
|
|
var only = SaveCodec.DeserializeHeaderOnly(bytes);
|
||
|
|
Assert.Equal("0xCAFE", only.WorldSeedHex);
|
||
|
|
Assert.Equal("Solo", only.PlayerName);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Roundtrip_HandlesEmptyBody()
|
||
|
|
{
|
||
|
|
var header = new SaveHeader { WorldSeedHex = "0x0" };
|
||
|
|
var body = new SaveBody();
|
||
|
|
var bytes = SaveCodec.Serialize(header, body);
|
||
|
|
var (rh, rb) = SaveCodec.Deserialize(bytes);
|
||
|
|
Assert.Equal("0x0", rh.WorldSeedHex);
|
||
|
|
Assert.Equal(0L, rb.Clock.InGameSeconds);
|
||
|
|
Assert.Empty(rb.ModifiedChunks);
|
||
|
|
Assert.Empty(rb.ModifiedWorldTiles);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SchemaVersion_IsCurrent()
|
||
|
|
{
|
||
|
|
// Bumped to 8 in Phase 7 M0 (Phase 6.5 was 7; Phase 6 was 6; Phase 5
|
||
|
|
// was 5; Phase 4 was 4). The header auto-stamps the current schema
|
||
|
|
// version on construction so a save written today carries the latest
|
||
|
|
// version. Each bump must add a chained ISaveMigration in
|
||
|
|
// <see cref="Theriapolis.Core.Persistence.SaveMigrations.Migrations"/>.
|
||
|
|
Assert.Equal(8, C.SAVE_SCHEMA_VERSION);
|
||
|
|
var h = new SaveHeader();
|
||
|
|
Assert.Equal(C.SAVE_SCHEMA_VERSION, h.Version);
|
||
|
|
}
|
||
|
|
}
|