4c67c2c90549c950731edb40b035f4e1a6ec1095
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
45daa6e114 |
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 |
||
|
|
3c60e2cd45 |
Close the kernel's four open questions
Real decisions with real code behind them, not just answers written
into the doc:
- Time and Log: both stay in the kernel, both on IPluginContext.
Log was already built this way by accident; Time (DeltaTime,
ElapsedTime, FrameCount) ships now, split into ITime (plugin-facing,
read-only) and Time (host-facing, an internal Tick(deltaTime) only
Engine.Host calls) — the same split Schedule/ISchedule already
established. The fixed-step accumulator from the original kernel
scope is explicitly NOT included: nothing exists to test it against
yet (no physics), so building it now would be exactly the kind of
untested speculative machinery this project has avoided everywhere
else. It arrives with M4, alongside the Stage.FixedUpdate it would
drive — a "FixedUpdate" stage with no real fixed-timestep semantics
behind it would be actively misleading, not just incomplete.
- Event Bus: real Publish/Subscribe/RemoveAllFrom, not events-as-
World-entities (a worse fit for GameObject's persistent identity
than for a disposable-entity pure ECS). Given a real consumer
immediately rather than shipped as an unused API: PluginHost now
publishes PluginLoaded/PluginUnloaded, and sandbox.echo subscribes
to PluginLoaded for real, cleaning up in Shutdown() via
Events.RemoveAllFrom — mirroring exactly how Schedule.RemoveAllFrom
was proven. The 200-cycle leak test in AlcUnloadTests now exercises
this cleanup path too, not just Schedule's; still green, stable
across repeated runs. GameWorld does NOT auto-publish on every
GameObject/Component change — a publish on every structural change
would tax the hot path for listeners that usually don't exist.
- Frame stages: fixed, kernel-defined, not plugin-extensible — a
stage is part of the shared vocabulary the host's loop and every
plugin rely on. Set stays {Update, Render} until FixedUpdate earns
its place alongside the accumulator in M4.
- Data-oriented fast path: left open on purpose, but with a trigger
condition instead of a deadline — revisit when a concrete system
(particles, the standing example) needs tens of thousands of
GameObjects updated per frame AND profiling, not intuition, shows
GameObject/Component overhead is the actual bottleneck.
PluginHost's constructor changed shape: takes EventBus (concrete, not
IEventBus — it needs RegisterPlugin, same reason it already took
Schedule instead of ISchedule) and ITime. NullEventBus is gone;
every call site now constructs a real EventBus. Engine.Host advances
Time each frame — real wall-clock delta in --windowed (via the
window's own Native.Time), a fixed nominal 1/60s in --headless, which
has no wall clock to measure and needs to stay deterministic anyway.
48 tests total now (40 in Engine.Kernel.Tests, 8 in
Engine.ConformanceHarness), all green on a clean build. Verified by
hand too: headless run against sandbox.echo now logs "[sandbox.echo]
observed load of 'sandbox.echo'" — the EventBus subscription actually
firing, not just compiling.
docs/kernel-contract.md's open-questions footer rewritten to record
each decision and why, plus the kernel scope table (§2) and the
IPluginContext listing (§3) updated to match what's actually built.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
|
||
|
|
246b969744 |
Close M0: headless CLI, JSON world dump, project-level plugin loading
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
|
||
|
|
978f34727f |
Implement PluginHost: two-ALC plugin loading, unloading verified live
The centerpiece of the whole "no domain reload" claim, now proven empirically rather than argued on paper: 200 load/unload cycles against a real plugin (sandbox.echo), each one checked with a WeakReference that the collectible ALC actually collected — docs/kernel-contract.md §4's leak test, previously Skip-marked since the very first scaffold, now runs and passes (stable across repeated runs). New pieces, minimal by design: - PluginLoadContext: the collectible ALC a plugin's implementation loads into. Load() defers to whatever's already in the Default ALC (Engine.Kernel, the plugin's own Contracts assembly) before consulting AssemblyDependencyResolver for genuinely private dependencies — the standard .NET plugin pattern, needed so component types stay identical across the plugin boundary instead of loading as two distinct, incompatible copies. - PluginHost: reads plugin.json, loads Contracts into the Default ALC (once — verified directly, not just inferred from the leak test), loads the implementation into a fresh PluginLoadContext, finds the IPlugin type via reflection, calls Configure(). Unload() calls Shutdown() first, then .Unload()s the ALC and hands back a WeakReference for the caller to check. - Schedule, ServiceRegistry, NullEventBus, ConsoleLogger: minimal real implementations of the remaining IPluginContext pieces — no stage execution or parallelism in Schedule yet, that's separate Scheduler work. Schedule.RemoveAllFrom is the one piece that has to be correct now, not later: it's what lets a plugin's Shutdown() actually drop the delegate reference into its own collectible ALC, which is exactly what the leak test is checking end to end. Explicitly out of scope for this pass: resolving a project's or plugin's dependsOn graph to order loading across multiple plugins. Nothing to test that against yet — sandbox.echo is deliberately the only, dependency-free fixture. Noted as a TODO on PluginHost rather than built speculatively. Sandbox.Echo.csproj gets <EnableDynamicLoading>true</EnableDynamicLoading> (future plugins with real dependencies will need the deps.json this generates). Engine.ConformanceHarness.csproj now copies plugin.json and both built DLLs into one flat directory under its own output, matching the layout PluginHost.Load(directory) expects — via $(Configuration)/ $(TargetFramework)-aware CopyToOutputDirectory items, correct in Release too, not just Debug. 17 tests total now (13 in Engine.Kernel.Tests, 4 in Engine.ConformanceHarness), all passing on a clean build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N |
||
|
|
1459657408 |
Scaffold the .sln and M0 project structure
Buildable skeleton matching docs/kernel-contract.md: - src/Engine.Kernel — the frozen kernel. Interfaces and data types only (World, Component, GameObject, Transform, ISchedule, IServiceRegistry, IEventBus, ILogger, IPlugin/IPluginContext, plugin.json and project.json manifest models). No PluginHost/Scheduler/World implementation yet — that's M0's actual work, not scaffolding. - src/Engine.Host — the CLI runtime entry point, placeholder for now. - plugins/sandbox.echo — a minimal two-assembly plugin (Contracts in Default ALC, implementation in collectible ALC) that exists only to exercise the reload loop end to end once PluginHost exists. - tests/Engine.Kernel.Tests, tests/Engine.ConformanceHarness — wired up with one Skip-marked placeholder test each, naming what M0 needs to make them real (including the 200-cycle ALC leak test from §4). The harness references the sandbox plugin with ReferenceOutputAssembly="false" so it loads it dynamically by path instead of linking its types into its own Default ALC. - samples/EmptyProject — a starter project.json. - Directory.Build.props centralizes shared settings across every project, including AllowUnsafeBlocks=false to enforce the §7 rule at the build level rather than by convention. Solution builds clean and `dotnet test` runs both placeholders as Skipped (not failing) — confirms the wiring, not the engine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N |