The last piece of the agent loop from docs/kernel-contract.md §7:
- WorldDumper serializes a World to JSON — every GameObject, its
transform, its components (arbitrary plugin-defined classes, so this
leans on System.Text.Json's own reflection with IncludeFields=true,
since components are public fields, not properties). Components are
a list of {type, data}, not a dictionary keyed by type name:
AddComponent<T>() doesn't enforce uniqueness, so a GameObject can
carry two components of the same type, and a dictionary would throw
on exactly that case — tested directly.
- PluginHost.LoadProject reads a project.json and resolves each
referenced plugin id against engine + project-local search paths,
finally putting ProjectManifest/PluginReference to use — they'd sat
unused since the very first scaffold commit.
- Engine.Host is a real CLI now: `engine run --headless --plugins <dir>
--project <project.json> --frames <n> [--dump <path>]`. --scene and
--assert are explicitly rejected with a message pointing at why
(no scene format yet — that's M2; no query DSL was ever actually
specified for --assert, and jq over a plain JSON dump already covers
that need), rather than silently ignored or generically rejected.
Same for `engine diag why-pinned`: nothing has ever failed to unload
in testing, so there's nothing to build that against yet.
- EchoPlugin now seeds one Ping-bearing GameObject in Configure() —
sandbox.echo is documented as a test fixture, not a real subsystem,
and there's no scene format yet to seed content any other way.
Verified by hand, not just by unit tests, since Program.cs itself
isn't covered by any: assembled a real flat plugin directory
(plugin.json + both built DLLs) and actually ran the CLI against
sandbox.echo end to end — 3 frames in, Ping.Count came back 3 in the
dump. Also checked every "not implemented" path (--scene, `diag`,
missing required args) prints its intended message rather than a
generic error.
32 tests total now (26 in Engine.Kernel.Tests, 6 in
Engine.ConformanceHarness), all green on a clean build.
README's Status section updated — M0 was the whole reason this
project exists (fast iteration without Unity's domain reload), and
that claim is no longer just architecture on paper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
69 lines
2.3 KiB
C#
69 lines
2.3 KiB
C#
using System.Numerics;
|
|
using System.Text.Json;
|
|
using Engine.Kernel.Diagnostics;
|
|
using Engine.Kernel.World;
|
|
|
|
namespace Engine.Kernel.Tests;
|
|
|
|
public class WorldDumperTests
|
|
{
|
|
private sealed class Health : Component
|
|
{
|
|
public int Value;
|
|
}
|
|
|
|
[Fact]
|
|
public void Dump_Includes_Name_Transform_And_Component_Fields()
|
|
{
|
|
var world = new GameWorld();
|
|
var go = world.CreateGameObject("Hero");
|
|
go.Transform.LocalPosition = new Vector3(1, 2, 3);
|
|
go.AddComponent<Health>().Value = 42;
|
|
|
|
using var doc = JsonDocument.Parse(WorldDumper.ToJson(world));
|
|
var root = doc.RootElement[0];
|
|
|
|
Assert.Equal("Hero", root.GetProperty("name").GetString());
|
|
|
|
var position = root.GetProperty("transform").GetProperty("position");
|
|
Assert.Equal(1, position[0].GetSingle());
|
|
Assert.Equal(2, position[1].GetSingle());
|
|
Assert.Equal(3, position[2].GetSingle());
|
|
|
|
var component = root.GetProperty("components")[0];
|
|
Assert.Equal("Health", component.GetProperty("type").GetString());
|
|
Assert.Equal(42, component.GetProperty("data").GetProperty("Value").GetInt32());
|
|
}
|
|
|
|
[Fact]
|
|
public void Dump_Nests_Children_Under_Their_Parent_Rather_Than_Listing_Them_At_The_Top_Level()
|
|
{
|
|
var world = new GameWorld();
|
|
var parent = world.CreateGameObject("Parent");
|
|
var child = world.CreateGameObject("Child");
|
|
child.SetParent(parent);
|
|
|
|
using var doc = JsonDocument.Parse(WorldDumper.ToJson(world));
|
|
|
|
Assert.Equal(1, doc.RootElement.GetArrayLength());
|
|
var root = doc.RootElement[0];
|
|
Assert.Equal("Parent", root.GetProperty("name").GetString());
|
|
Assert.Equal("Child", root.GetProperty("children")[0].GetProperty("name").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public void Dump_Represents_Duplicate_Components_Of_The_Same_Type_As_A_List_Not_A_Dictionary()
|
|
{
|
|
var world = new GameWorld();
|
|
var go = world.CreateGameObject("A");
|
|
go.AddComponent<Health>().Value = 1;
|
|
go.AddComponent<Health>().Value = 2;
|
|
|
|
// A dictionary keyed by type name would throw on this exact case.
|
|
using var doc = JsonDocument.Parse(WorldDumper.ToJson(world));
|
|
|
|
var components = doc.RootElement[0].GetProperty("components");
|
|
Assert.Equal(2, components.GetArrayLength());
|
|
}
|
|
}
|