M2, part one: scene format — World actually saves and loads now
The first half of M2's "done when" (a scene loads and saves) rather than the whole milestone — the asset hot-reload half is a comparably sized, separate chunk of work, staged on its own rather than crammed in alongside this. SceneFormat replaces WorldDumper rather than sitting next to it: there was never a real reason for "what an agent reads to check a frame" (the existing --dump) and "what a scene file actually is" to be two different JSON shapes, and keeping them one removes the question of which shape a save/load round trip is supposed to match. Moved from Engine.Kernel.Diagnostics to Engine.Kernel.World to match — this is core content loading now, not a debug tool that happens to also serialize things. Components are tagged "TypeFullName, AssemblyName" (partial-name form, deliberately no version) so Type.GetType resolves them against whatever's loaded regardless of an incidental version bump on the plugin that defines them — full four-part AssemblyQualifiedName would have made every saved scene brittle against that. Loading a scene whose component type isn't loaded fails loudly, naming the missing type, rather than silently dropping data. New kernel API this needed: GameObject.AddComponent(Component) — attaches an already-constructed instance, for a caller (the deserializer) that only has a runtime Type from a file, not a compile-time T. Deserializing straight into a real instance via JsonSerializer.Deserialize(json, componentType) and attaching that is simpler and more certain than constructing an empty component through reflection and then trying to populate it after the fact. Engine.Host: --scene now does something (was an explicit "not implemented yet" since the very first CLI pass) — loads additively after every plugin in --project, since a scene's component type tags only resolve once the plugin defining them has loaded its Contracts assembly. Verified beyond the round-trip unit tests: two separate real CLI runs, sandbox.echo both times. Run 1 ticks 3 frames and dumps a scene (Ping.Count: 3). Run 2 loads that scene fresh alongside its own newly-seeded Ping (Count: 0) and ticks 2 more frames — dump shows Count: 2 for the fresh one and Count: 5 for the loaded one. Not just "the file round-trips" — the loaded component's state kept being a real, live, Scheduler-ticked object across the save/load boundary. 44 tests in Engine.Kernel.Tests now (up from 40 — 3 carried over from WorldDumperTests plus 4 new round-trip/error-path tests), 8 in Engine.ConformanceHarness unaffected. All green on a clean build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Engine.Kernel.World;
|
||||
|
||||
namespace Engine.Kernel.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a World to JSON for the headless introspection loop described
|
||||
/// in docs/kernel-contract.md §7 — an agent's only way to see the effect of
|
||||
/// an edit without a screen. Read-only and outside any system's execution,
|
||||
/// so it isn't subject to SystemAccessScope enforcement.
|
||||
///
|
||||
/// Component types are arbitrary plugin-defined classes, so this leans on
|
||||
/// System.Text.Json's own reflection rather than anything bespoke —
|
||||
/// including IncludeFields, since components are plain public fields
|
||||
/// (docs/kernel-contract.md §1), not properties.
|
||||
/// </summary>
|
||||
public static class WorldDumper
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
IncludeFields = true,
|
||||
};
|
||||
|
||||
public static string ToJson(IWorld world)
|
||||
{
|
||||
var roots = world.Roots.Select(Dump).ToList();
|
||||
return JsonSerializer.Serialize(roots, Options);
|
||||
}
|
||||
|
||||
private static object Dump(GameObject go) => new
|
||||
{
|
||||
name = go.Name,
|
||||
transform = new
|
||||
{
|
||||
position = ToArray(go.Transform.LocalPosition),
|
||||
rotation = new[]
|
||||
{
|
||||
go.Transform.LocalRotation.X, go.Transform.LocalRotation.Y,
|
||||
go.Transform.LocalRotation.Z, go.Transform.LocalRotation.W,
|
||||
},
|
||||
scale = ToArray(go.Transform.LocalScale),
|
||||
},
|
||||
// A list, not a dictionary keyed by type name: AddComponent<T>()
|
||||
// doesn't enforce uniqueness (see the World row in §2), so a
|
||||
// GameObject can legitimately carry two components of the same
|
||||
// type. A dictionary would throw on the very case this needs to
|
||||
// represent correctly.
|
||||
components = go.Components
|
||||
.Select(c => new { type = c.GetType().Name, data = (object)c })
|
||||
.ToList(),
|
||||
children = go.Children.Select(Dump).ToList(),
|
||||
};
|
||||
|
||||
private static float[] ToArray(System.Numerics.Vector3 v) => [v.X, v.Y, v.Z];
|
||||
}
|
||||
Reference in New Issue
Block a user