Files
lingua-engine/tests/Engine.Kernel.Tests/WorldSnapshotTests.cs
T
EmilandClaude Sonnet 5 2f5a8bfde6 M3, part one: World.Snapshot()/Restore() — Play mode's actual mechanic
The TODO left on IWorld since the very first kernel scaffold commit,
closed: a deep, opaque snapshot of every GameObject and Component,
for Play/Stop to build on. Not a new clone mechanism — backed by
SceneFormat. A scene file and a Play-mode snapshot are the same
problem (capture every GameObject faithfully enough to reconstruct
it) at two different moments; reusing already-proven serialization
beats maintaining a second way to walk the same graph.

Restore() is NOT additive the way SceneFormat.Load() is by design —
it destroys every current root first. Play mode always restores onto
a world it's about to fully own; additive semantics would be the
wrong default here even though they're the right one for loading a
scene into existing content.

Verified against M3's actual "done when" (entering Play takes under
100 ms), not just round-trip correctness: 300 GameObjects, each with
a component, snapshot + restore end to end comes in well under the
100 ms bound — asserted directly with a Stopwatch, not eyeballed.
Also covers what Play mode depends on specifically: mutations,
GameObjects created or destroyed, and hierarchy changes made after
the snapshot are all discarded on Restore.

73 tests total now (49 in Engine.Kernel.Tests, 8 in
Engine.Assets.Tests, 8 in Engine.ConformanceHarness... — wait, that's
65; the two build-time contract projects add no test counts. Actual
total per the run: 49+8+8 = 65.), all green on a clean build.

Next for M3: the editor itself — hierarchy, a reflection-based
inspector, Play/Stop wired to this, gizmos. All still ahead; this
commit is only the kernel mechanic underneath Play/Stop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 15:54:49 +03:00

107 lines
3.1 KiB
C#

using System.Diagnostics;
using Engine.Kernel.World;
namespace Engine.Kernel.Tests;
public class WorldSnapshotTests
{
private sealed class Health : Component
{
public int Value;
}
[Fact]
public void Restore_Discards_Mutations_Made_After_The_Snapshot()
{
var world = new GameWorld();
var go = world.CreateGameObject("Hero");
go.AddComponent<Health>().Value = 100;
var snapshot = world.Snapshot();
go.GetComponent<Health>()!.Value = 1; // "took damage" during Play
world.CreateGameObject("SpawnedDuringPlay");
world.Restore(snapshot);
var root = Assert.Single(world.Roots);
Assert.Equal("Hero", root.Name);
Assert.Equal(100, root.GetComponent<Health>()!.Value);
}
[Fact]
public void Restore_Removes_GameObjects_Created_After_The_Snapshot()
{
var world = new GameWorld();
world.CreateGameObject("Original");
var snapshot = world.Snapshot();
world.CreateGameObject("Spawned");
world.Restore(snapshot);
Assert.Single(world.Roots);
Assert.Equal("Original", world.Roots[0].Name);
}
[Fact]
public void Restore_Recreates_GameObjects_Destroyed_After_The_Snapshot()
{
var world = new GameWorld();
world.CreateGameObject("WillBeDestroyed");
var snapshot = world.Snapshot();
world.Destroy(world.Roots[0]);
Assert.Empty(world.Roots);
world.Restore(snapshot);
Assert.Single(world.Roots);
Assert.Equal("WillBeDestroyed", world.Roots[0].Name);
}
[Fact]
public void Restore_Preserves_The_Parent_Child_Hierarchy()
{
var world = new GameWorld();
var parent = world.CreateGameObject("Parent");
var child = world.CreateGameObject("Child");
child.SetParent(parent);
var snapshot = world.Snapshot();
child.SetParent(null); // detach during Play
world.Restore(snapshot);
var root = Assert.Single(world.Roots);
var restoredChild = Assert.Single(root.Children);
Assert.Equal("Child", restoredChild.Name);
}
// M3's actual "done when": entering Play takes under 100 ms. A
// realistic indie-scale scene (a few hundred GameObjects, each with a
// component) should clear that with room to spare — this asserts a
// generous 100 ms bound end to end (snapshot + restore, i.e. both
// EnterPlay and ExitPlay), not a tight one that would make this test
// flaky on a loaded CI box for no reason.
[Fact]
public void Snapshot_And_Restore_A_Few_Hundred_GameObjects_Well_Under_100ms()
{
var world = new GameWorld();
for (var i = 0; i < 300; i++)
{
var go = world.CreateGameObject($"Object{i}");
go.AddComponent<Health>().Value = i;
}
var stopwatch = Stopwatch.StartNew();
var snapshot = world.Snapshot();
world.Restore(snapshot);
stopwatch.Stop();
Assert.True(
stopwatch.ElapsedMilliseconds < 100,
$"Snapshot + Restore of 300 GameObjects took {stopwatch.ElapsedMilliseconds} ms, expected < 100 ms.");
}
}