Schedule gains real execution on top of the registration bookkeeping
from the PluginHost pass: RunStage(stage, world) runs every system
registered for a stage, and Reads<>/Writes<>() declarations are
enforced live via SystemAccessScope — a system touching a component it
didn't declare throws immediately, with a message naming the
violation, from GameWorld.Query<T>() and GameObject.GetComponent<T>()/
AddComponent<T>()/RemoveComponent<T>(). Outside of a running system
(editor code, tests, scene construction) nothing is enforced.
Scope cut made deliberately, not by accident: systems are grouped into
conflict-free batches by declared access (ComputeBatches, tested
directly), but batches run sequentially rather than on real threads.
Actually parallelizing them needs GameWorld's structural changes
(Create/Destroy/AddComponent/RemoveComponent) deferred to a command
buffer first — without that, two systems with disjoint *declared*
types can still race on shared storage, since AddComponent<T>() on a
GameObject mutates that object's own component list regardless of T.
Building real concurrency on top of a known thread-safety hole would
be worse than not building it yet. Noted as a TODO on RunStage.
Two real bugs found and fixed while wiring this up, not designed in
from the start:
- ISchedule.Add took `Delegate`, and a lambda passed there doesn't
reliably compile down to `Action<IWorld>` at runtime — the
compiler's natural-type inference for lambdas (as opposed to method
groups, which do work this way) can synthesize a different, private
delegate type instead, so `is Action<IWorld>` silently failed for
every lambda-registered system. Changed Add's parameter type to
Action<IWorld> directly, which sidesteps the inference question
entirely — found by ScheduleTests actually using lambdas, which
EchoPlugin's method-group-based Tick had been masking.
- AlcUnloadTests started failing intermittently ("ALC survived unload
cycle 3") once PluginSystemTests existed alongside it — xUnit
parallelizes across test classes by default, and ALC-unload tests
are sensitive to any concurrent activity in the process. Added
[CollectionBehavior(DisableTestParallelization = true)] to the
harness assembly; stable across 5+ repeated runs since.
EchoPlugin.Tick is no longer a stub — it increments every Ping.Count
in World, which two new integration tests in
Engine.ConformanceHarness/PluginSystemTests.cs exercise end to end: a
plugin loaded from a real collectible ALC registers a system, Schedule
actually invokes that cross-ALC delegate, and it correctly mutates a
component owned by the Default-ALC World. This only works because
PluginLoadContext resolves Sandbox.Echo.Contracts to the copy this
test project references directly, rather than loading a second,
type-incompatible one — the harness's new normal ProjectReference to
Sandbox.Echo.Contracts.csproj makes that a live assertion, not just an
implementation detail no test would notice breaking.
27 tests total now (21 in Engine.Kernel.Tests, 6 in
Engine.ConformanceHarness), all green on a clean build, harness
verified stable across repeated runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
60 lines
2.8 KiB
XML
60 lines
2.8 KiB
XML
<Project Sdk="Microsoft.NET.Sdk">
|
|
|
|
<PropertyGroup>
|
|
<IsPackable>false</IsPackable>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
|
<PackageReference Include="xunit" Version="2.9.2" />
|
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<Using Include="Xunit" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<ProjectReference Include="..\..\src\Engine.Kernel\Engine.Kernel.csproj" />
|
|
|
|
<!-- Normal reference, on purpose: Contracts assemblies are designed to
|
|
be shared safely — a Ping component constructed here and one
|
|
constructed inside the dynamically-loaded plugin are the exact
|
|
same runtime type, precisely because PluginHost's "already in
|
|
Default ALC?" check (see PluginLoadContext) finds this copy and
|
|
defers to it instead of loading a second one. -->
|
|
<ProjectReference Include="..\..\plugins\sandbox.echo\Sandbox.Echo.Contracts\Sandbox.Echo.Contracts.csproj" />
|
|
|
|
<!-- Build-order only: the harness must load this plugin the same way
|
|
PluginHost does at runtime, into its own collectible ALC by file
|
|
path. A normal ProjectReference would link its types straight into
|
|
the harness's Default ALC, which defeats the point of a leak test. -->
|
|
<ProjectReference Include="..\..\plugins\sandbox.echo\Sandbox.Echo\Sandbox.Echo.csproj"
|
|
ReferenceOutputAssembly="false" />
|
|
</ItemGroup>
|
|
|
|
<!-- Flatten plugin.json + both built DLLs into one directory under this
|
|
project's own output — plugins/sandbox.echo/ — matching the flat
|
|
layout PluginHost.Load(directory) expects. The ProjectReference
|
|
above guarantees these DLLs exist by the time this runs (transitive
|
|
build order also covers Sandbox.Echo.Contracts, which Sandbox.Echo
|
|
itself references). $(Configuration)/$(TargetFramework) keep this
|
|
correct in Release, not just Debug. -->
|
|
<ItemGroup>
|
|
<None Include="..\..\plugins\sandbox.echo\plugin.json">
|
|
<Link>plugins\sandbox.echo\plugin.json</Link>
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
<None Include="..\..\plugins\sandbox.echo\Sandbox.Echo.Contracts\bin\$(Configuration)\$(TargetFramework)\Sandbox.Echo.Contracts.dll">
|
|
<Link>plugins\sandbox.echo\Sandbox.Echo.Contracts.dll</Link>
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
<None Include="..\..\plugins\sandbox.echo\Sandbox.Echo\bin\$(Configuration)\$(TargetFramework)\Sandbox.Echo.dll">
|
|
<Link>plugins\sandbox.echo\Sandbox.Echo.dll</Link>
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
</ItemGroup>
|
|
|
|
</Project>
|