Fix real bugs from independent review (docs/review-handoff.md)
A second AI session read the whole codebase in parallel (read-only, no code changes) and left a handoff doc. Addressed the correctness findings: - PhysicsWorld.Sync's early return compared _bodies.Count to live.Count, not their contents — same size, different membership (destroy one tracked GameObject, gain one untracked-because-no-collider one; or any Restore where the scene has both a Rigidbody+collider object and a Rigidbody-without-one) skipped cleanup entirely, leaking the native Box3D body forever. Fixed by checking the actual stale set. Two new regression tests reproduce the review's own two scenarios via a new Lingua_GetBodyCount native export, asserting on the native table's own count rather than PhysicsWorld's C#-side bookkeeping. - TryCreateBody stored handle -1 (native shim refused: invalid world, or its 8192-slot body table full) as if it were real — every later GetBodyTransform on it silently teleported the GameObject to the origin with a degenerate rotation, no error anywhere. Now checked and warned once, same as the missing-collider case. - PluginHost.Load didn't roll back anything when Configure threw partway through: Schedule.Add/Events.Subscribe registrations it already made stayed forever, and its ALC was never unloaded — neither loaded (no _loaded entry) nor cleanly unloadable. Fixed with try/catch: best-effort Shutdown (the only thing that knows which services this plugin provided), RemoveAllFrom on both Schedule and EventBus, best-effort ALC unload, rethrow. New fixture plugin (sandbox.failing-configure, mirrors sandbox.echo's own real-load pattern) registers a system against a shared Ping component then throws, so FailedConfigureRollbackTests can assert the dangling system actually stops firing — an earlier version tried to prove this via AssemblyLoadContext.All instead, which passed even against the deliberately-reverted buggy code (the ALC turned out to get collected either way once its only references went out of scope); watching the dangling system is what actually distinguishes rolled-back from not, confirmed by deliberately reverting the fix and watching this specific test fail before restoring it. - Engine.Host's "r <id>" left a plugin unloaded on a failed reload with no honest indication of that, and retrying threw "not loaded" instead of ever reaching Load again. Added PluginHost.IsLoaded so the handler only calls Unload when there's something to unload, and the failure message now says the plugin is unloaded, not just "failed." - AssetService.ReloadWithRetry's `when (attempt < 4)` guard meant the 5th and final IOException fell out of the loop and propagated from a discarded fire-and-forget Task — no log, no event, nothing. Now logged. - EditorState.Selected kept pointing at a GameObject Restore had already destroyed after ExitPlay, so Inspector/gizmo would silently keep editing something no longer in the world. EditorPlugin.DrawUi now compares IsPlaying against its own previous frame (not just reacting to the Stop button) so this is caught whether Play was exited via the button or the "stop" stdin command — the same stdin-vs-real-control gap already hit once earlier this session — and re-resolves the selection by name. - README.md and kernel-contract.md's Event Bus/Time rows had fallen a milestone behind (still said "no asset system yet" and "no fixed-step accumulator yet" after both shipped). Full suite: 98 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
This commit is contained in:
@@ -53,8 +53,8 @@ nothing.
|
||||
| 02 | **Scheduler** | Frame stages, topological system ordering, parallel execution of systems with disjoint declared access, and debug-mode enforcement of that access — see §7. Structural changes (adding/removing a `GameObject` or `Component`) are queued and applied at the stage boundary, so a running system never sees a collection mutate under it. |
|
||||
| 03 | **Plugin Host** | Manifest parsing, dependency resolution, ALC loading, unloading, reload. |
|
||||
| 04 | **Service Registry** | Publishing and discovering interfaces between plugins. Control path, not the hot path. |
|
||||
| 05 | **Event Bus** | `Publish`/`Subscribe`, ownership-tracked and leak-safe the same way as the Scheduler's systems — see §7. `PluginHost` publishes `PluginLoaded`/`PluginUnloaded`; nothing publishes `GameObject` created/destroyed or asset-reloaded facts yet — the first because `GameWorld` doesn't touch the bus at all (a publish on every structural change would tax the hot path for listeners that usually don't exist), the second because there's no asset system yet. |
|
||||
| 06 | **Time & Log** | Frame clock (`DeltaTime`, `ElapsedTime`, `FrameCount`) and logging, both on `IPluginContext`. No fixed-step accumulator yet — deferred to M4, alongside the physics system it would actually drive. |
|
||||
| 05 | **Event Bus** | `Publish`/`Subscribe`, ownership-tracked and leak-safe the same way as the Scheduler's systems — see §7. `PluginHost` publishes `PluginLoaded`/`PluginUnloaded`; `engine.assets` publishes `TextureReloaded` when a watched file changes on disk (M2). Still nothing publishes `GameObject` created/destroyed facts — `GameWorld` doesn't touch the bus at all, deliberately: a publish on every structural change would tax the hot path for listeners that usually don't exist. |
|
||||
| 06 | **Time & Log** | Frame clock (`DeltaTime`, `ElapsedTime`, `FrameCount`, `FixedDeltaTime`) and logging, both on `IPluginContext`. The fixed-step accumulator (`Time.ConsumeFixedSteps`) shipped with M4, driving `Stage.FixedUpdate` — `engine.physics` is its first real consumer. |
|
||||
|
||||
`GameObject.Transform` is the one field embedded directly rather than
|
||||
modeled as a `Component` subclass — it's a plain struct holding local
|
||||
@@ -436,7 +436,7 @@ cost of changing course is still zero.
|
||||
| **M1** | **Window, input, a triangle.** Three separate plugins over Silk.NET. First real-load test of the data channel. | The triangle's color changes by editing system code, with no app restart. |
|
||||
| **M2** | **Assets and scenes.** Hot-reloading asset plugin, scene format, `World` serialization. | Swapping a texture on disk changes the picture with nothing stopped; a scene loads and saves. |
|
||||
| **M3** | **Editor as plugins — done.** Shell (`engine.editor`, ImGui over the live scene, see §5 and the new `Stage.Present`), reflection-based inspector and hierarchy, Play/Stop on snapshots, a real 3D translate gizmo driven by the actual camera. | Entering Play takes under 100 ms — the original complaint about Unity is closed. Proven twice: `WorldSnapshotTests` (kernel, 300 `GameObject`s) and a real editor run against `samples/WindowDemo` (13ms, logged by `PlayModeController`). |
|
||||
| **M4** | **One small game, end to end.** `engine.physics` over Box3D, audio, a Linux + Windows build pipeline. A 20-minute game, shipped as an executable. | The build runs on both platforms with no editor plugins in the shipped binary. |
|
||||
| **M4** | **One small game, end to end — in progress.** `engine.physics` (Box3D) and `engine.audio` (miniaudio) done, both over a narrow, scalars-only P/Invoke shim; `samples/PhysicsDemo` ties physics/audio/input/render together with `engine.editor` deliberately excluded from its `project.json`; `.github/workflows/build.yml` builds and tests on real `ubuntu-latest`/`windows-latest` runners. Not yet: the workflow has actually run on GitHub (blocked on an OAuth `workflow` scope grant), and the demo is a physics sandbox, not yet a scored 20-minute game. | The build runs on both platforms with no editor plugins in the shipped binary. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user