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
51 lines
1.8 KiB
C#
51 lines
1.8 KiB
C#
using Engine.Kernel.Plugins;
|
|
using Engine.Kernel.Scheduling;
|
|
using Engine.Kernel.World;
|
|
using Sandbox.Echo.Contracts;
|
|
|
|
namespace Sandbox.Echo;
|
|
|
|
/// <summary>
|
|
/// M0's test fixture. Exists to exercise the full loop end to end — plugin
|
|
/// load, a real cross-ALC system registered and invoked by Schedule, and
|
|
/// hot reload — before any real subsystem exists to test any of it
|
|
/// against. See M0 in docs/kernel-contract.md §8.
|
|
/// </summary>
|
|
public sealed class EchoPlugin : IPlugin
|
|
{
|
|
public void Configure(IPluginContext ctx)
|
|
{
|
|
ctx.Schedule.Add(Stage.Update, Tick)
|
|
.Writes<Ping>();
|
|
|
|
// A real cross-ALC subscription, not just a system — this is what
|
|
// makes the 200-cycle leak test in AlcUnloadTests actually prove
|
|
// EventBus.RemoveAllFrom works, the same way registering Tick
|
|
// above proves it for Schedule.RemoveAllFrom.
|
|
ctx.Events.Subscribe<PluginLoaded>(OnPluginLoaded);
|
|
|
|
// There's no scene format yet (that's M2) — nothing else will ever
|
|
// put a GameObject in front of `engine run --headless`, so this
|
|
// deliberately-a-test-fixture plugin seeds its own. A real plugin
|
|
// wouldn't do this; scene content isn't a subsystem's job.
|
|
ctx.World.CreateGameObject("sandbox.echo:ping").AddComponent<Ping>();
|
|
|
|
ctx.Log.Info("sandbox.echo configured");
|
|
}
|
|
|
|
public void Shutdown(IPluginContext ctx)
|
|
{
|
|
ctx.Schedule.RemoveAllFrom("sandbox.echo");
|
|
ctx.Events.RemoveAllFrom("sandbox.echo");
|
|
}
|
|
|
|
static void Tick(IWorld world)
|
|
{
|
|
foreach (var go in world.Query<Ping>())
|
|
go.GetComponent<Ping>()!.Count++;
|
|
}
|
|
|
|
static void OnPluginLoaded(PluginLoaded evt) =>
|
|
Console.WriteLine($"[sandbox.echo] observed load of '{evt.PluginId}'");
|
|
}
|