split the scene into Bootstrap, Systems and Level

The game now starts from Bootstrap.unity, which additively loads Systems (the
dialogue runner, skills, UI layer, camera rig and the player) and then a level
(geometry, light, navmesh, reveal cameras). The level becomes the active scene so
new objects and lighting land there.

The player moved into Systems. It had been parented under Room_101 — under level
geometry — and it is persistent content, not level content. Moving it also
removes most of the cross-scene breakage on its own.

Unity nulls any serialized reference that crosses a scene boundary. Measured
rather than guessed: the pre-split scene was pulled from git and its null
references diffed against the split result. Exactly seven were lost — both NPCs'
dialogueRunner, dialogueUI and player, plus the reveal camera's tracking target.
A first naive audit reported 210, which turned out to be pre-existing Unity
defaults like Image.m_Material; the baseline diff is what separated the two.

Each of the seven now resolves at runtime. SceneServices.Resolve fills in a null
Inspector reference by searching the loaded scenes, keeping an assigned value if
there is one; CinemachineFollowsPlayer binds the reveal camera once the player
exists. Both follow the pattern already used by CameraFramingVolume and by
NPCStandIn's player lookup, rather than introducing a new one.

Assets/Tests/PlayMode is new, and it immediately paid for itself. BootstrapTests
loads Bootstrap and asserts the whole game comes up; on its first run it caught a
real bug the split had introduced. The player's NavMeshAgent lives in Systems
while the NavMesh is baked into the level, so during load the agent exists
off-mesh and ResetPath logs an error. ClickToMoveController now guards on
agent.isOnNavMesh rather than a bare null check. No EditMode test could have seen
that, because none of it has run yet.

Two of those checks reach their types by name through reflection: NPCStandIn and
the Cinematics namespace live in Assembly-CSharp, and an asmdef test assembly
cannot reference the predefined assemblies. Per-area asmdefs remove the need.

Build settings list all three scenes with Bootstrap at index 0, which
LoadSceneAsync by name requires.

EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 20:50:25 +02:00
co-authored by Claude Opus 5
parent 1c1e50809a
commit 40b4547c71
24 changed files with 2545 additions and 1727 deletions
+79 -35
View File
@@ -47,12 +47,12 @@ AI Navigation 2.0.14, ProBuilder 6.1.2, Test Framework 1.7.0. Yarn Spinner is an
overwritten by the Editor setup scripts. Edit the JSON.
2. **Never hand-edit scene or prefab YAML unless you have read the surrounding block.**
GUID/fileID surgery in `.unity` / `.prefab` files silently detaches components.
3. **One Unity instance per project path.** Unity refuses to open a project in batch
5. **One Unity instance per project path.** Unity refuses to open a project in batch
mode while the Editor has it open — a single instance at a time. Always check before
any `-batchmode` invocation (§3.1).
4. **Prefer YarnCheck over Unity for dialogue validation.** It is seconds, not minutes,
and needs no Editor lock (§5.4).
5. **Do not commit on the user's behalf** unless asked. Unity writes to tracked files
6. **Prefer YarnCheck over Unity for dialogue validation.** It is seconds, not minutes,
and needs no Editor lock (§5.5).
7. **Do not commit on the user's behalf** unless asked. Unity writes to tracked files
(`ProjectSettings/*`, `.meta`, scenes) as a side effect of simply being open, so
`git status` noise is expected and is not always yours.
@@ -133,10 +133,11 @@ misread the check. `Temp/UnityLockfile` and `unity status` are the other two sig
Useful flags: `--filter <pattern>`, `--retries <0-10>` (reports tests that pass on retry as
flaky), `--rerun-failed`, `--shard N/M` for parallel CI, `--coverage`, `--timeout <seconds>`.
`--mode PlayMode` runs the PlayMode suite. **There are none yet** — `Assets/Tests/` is EditMode
only, so that command reports zero tests. A PlayMode suite needs a new
`Assets/Tests/PlayMode/*.asmdef` referencing `UnityEngine.TestRunner` with `includePlatforms`
left empty.
`--mode PlayMode` runs `Assets/Tests/PlayMode` — currently `BootstrapTests`, which loads
`Bootstrap.unity` and asserts the whole game comes up: both scenes loaded, the player present
and tagged, the dialogue runner found, both NPCs having resolved their runner across the scene
boundary, and the reveal camera bound to the player. **If that suite fails, the game does not
start.**
Raw-Editor equivalent, if you need a flag the CLI does not expose:
@@ -208,6 +209,20 @@ NightclubArcadia/Assets/Dialogue/
The writer-facing context sits at the repo root, outside `Assets/` so it gets no `.meta` files:
The scenes are split three ways, loaded additively from `Bootstrap`:
```
Assets/Scenes/
├── Bootstrap.unity entry point; GameBootstrap loads the rest
├── Systems/Systems.unity dialogue, skills, UI, camera rig, and the PLAYER
└── Levels/
├── SC101_ConferenceHall.unity geometry, light, navmesh, reveal cameras
└── SC101_ConferenceHall/ baked data for that scene (NavMesh asset)
```
`Bootstrap` must stay index 0 in the build settings. The level is made the active scene after
load, so new objects and lighting land there rather than in Bootstrap.
```
writing/
├── README.md session entry point
@@ -357,7 +372,17 @@ or a branch.** If a writing task requires either, that is a pipeline bug — fil
`$self_lean_*` / `$found_*` in any `.yarn` file resolves to a real id in `CandidateDatabase`.
It is a tripwire, not a proof.
### 5.2 Voice-sheet sync
### 5.2 PlayMode suite
`Assets/Tests/PlayMode/BootstrapTests.cs`. Verifies the scene split at runtime — the thing no
EditMode test can see, because none of the runtime resolution has happened yet.
Two of its checks reach their types by name through reflection. `NPCStandIn` and
`NightclubArcadia.Cinematics` live in `Assembly-CSharp`, and an asmdef test assembly cannot
reference the predefined assemblies — only the reverse. Giving the runtime code its own asmdefs
removes the need; until then the reflection is confined to those two tests.
### 5.3 Voice-sheet sync
```bash
python3 tools/writing/sync_voices.py --check
@@ -365,11 +390,11 @@ python3 tools/writing/sync_voices.py --check
Seconds, no Unity. Run it after touching `writing/voices/*.md` or `skill_bible.json` (§4.6).
### 5.3 One-click in-Editor run
### 5.4 One-click in-Editor run
`Tools ▸ Nightclub Arcadia ▸ Run Skill EditMode Tests` (`Assets/Editor/SkillSystemTestRunner.cs`).
### 5.4 YarnCheck — validate dialogue without Unity ⭐
### 5.5 YarnCheck — validate dialogue without Unity ⭐
`tools/YarnCheck` is a .NET 9 console app that compiles **and plays** the Yarn scripts using
the exact Yarn Spinner DLLs shipped in `Packages/dev.yarnspinner.unity/Runtime/DLLs`, so it
@@ -420,29 +445,48 @@ should not be — see `docs/restructure-plan.md`.
## 7. Traps
1. **`Assets/Scenes/DialogueTest.unity` is serialized as binary**, and Unity will not convert it
from a headless run. The project is set to Force Text (`m_SerializationMode: 2`) and the API
agrees — `EditorSettings.serializationMode` reads `ForceText` — but Force Text governs
conversion **at the moment the setting changes**, not on every save. A file that entered the
project as binary stays binary. Four approaches were tried and all failed silently:
`AssetDatabase.ForceReserializeAssets` does not rewrite scene files; `OpenScene` +
`MarkSceneDirty` + `SaveScene` to the same path returns `true` and writes nothing, because
`MarkSceneDirty` does not take in batch mode; `SaveScene` to a *new* path writes a fresh file
that is also binary; and re-assigning `serializationMode` (Mixed → ForceText) from script
does not trigger a reserialize pass. **The fix is Editor-UI only:** Project Settings ▸ Editor
▸ Asset Serialization, switch the mode and switch it back, which reserializes on apply.
Until then the scene is not diffable or mergeable. It is committed and intact (14 roots).
2. **`unity run` reserves `-batchmode`, `-nographics`, `-quit`, `-logFile`.** Passing any of them
1. **A scene silently turns binary if it holds an object that prefers binary serialization.**
This one cost real time. `DialogueTest.unity` was binary for weeks despite the project being
set to Force Text, because the `NavMeshSurface` held its baked `NavMeshData` *embedded in the
scene* rather than as an asset. One such object forces the whole file to binary, and no amount
of re-saving or toggling the setting fixes it — Force Text only converts at the moment the
setting changes. The fix is to extract the data to its own asset, which is what baking from
the NavMeshSurface inspector produces anyway. If a scene goes binary again, bisect: move the
roots one at a time into a fresh scene and see which one yields a binary file.
2. **Serialized references cannot cross a scene boundary.** Unity nulls them. The split cost
exactly seven: both NPCs' `dialogueRunner`, `dialogueUI` and `player`, plus the reveal
camera's tracking target. All are resolved at runtime now — `SceneServices.Resolve` for the
first, `CinemachineFollowsPlayer` for the last. Anything new that a level object needs from
Systems must follow that pattern, and `BootstrapTests` is where you prove it works.
3. **The player's NavMeshAgent outlives the NavMesh.** The player is in Systems, the NavMesh is
baked into the level, so there is a window during load where the agent exists and is not on a
mesh. `ResetPath`, `isStopped` and `SetDestination` all log errors in that state. Guard on
`agent.isOnNavMesh`, not just a null check.
4. **An asmdef assembly cannot reference `Assembly-CSharp`.** Only the reverse. Most runtime code
here has no asmdef, so a test assembly cannot see `NPCStandIn` or `NightclubArcadia.Cinematics`
directly — hence the reflection in `BootstrapTests`. Adding per-area asmdefs removes this.
5. **`unity run` reserves `-batchmode`, `-nographics`, `-quit`, `-logFile`.** Passing any of them
after `--` is a hard error, and its output does not reach stdout — read `Logs/Editor.log` (§3.4).
3. **`-quit` with `-runTests`** hangs or truncates. Omit it (§3.2).
4. **Exit code alone is not a test verdict.** Parse the results XML (§3.2).
5. **`which unity` fails** in non-interactive shells even though the CLI is installed. Use the
6. **`-quit` with `-runTests`** hangs or truncates. Omit it (§3.2).
7. **Exit code alone is not a test verdict.** Parse the results XML (§3.2), which is what
`make test` does.
8. **`which unity` fails** in non-interactive shells even though the CLI is installed. Use the
absolute path `/Users/lennart/.unity/bin/unity` (§3.0).
6. **Editor lock** — check before every headless run, filtering out `AssetImportWorker` children (§3.1).
7. **Regenerated assets** — hand edits to `Skills/Definitions/*`, `Candidates/Definitions/*`
and the two `*Database.asset` files are silently discarded (§2.1).
8. **`top_skill` ties** resolve to the first argument. Reordering arguments changes the game.
9. **No `.gitattributes`** — no Unity YAML merge driver, no LFS rule for `Assets/Art/`.
Scene and prefab merges will conflict destructively under branching.
10. **Template leftovers still present**: `Assets/Scenes/SampleScene.unity`, `Assets/Readme.asset`,
`Assets/TutorialInfo/`. `SampleScene` is in the build scene list. Not load-bearing.
9. **Editor lock** — check before every headless run, filtering out `AssetImportWorker` children.
`make test` and `make build` do this for you (§2b).
10. **Regenerated assets** — hand edits to `Skills/Definitions/*`, `Candidates/Definitions/*`
and the two `*Database.asset` files are silently discarded (§2.1).
11. **`top_skill` ties** resolve to the first argument. Reordering arguments changes the game.
12. **`Bootstrap` must stay index 0** in the build settings, and all three scenes must be listed,
or `LoadSceneAsync` by name fails at runtime with nothing but a console error.