Commit Graph
2 Commits
Author SHA1 Message Date
EmilandClaude Sonnet 5 88e0afa46b Fix: exiting Play mode from the editor's own Stop button crashed
Real bug, caught by hand (not by any automated test): clicking Stop in
the Lingua Editor panel threw InvalidOperationException — "A system
structurally changed 'QuadRenderer' without declaring Writes<QuadRenderer>()".

Root cause: GameWorld.Restore rebuilds the world via GameObject.
AddComponent, which SystemAccessScope checks. That's fine when Restore is
called from unconstrained code — every existing WorldSnapshotTests test
calls it directly, outside any system, which is exactly why none of them
caught this. But PlayModeController.ExitPlay is called from inside
EditorPlugin's own Stage.Render system (the button click handler runs as
part of DrawUi), which correctly declares no Reads/Writes at all — it has
no compile-time knowledge of QuadRenderer or any other game's component
types. So the ambient SystemAccessScope was still active when Restore
tried to rebuild them.

SystemAccessScope's own doc comment already said "editor code... [is]
unconstrained by design" — true only when the call happened to originate
outside a system's scope, not actually true in general. Fixed with
SystemAccessScope.Suspend(), which GameWorld.Restore now wraps its entire
rebuild in: Restore is a bulk, whole-world reset regardless of who calls
it, the same category of operation Destroy already is (which never went
through the checked RemoveComponent path to begin with).

New regression test reproduces the exact shape: a system with no declared
access calling Restore, run through the real Schedule — not calling
SystemAccessScope directly, and not calling Restore outside a system
either, which is why this slipped through the first time. Passed
immediately after the fix; would have failed loudly before it.

Full suite: 87 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 17:24:14 +03:00
EmilandClaude Sonnet 5 2f5a8bfde6 M3, part one: World.Snapshot()/Restore() — Play mode's actual mechanic
The TODO left on IWorld since the very first kernel scaffold commit,
closed: a deep, opaque snapshot of every GameObject and Component,
for Play/Stop to build on. Not a new clone mechanism — backed by
SceneFormat. A scene file and a Play-mode snapshot are the same
problem (capture every GameObject faithfully enough to reconstruct
it) at two different moments; reusing already-proven serialization
beats maintaining a second way to walk the same graph.

Restore() is NOT additive the way SceneFormat.Load() is by design —
it destroys every current root first. Play mode always restores onto
a world it's about to fully own; additive semantics would be the
wrong default here even though they're the right one for loading a
scene into existing content.

Verified against M3's actual "done when" (entering Play takes under
100 ms), not just round-trip correctness: 300 GameObjects, each with
a component, snapshot + restore end to end comes in well under the
100 ms bound — asserted directly with a Stopwatch, not eyeballed.
Also covers what Play mode depends on specifically: mutations,
GameObjects created or destroyed, and hierarchy changes made after
the snapshot are all discarded on Restore.

73 tests total now (49 in Engine.Kernel.Tests, 8 in
Engine.Assets.Tests, 8 in Engine.ConformanceHarness... — wait, that's
65; the two build-time contract projects add no test counts. Actual
total per the run: 49+8+8 = 65.), all green on a clean build.

Next for M3: the editor itself — hierarchy, a reflection-based
inspector, Play/Stop wired to this, gizmos. All still ahead; this
commit is only the kernel mechanic underneath Play/Stop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 15:54:49 +03:00