diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..e90715f --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + net9.0 + 13.0 + enable + enable + + + false + + + diff --git a/LinguaEngine.sln b/LinguaEngine.sln new file mode 100644 index 0000000..05fbe3f --- /dev/null +++ b/LinguaEngine.sln @@ -0,0 +1,121 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Kernel", "src\Engine.Kernel\Engine.Kernel.csproj", "{40297197-30BF-4180-88C9-916E66859E0F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Host", "src\Engine.Host\Engine.Host.csproj", "{A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{07D57EEB-2F50-60C4-C011-FE4FA775C9A8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox.echo", "sandbox.echo", "{5277871D-FD82-4E0F-9994-47CB654DDE62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox.Echo.Contracts", "plugins\sandbox.echo\Sandbox.Echo.Contracts\Sandbox.Echo.Contracts.csproj", "{6CDA3EB0-CC08-48E6-B946-183D31840661}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox.Echo", "plugins\sandbox.echo\Sandbox.Echo\Sandbox.Echo.csproj", "{B0F967C4-B801-4A53-829E-689C2728553A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Kernel.Tests", "tests\Engine.Kernel.Tests\Engine.Kernel.Tests.csproj", "{D7C61E35-8805-4C28-B3D7-084D8FBC6820}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.ConformanceHarness", "tests\Engine.ConformanceHarness\Engine.ConformanceHarness.csproj", "{C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {40297197-30BF-4180-88C9-916E66859E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Debug|x64.Build.0 = Debug|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Debug|x86.ActiveCfg = Debug|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Debug|x86.Build.0 = Debug|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Release|Any CPU.Build.0 = Release|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Release|x64.ActiveCfg = Release|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Release|x64.Build.0 = Release|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Release|x86.ActiveCfg = Release|Any CPU + {40297197-30BF-4180-88C9-916E66859E0F}.Release|x86.Build.0 = Release|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Debug|x64.Build.0 = Debug|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Debug|x86.Build.0 = Debug|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Release|Any CPU.Build.0 = Release|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Release|x64.ActiveCfg = Release|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Release|x64.Build.0 = Release|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Release|x86.ActiveCfg = Release|Any CPU + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF}.Release|x86.Build.0 = Release|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Debug|x64.ActiveCfg = Debug|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Debug|x64.Build.0 = Debug|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Debug|x86.ActiveCfg = Debug|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Debug|x86.Build.0 = Debug|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Release|Any CPU.Build.0 = Release|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Release|x64.ActiveCfg = Release|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Release|x64.Build.0 = Release|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Release|x86.ActiveCfg = Release|Any CPU + {6CDA3EB0-CC08-48E6-B946-183D31840661}.Release|x86.Build.0 = Release|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Debug|x64.Build.0 = Debug|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Debug|x86.Build.0 = Debug|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Release|Any CPU.Build.0 = Release|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Release|x64.ActiveCfg = Release|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Release|x64.Build.0 = Release|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Release|x86.ActiveCfg = Release|Any CPU + {B0F967C4-B801-4A53-829E-689C2728553A}.Release|x86.Build.0 = Release|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Debug|x64.Build.0 = Debug|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Debug|x86.Build.0 = Debug|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Release|Any CPU.Build.0 = Release|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Release|x64.ActiveCfg = Release|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Release|x64.Build.0 = Release|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Release|x86.ActiveCfg = Release|Any CPU + {D7C61E35-8805-4C28-B3D7-084D8FBC6820}.Release|x86.Build.0 = Release|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Debug|x64.Build.0 = Debug|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Debug|x86.Build.0 = Debug|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Release|Any CPU.Build.0 = Release|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Release|x64.ActiveCfg = Release|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Release|x64.Build.0 = Release|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Release|x86.ActiveCfg = Release|Any CPU + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {40297197-30BF-4180-88C9-916E66859E0F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A12B1299-FAF6-4BC9-A54A-D23F7A6DB6DF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5277871D-FD82-4E0F-9994-47CB654DDE62} = {07D57EEB-2F50-60C4-C011-FE4FA775C9A8} + {6CDA3EB0-CC08-48E6-B946-183D31840661} = {5277871D-FD82-4E0F-9994-47CB654DDE62} + {B0F967C4-B801-4A53-829E-689C2728553A} = {5277871D-FD82-4E0F-9994-47CB654DDE62} + {D7C61E35-8805-4C28-B3D7-084D8FBC6820} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {C9725988-61CD-4EFC-A3DD-F7F42B3C4B2D} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + EndGlobalSection +EndGlobal diff --git a/plugins/sandbox.echo/Sandbox.Echo.Contracts/Ping.cs b/plugins/sandbox.echo/Sandbox.Echo.Contracts/Ping.cs new file mode 100644 index 0000000..c301a16 --- /dev/null +++ b/plugins/sandbox.echo/Sandbox.Echo.Contracts/Ping.cs @@ -0,0 +1,13 @@ +using Engine.Kernel.World; + +namespace Sandbox.Echo.Contracts; + +/// +/// Minimal component for exercising the reload loop described in +/// docs/kernel-contract.md §7 — not a real engine feature, just something +/// for a system to touch before any real plugin exists. +/// +public sealed class Ping : Component +{ + public int Count; +} diff --git a/plugins/sandbox.echo/Sandbox.Echo.Contracts/Sandbox.Echo.Contracts.csproj b/plugins/sandbox.echo/Sandbox.Echo.Contracts/Sandbox.Echo.Contracts.csproj new file mode 100644 index 0000000..e4acf50 --- /dev/null +++ b/plugins/sandbox.echo/Sandbox.Echo.Contracts/Sandbox.Echo.Contracts.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/sandbox.echo/Sandbox.Echo/EchoPlugin.cs b/plugins/sandbox.echo/Sandbox.Echo/EchoPlugin.cs new file mode 100644 index 0000000..e8da0fb --- /dev/null +++ b/plugins/sandbox.echo/Sandbox.Echo/EchoPlugin.cs @@ -0,0 +1,33 @@ +using Engine.Kernel.Plugins; +using Engine.Kernel.Scheduling; +using Engine.Kernel.World; +using Sandbox.Echo.Contracts; + +namespace Sandbox.Echo; + +/// +/// M0's test fixture. Exists only to exercise the reload loop end to end — +/// edit, rebuild, ALC reload, headless run, dump — before any real +/// subsystem exists to test it against. See M0 in docs/kernel-contract.md §8. +/// +public sealed class EchoPlugin : IPlugin +{ + public void Configure(IPluginContext ctx) + { + ctx.Schedule.Add(Stage.Update, Tick) + .Writes(); + + ctx.Log.Info("sandbox.echo configured"); + } + + public void Shutdown(IPluginContext ctx) + { + ctx.Schedule.RemoveAllFrom("sandbox.echo"); + } + + static void Tick(IWorld world) + { + // TODO(M0): bump every Ping.Count by one. Placeholder until the + // Scheduler actually exists to invoke this. + } +} diff --git a/plugins/sandbox.echo/Sandbox.Echo/Sandbox.Echo.csproj b/plugins/sandbox.echo/Sandbox.Echo/Sandbox.Echo.csproj new file mode 100644 index 0000000..01983e7 --- /dev/null +++ b/plugins/sandbox.echo/Sandbox.Echo/Sandbox.Echo.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/sandbox.echo/plugin.json b/plugins/sandbox.echo/plugin.json new file mode 100644 index 0000000..f686ea8 --- /dev/null +++ b/plugins/sandbox.echo/plugin.json @@ -0,0 +1,8 @@ +{ + "id": "sandbox.echo", + "version": "0.0.1", + "contracts": "Sandbox.Echo.Contracts.dll", + "assembly": "Sandbox.Echo.dll", + "dependsOn": {}, + "reloadable": true +} diff --git a/samples/EmptyProject/project.json b/samples/EmptyProject/project.json new file mode 100644 index 0000000..1b79ced --- /dev/null +++ b/samples/EmptyProject/project.json @@ -0,0 +1,7 @@ +{ + "engineVersion": "^0.1", + "plugins": [ + { "id": "sandbox.echo" } + ], + "pluginPaths": [] +} diff --git a/src/Engine.Host/Engine.Host.csproj b/src/Engine.Host/Engine.Host.csproj new file mode 100644 index 0000000..7b13f52 --- /dev/null +++ b/src/Engine.Host/Engine.Host.csproj @@ -0,0 +1,11 @@ + + + + + + + + Exe + + + diff --git a/src/Engine.Host/Program.cs b/src/Engine.Host/Program.cs new file mode 100644 index 0000000..36b5ed8 --- /dev/null +++ b/src/Engine.Host/Program.cs @@ -0,0 +1,11 @@ +// The runtime host: resolves a project's plugins and runs the engine loop. +// Its shape is sketched in docs/kernel-contract.md §7 — +// +// engine run --headless --frames 60 --scene ... --dump ... --assert ... +// engine diag why-pinned +// +// Neither the argument parsing nor the loop it drives exists yet; both +// depend on PluginHost and Scheduler, which are M0's actual work. This is +// a placeholder so the solution builds and runs end to end. + +Console.WriteLine("Lingua Engine host — not yet implemented. See docs/kernel-contract.md, M0."); diff --git a/src/Engine.Kernel/Diagnostics/ILogger.cs b/src/Engine.Kernel/Diagnostics/ILogger.cs new file mode 100644 index 0000000..3ddac55 --- /dev/null +++ b/src/Engine.Kernel/Diagnostics/ILogger.cs @@ -0,0 +1,8 @@ +namespace Engine.Kernel.Diagnostics; + +public interface ILogger +{ + void Info(string message); + void Warn(string message); + void Error(string message); +} diff --git a/src/Engine.Kernel/Engine.Kernel.csproj b/src/Engine.Kernel/Engine.Kernel.csproj new file mode 100644 index 0000000..c632161 --- /dev/null +++ b/src/Engine.Kernel/Engine.Kernel.csproj @@ -0,0 +1,3 @@ + + + diff --git a/src/Engine.Kernel/Events/IEventBus.cs b/src/Engine.Kernel/Events/IEventBus.cs new file mode 100644 index 0000000..a37acab --- /dev/null +++ b/src/Engine.Kernel/Events/IEventBus.cs @@ -0,0 +1,11 @@ +namespace Engine.Kernel.Events; + +/// +/// 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. +/// +public interface IEventBus +{ +} diff --git a/src/Engine.Kernel/Plugins/IPlugin.cs b/src/Engine.Kernel/Plugins/IPlugin.cs new file mode 100644 index 0000000..f86b101 --- /dev/null +++ b/src/Engine.Kernel/Plugins/IPlugin.cs @@ -0,0 +1,17 @@ +namespace Engine.Kernel.Plugins; + +/// +/// 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. +/// +public interface IPlugin +{ + /// Registration: services, systems, component types. + void Configure(IPluginContext ctx); + + /// + /// Full undo of Configure. Whether this method is honest determines + /// whether the ALC unloads at all — see §4. + /// + void Shutdown(IPluginContext ctx); +} diff --git a/src/Engine.Kernel/Plugins/IPluginContext.cs b/src/Engine.Kernel/Plugins/IPluginContext.cs new file mode 100644 index 0000000..a2a7730 --- /dev/null +++ b/src/Engine.Kernel/Plugins/IPluginContext.cs @@ -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 / Require + ISchedule Schedule { get; } // systems and ordering + IEventBus Events { get; } + ILogger Log { get; } +} diff --git a/src/Engine.Kernel/Plugins/PluginManifest.cs b/src/Engine.Kernel/Plugins/PluginManifest.cs new file mode 100644 index 0000000..7d2993a --- /dev/null +++ b/src/Engine.Kernel/Plugins/PluginManifest.cs @@ -0,0 +1,22 @@ +namespace Engine.Kernel.Plugins; + +/// +/// Deserialized shape of a plugin's plugin.json. Read *before* +/// anything loads, so the Plugin Host can build the dependency graph ahead +/// of any ALC loading — see docs/kernel-contract.md §3. +/// +public sealed class PluginManifest +{ + public required string Id { get; init; } + public required string Version { get; init; } + + /// Assembly loaded into the Default ALC. Never unloads. See §4. + public required string Contracts { get; init; } + + /// Assembly loaded into a collectible ALC. Reloadable. See §4. + public required string Assembly { get; init; } + + public Dictionary DependsOn { get; init; } = new(); + + public bool Reloadable { get; init; } = true; +} diff --git a/src/Engine.Kernel/Plugins/ProjectManifest.cs b/src/Engine.Kernel/Plugins/ProjectManifest.cs new file mode 100644 index 0000000..9d91937 --- /dev/null +++ b/src/Engine.Kernel/Plugins/ProjectManifest.cs @@ -0,0 +1,25 @@ +namespace Engine.Kernel.Plugins; + +/// +/// Deserialized shape of a project's project.json — 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. +/// +public sealed class ProjectManifest +{ + public required string EngineVersion { get; init; } + + public List Plugins { get; init; } = new(); + + /// Search paths for plugins local to this project, not shipped + /// with the engine. + public List PluginPaths { get; init; } = new(); +} + +public sealed class PluginReference +{ + public required string Id { get; init; } + + /// Null means "whatever the engine ships by default." + public string? Version { get; init; } +} diff --git a/src/Engine.Kernel/Scheduling/ISchedule.cs b/src/Engine.Kernel/Scheduling/ISchedule.cs new file mode 100644 index 0000000..a28ff58 --- /dev/null +++ b/src/Engine.Kernel/Scheduling/ISchedule.cs @@ -0,0 +1,13 @@ +namespace Engine.Kernel.Scheduling; + +/// +/// Frame stages and system ordering. See docs/kernel-contract.md §2. +/// +public interface ISchedule +{ + ISystemBuilder Add(Stage stage, Delegate system); + + /// Called from a plugin's Shutdown() — must remove everything + /// Configure() added, or the ALC it lives in will never unload. See §4. + void RemoveAllFrom(string pluginId); +} diff --git a/src/Engine.Kernel/Scheduling/ISystemBuilder.cs b/src/Engine.Kernel/Scheduling/ISystemBuilder.cs new file mode 100644 index 0000000..5c61590 --- /dev/null +++ b/src/Engine.Kernel/Scheduling/ISystemBuilder.cs @@ -0,0 +1,16 @@ +using Engine.Kernel.World; + +namespace Engine.Kernel.Scheduling; + +/// +/// Fluent builder returned by . 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. +/// +public interface ISystemBuilder +{ + ISystemBuilder After(string systemId); + ISystemBuilder Reads() where T : Component; + ISystemBuilder Writes() where T : Component; +} diff --git a/src/Engine.Kernel/Scheduling/Stage.cs b/src/Engine.Kernel/Scheduling/Stage.cs new file mode 100644 index 0000000..0f54625 --- /dev/null +++ b/src/Engine.Kernel/Scheduling/Stage.cs @@ -0,0 +1,12 @@ +namespace Engine.Kernel.Scheduling; + +/// +/// 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. +/// +public enum Stage +{ + Update, + Render, +} diff --git a/src/Engine.Kernel/Services/IServiceRegistry.cs b/src/Engine.Kernel/Services/IServiceRegistry.cs new file mode 100644 index 0000000..85b8984 --- /dev/null +++ b/src/Engine.Kernel/Services/IServiceRegistry.cs @@ -0,0 +1,13 @@ +namespace Engine.Kernel.Services; + +/// +/// 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. +/// +public interface IServiceRegistry +{ + void Provide(T instance) where T : class; + T Require() where T : class; + void Revoke() where T : class; +} diff --git a/src/Engine.Kernel/World/Component.cs b/src/Engine.Kernel/World/Component.cs new file mode 100644 index 0000000..70e7d68 --- /dev/null +++ b/src/Engine.Kernel/World/Component.cs @@ -0,0 +1,11 @@ +namespace Engine.Kernel.World; + +/// +/// 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. +/// +public abstract class Component +{ + // Intentionally empty. A subclass adds fields only. +} diff --git a/src/Engine.Kernel/World/GameObject.cs b/src/Engine.Kernel/World/GameObject.cs new file mode 100644 index 0000000..a19ac8e --- /dev/null +++ b/src/Engine.Kernel/World/GameObject.cs @@ -0,0 +1,28 @@ +namespace Engine.Kernel.World; + +/// +/// The entity type: identity, hierarchy, and a list of components. See +/// docs/kernel-contract.md §2. +/// +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() O(matches), not O(all). + + public IReadOnlyList Children => throw new NotImplementedException(); + + public T? GetComponent() where T : Component + => throw new NotImplementedException(); + + public T AddComponent() where T : Component, new() + => throw new NotImplementedException(); + + public void RemoveComponent() where T : Component + => throw new NotImplementedException(); +} diff --git a/src/Engine.Kernel/World/IWorld.cs b/src/Engine.Kernel/World/IWorld.cs new file mode 100644 index 0000000..08b17c5 --- /dev/null +++ b/src/Engine.Kernel/World/IWorld.cs @@ -0,0 +1,19 @@ +namespace Engine.Kernel.World; + +/// +/// Kernel-owned storage for every 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. +/// +public interface IWorld +{ + GameObject CreateGameObject(string name); + + void Destroy(GameObject go); + + /// Type-indexed lookup — O(matches), not O(all). See §2. + IEnumerable Query() where T : Component; + + // TODO(§5): Snapshot()/Restore() for Play mode — a deep clone of the + // GameObject graph, taken on EnterPlay and discarded on ExitPlay. +} diff --git a/src/Engine.Kernel/World/Transform.cs b/src/Engine.Kernel/World/Transform.cs new file mode 100644 index 0000000..af8af1b --- /dev/null +++ b/src/Engine.Kernel/World/Transform.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Engine.Kernel.World; + +/// +/// Embedded directly on rather than modeled as a +/// subclass, because nearly every system touches it +/// every frame — see the kernel scope table in docs/kernel-contract.md §2. +/// +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(); +} diff --git a/tests/Engine.ConformanceHarness/AlcUnloadTests.cs b/tests/Engine.ConformanceHarness/AlcUnloadTests.cs new file mode 100644 index 0000000..33d1fca --- /dev/null +++ b/tests/Engine.ConformanceHarness/AlcUnloadTests.cs @@ -0,0 +1,24 @@ +namespace Engine.ConformanceHarness; + +/// +/// The test docs/kernel-contract.md §4 calls the one thing that keeps this +/// architecture from slowly degrading: load and unload a plugin 200 times, +/// and after every cycle verify the ALC actually collected. Runs against +/// Sandbox.Echo — see the ReferenceOutputAssembly="false" note in this +/// project's .csproj for why that reference doesn't link its types in. +/// +public class AlcUnloadTests +{ + // TODO(M0): once PluginHost exists — + // for (int i = 0; i < 200; i++) { + // var handle = host.Load("sandbox.echo"); + // var weakAlc = host.Unload(handle); + // GC.Collect(); GC.WaitForPendingFinalizers(); + // Assert.False(weakAlc.IsAlive, $"ALC survived cycle {i}"); + // } + // and assert working-set memory hasn't grown beyond noise. + [Fact(Skip = "PluginHost has no implementation yet — see M0 in docs/kernel-contract.md §8.")] + public void Plugin_Survives_200_Load_Unload_Cycles() + { + } +} diff --git a/tests/Engine.ConformanceHarness/Engine.ConformanceHarness.csproj b/tests/Engine.ConformanceHarness/Engine.ConformanceHarness.csproj new file mode 100644 index 0000000..938b1e7 --- /dev/null +++ b/tests/Engine.ConformanceHarness/Engine.ConformanceHarness.csproj @@ -0,0 +1,29 @@ + + + + false + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Engine.Kernel.Tests/Engine.Kernel.Tests.csproj b/tests/Engine.Kernel.Tests/Engine.Kernel.Tests.csproj new file mode 100644 index 0000000..b3078b1 --- /dev/null +++ b/tests/Engine.Kernel.Tests/Engine.Kernel.Tests.csproj @@ -0,0 +1,22 @@ + + + + false + + + + + + + + + + + + + + + + + + diff --git a/tests/Engine.Kernel.Tests/WorldTests.cs b/tests/Engine.Kernel.Tests/WorldTests.cs new file mode 100644 index 0000000..f452e4f --- /dev/null +++ b/tests/Engine.Kernel.Tests/WorldTests.cs @@ -0,0 +1,14 @@ +namespace Engine.Kernel.Tests; + +public class WorldTests +{ + // TODO(M0): once IWorld has a concrete implementation — + // - GameObject hierarchy (parent/children) behaves correctly + // - Query() returns exactly the GameObjects holding a T, and is + // O(matches) — see the World row in docs/kernel-contract.md §2 + // - AddComponent/RemoveComponent keep the type index consistent + [Fact(Skip = "World has no implementation yet — see M0 in docs/kernel-contract.md §8.")] + public void Placeholder() + { + } +}