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
This commit is contained in:
Emil
2026-09-02 00:26:08 +03:00
co-authored by Claude Sonnet 5
parent 7fe966e42f
commit 1459657408
29 changed files with 556 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
namespace Engine.Kernel.Diagnostics;
public interface ILogger
{
void Info(string message);
void Warn(string message);
void Error(string message);
}
+3
View File
@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>
+11
View File
@@ -0,0 +1,11 @@
namespace Engine.Kernel.Events;
/// <summary>
/// Open question (see the footer of docs/kernel-contract.md): whether this
/// is needed at launch at all, or whether event-components in World cover
/// its role. Left as a bare marker so IPluginContext compiles — no API
/// decided yet.
/// </summary>
public interface IEventBus
{
}
+17
View File
@@ -0,0 +1,17 @@
namespace Engine.Kernel.Plugins;
/// <summary>
/// A plugin holds no game state — none. State lives in World; the plugin
/// is code that operates on it. See docs/kernel-contract.md §3.
/// </summary>
public interface IPlugin
{
/// <summary>Registration: services, systems, component types.</summary>
void Configure(IPluginContext ctx);
/// <summary>
/// Full undo of Configure. Whether this method is honest determines
/// whether the ALC unloads at all — see §4.
/// </summary>
void Shutdown(IPluginContext ctx);
}
@@ -0,0 +1,16 @@
using Engine.Kernel.Diagnostics;
using Engine.Kernel.Events;
using Engine.Kernel.Scheduling;
using Engine.Kernel.Services;
using Engine.Kernel.World;
namespace Engine.Kernel.Plugins;
public interface IPluginContext
{
IWorld World { get; } // data
IServiceRegistry Services { get; } // Provide<T> / Require<T>
ISchedule Schedule { get; } // systems and ordering
IEventBus Events { get; }
ILogger Log { get; }
}
@@ -0,0 +1,22 @@
namespace Engine.Kernel.Plugins;
/// <summary>
/// Deserialized shape of a plugin's <c>plugin.json</c>. Read *before*
/// anything loads, so the Plugin Host can build the dependency graph ahead
/// of any ALC loading — see docs/kernel-contract.md §3.
/// </summary>
public sealed class PluginManifest
{
public required string Id { get; init; }
public required string Version { get; init; }
/// <summary>Assembly loaded into the Default ALC. Never unloads. See §4.</summary>
public required string Contracts { get; init; }
/// <summary>Assembly loaded into a collectible ALC. Reloadable. See §4.</summary>
public required string Assembly { get; init; }
public Dictionary<string, string> DependsOn { get; init; } = new();
public bool Reloadable { get; init; } = true;
}
@@ -0,0 +1,25 @@
namespace Engine.Kernel.Plugins;
/// <summary>
/// Deserialized shape of a project's <c>project.json</c> — which plugins a
/// specific game loads, at which versions, and where to find its own. See
/// "Per-project configuration" in docs/kernel-contract.md §2.
/// </summary>
public sealed class ProjectManifest
{
public required string EngineVersion { get; init; }
public List<PluginReference> Plugins { get; init; } = new();
/// <summary>Search paths for plugins local to this project, not shipped
/// with the engine.</summary>
public List<string> PluginPaths { get; init; } = new();
}
public sealed class PluginReference
{
public required string Id { get; init; }
/// <summary>Null means "whatever the engine ships by default."</summary>
public string? Version { get; init; }
}
+13
View File
@@ -0,0 +1,13 @@
namespace Engine.Kernel.Scheduling;
/// <summary>
/// Frame stages and system ordering. See docs/kernel-contract.md §2.
/// </summary>
public interface ISchedule
{
ISystemBuilder Add(Stage stage, Delegate system);
/// <summary>Called from a plugin's Shutdown() — must remove everything
/// Configure() added, or the ALC it lives in will never unload. See §4.</summary>
void RemoveAllFrom(string pluginId);
}
@@ -0,0 +1,16 @@
using Engine.Kernel.World;
namespace Engine.Kernel.Scheduling;
/// <summary>
/// Fluent builder returned by <see cref="ISchedule.Add"/>. Declared
/// Reads/Writes are what lets the scheduler run systems with disjoint
/// access in parallel and, in debug builds, enforce that a system only
/// touches what it declared. See docs/kernel-contract.md §2 and §7.
/// </summary>
public interface ISystemBuilder
{
ISystemBuilder After(string systemId);
ISystemBuilder Reads<T>() where T : Component;
ISystemBuilder Writes<T>() where T : Component;
}
+12
View File
@@ -0,0 +1,12 @@
namespace Engine.Kernel.Scheduling;
/// <summary>
/// Open question (see the footer of docs/kernel-contract.md): whether the
/// set of stages is fixed or plugin-extensible. This is only enough to
/// make the §3 example compile — not a decision.
/// </summary>
public enum Stage
{
Update,
Render,
}
@@ -0,0 +1,13 @@
namespace Engine.Kernel.Services;
/// <summary>
/// The control-plane channel between plugins: commands and resources, not
/// per-entity data. See the "two channels" table in docs/kernel-contract.md
/// §2 — and the rule right below it about what never belongs here.
/// </summary>
public interface IServiceRegistry
{
void Provide<T>(T instance) where T : class;
T Require<T>() where T : class;
void Revoke<T>() where T : class;
}
+11
View File
@@ -0,0 +1,11 @@
namespace Engine.Kernel.World;
/// <summary>
/// Base class for all component data. Plain fields, no methods, no
/// lifecycle hooks — behavior lives in systems, never on the component
/// itself. See docs/kernel-contract.md §1 and §7.
/// </summary>
public abstract class Component
{
// Intentionally empty. A subclass adds fields only.
}
+28
View File
@@ -0,0 +1,28 @@
namespace Engine.Kernel.World;
/// <summary>
/// The entity type: identity, hierarchy, and a list of components. See
/// docs/kernel-contract.md §2.
/// </summary>
public sealed class GameObject
{
public required string Name { get; set; }
public Transform Transform;
public GameObject? Parent { get; internal set; }
// TODO(M0): backing storage for children, tags, and the component list
// + type index that makes World.Query<T>() O(matches), not O(all).
public IReadOnlyList<GameObject> Children => throw new NotImplementedException();
public T? GetComponent<T>() where T : Component
=> throw new NotImplementedException();
public T AddComponent<T>() where T : Component, new()
=> throw new NotImplementedException();
public void RemoveComponent<T>() where T : Component
=> throw new NotImplementedException();
}
+19
View File
@@ -0,0 +1,19 @@
namespace Engine.Kernel.World;
/// <summary>
/// Kernel-owned storage for every <see cref="GameObject"/> in a scene.
/// Shape sketched from its usage throughout docs/kernel-contract.md — not a
/// final API; M0's job is to actually implement this.
/// </summary>
public interface IWorld
{
GameObject CreateGameObject(string name);
void Destroy(GameObject go);
/// <summary>Type-indexed lookup — O(matches), not O(all). See §2.</summary>
IEnumerable<GameObject> Query<T>() where T : Component;
// TODO(§5): Snapshot()/Restore() for Play mode — a deep clone of the
// GameObject graph, taken on EnterPlay and discarded on ExitPlay.
}
+18
View File
@@ -0,0 +1,18 @@
using System.Numerics;
namespace Engine.Kernel.World;
/// <summary>
/// Embedded directly on <see cref="GameObject"/> rather than modeled as a
/// <see cref="Component"/> subclass, because nearly every system touches it
/// every frame — see the kernel scope table in docs/kernel-contract.md §2.
/// </summary>
public struct Transform
{
public Vector3 LocalPosition;
public Quaternion LocalRotation;
public Vector3 LocalScale;
// TODO(M0): derive from the GameObject hierarchy once World can walk it.
public readonly Matrix4x4 WorldMatrix => throw new NotImplementedException();
}