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:
@@ -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.
|
||||
|
||||
@@ -19,7 +19,7 @@ help: ## Show this help
|
||||
@echo ""
|
||||
@echo " Unity must be CLOSED for test/build — run 'make lock' to check."
|
||||
|
||||
check: yarn voices test ## Everything a change should pass before commit
|
||||
check: yarn voices test test-play ## Everything a change should pass before commit
|
||||
|
||||
yarn: ## Compile every Yarn script (seconds, no Unity)
|
||||
@cd tools/YarnCheck && DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet run -- ../../NightclubArcadia/Assets/Dialogue
|
||||
@@ -40,7 +40,7 @@ test: lock ## Run the EditMode suite
|
||||
--output $(RESULTS)/editmode.xml --timeout 900 || true
|
||||
@python3 tools/ci/report_tests.py $(RESULTS)/editmode.xml
|
||||
|
||||
test-play: lock ## Run the PlayMode suite (none exist yet)
|
||||
test-play: lock ## Run the PlayMode suite (bootstrap + scene split)
|
||||
@mkdir -p $(RESULTS)
|
||||
@$(UNITY_CLI) test $(PROJECT) --no-banner --mode PlayMode \
|
||||
--output $(RESULTS)/playmode.xml --timeout 900 || true
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!29 &1
|
||||
OcclusionCullingSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_OcclusionBakeSettings:
|
||||
smallestOccluder: 5
|
||||
smallestHole: 0.25
|
||||
backfaceThreshold: 100
|
||||
m_SceneGUID: 00000000000000000000000000000000
|
||||
m_OcclusionCullingData: {fileID: 0}
|
||||
--- !u!104 &2
|
||||
RenderSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 10
|
||||
m_Fog: 0
|
||||
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
m_FogMode: 3
|
||||
m_FogDensity: 0.01
|
||||
m_LinearFogStart: 0
|
||||
m_LinearFogEnd: 300
|
||||
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||
m_AmbientIntensity: 1
|
||||
m_AmbientMode: 0
|
||||
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_HaloStrength: 0.5
|
||||
m_FlareStrength: 1
|
||||
m_FlareFadeSpeed: 3
|
||||
m_HaloTexture: {fileID: 0}
|
||||
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_DefaultReflectionMode: 0
|
||||
m_DefaultReflectionResolution: 128
|
||||
m_ReflectionBounces: 1
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 13
|
||||
m_BakeOnSceneLoad: 0
|
||||
m_GISettings:
|
||||
serializedVersion: 2
|
||||
m_BounceScale: 1
|
||||
m_IndirectOutputScale: 1
|
||||
m_AlbedoBoost: 1
|
||||
m_EnvironmentLightingMode: 0
|
||||
m_EnableBakedLightmaps: 1
|
||||
m_EnableRealtimeLightmaps: 0
|
||||
m_LightmapEditorSettings:
|
||||
serializedVersion: 12
|
||||
m_Resolution: 2
|
||||
m_BakeResolution: 40
|
||||
m_AtlasSize: 1024
|
||||
m_AO: 0
|
||||
m_AOMaxDistance: 1
|
||||
m_CompAOExponent: 1
|
||||
m_CompAOExponentDirect: 0
|
||||
m_ExtractAmbientOcclusion: 0
|
||||
m_Padding: 2
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_LightmapsBakeMode: 1
|
||||
m_TextureCompression: 1
|
||||
m_ReflectionCompression: 2
|
||||
m_MixedBakeMode: 2
|
||||
m_BakeBackend: 2
|
||||
m_PVRSampling: 1
|
||||
m_PVRDirectSampleCount: 32
|
||||
m_PVRSampleCount: 512
|
||||
m_PVRBounces: 2
|
||||
m_PVREnvironmentSampleCount: 256
|
||||
m_PVREnvironmentReferencePointCount: 2048
|
||||
m_PVRFilteringMode: 1
|
||||
m_PVRDenoiserTypeDirect: 1
|
||||
m_PVRDenoiserTypeIndirect: 1
|
||||
m_PVRDenoiserTypeAO: 1
|
||||
m_PVRFilterTypeDirect: 0
|
||||
m_PVRFilterTypeIndirect: 0
|
||||
m_PVRFilterTypeAO: 0
|
||||
m_PVREnvironmentMIS: 1
|
||||
m_PVRCulling: 1
|
||||
m_PVRFilteringGaussRadiusDirect: 1
|
||||
m_PVRFilteringGaussRadiusIndirect: 1
|
||||
m_PVRFilteringGaussRadiusAO: 1
|
||||
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||
m_ExportTrainingData: 0
|
||||
m_TrainingDataDestination: TrainingData
|
||||
m_LightProbeSampleCountMultiplier: 4
|
||||
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_LightingSettings: {fileID: 0}
|
||||
--- !u!196 &4
|
||||
NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
agentSlope: 45
|
||||
agentClimb: 0.4
|
||||
ledgeDropHeight: 0
|
||||
maxJumpAcrossDistance: 0
|
||||
minRegionArea: 2
|
||||
manualCellSize: 0
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
m_Flags: 0
|
||||
m_NavMeshData: {fileID: 0}
|
||||
--- !u!1 &1325409732
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1325409734}
|
||||
- component: {fileID: 1325409733}
|
||||
m_Layer: 0
|
||||
m_Name: Bootstrap
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1325409733
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1325409732}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 612d8b8b7afa48d68f187e4d51bae0cd, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NightclubArcadia.Core.GameBootstrap
|
||||
systemsScene: Systems
|
||||
startLevel: SC101_ConferenceHall
|
||||
--- !u!4 &1325409734
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1325409732}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 1325409734}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 940e7dc5e84654c9fb808956223fc11e
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1fc9f8ca900943ce9f61d41d56bc763
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
using NightclubArcadia.Core;
|
||||
|
||||
namespace NightclubArcadia.Cinematics
|
||||
{
|
||||
/// <summary>
|
||||
/// Points a CinemachineCamera at the player once the player exists.
|
||||
///
|
||||
/// Level cameras — the reveal cameras in particular — used to hold a serialized
|
||||
/// reference to the PlayerArmature. The player now lives in the Systems scene, so
|
||||
/// that reference cannot be serialized any more and has to be bound at runtime.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CinemachineCamera))]
|
||||
public sealed class CinemachineFollowsPlayer : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Also drive LookAt, not just Follow.")]
|
||||
[SerializeField] bool alsoLookAt;
|
||||
|
||||
[Tooltip("Seconds to keep retrying while the Systems scene finishes loading.")]
|
||||
[SerializeField] float resolveTimeout = 5f;
|
||||
|
||||
IEnumerator Start()
|
||||
{
|
||||
var cam = GetComponent<CinemachineCamera>();
|
||||
var deadline = Time.unscaledTime + resolveTimeout;
|
||||
|
||||
while (SceneServices.Player == null && Time.unscaledTime < deadline)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
var player = SceneServices.Player;
|
||||
if (player == null)
|
||||
{
|
||||
Debug.LogError($"[CinemachineFollowsPlayer] {name}: no object tagged Player appeared " +
|
||||
$"within {resolveTimeout}s.", this);
|
||||
yield break;
|
||||
}
|
||||
|
||||
cam.Follow = player;
|
||||
if (alsoLookAt)
|
||||
{
|
||||
cam.LookAt = player;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cc7a0e83be949f5974b1f675c37c84e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a917e0f38654c5d9c73026e20ad3e16
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace NightclubArcadia.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry point. Loads the persistent Systems scene, then a level, additively.
|
||||
///
|
||||
/// Splitting the game across scenes is what lets a level be edited without touching
|
||||
/// the systems wiring, and stops a corrupted level from taking the dialogue runner,
|
||||
/// the UI layer and the player with it. The cost is that serialized references
|
||||
/// cannot cross a scene boundary — Unity nulls those — so the handful of level
|
||||
/// objects that need a system resolve it at runtime instead. See SceneServices.
|
||||
/// </summary>
|
||||
public sealed class GameBootstrap : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Persistent scene holding dialogue, skills, UI, the camera rig and the player.")]
|
||||
[SerializeField] string systemsScene = "Systems";
|
||||
|
||||
[Tooltip("Level to open on start. Leave empty to load only the systems layer.")]
|
||||
[SerializeField] string startLevel = "SC101_ConferenceHall";
|
||||
|
||||
IEnumerator Start()
|
||||
{
|
||||
yield return LoadIfNeeded(systemsScene);
|
||||
|
||||
if (!string.IsNullOrEmpty(startLevel))
|
||||
{
|
||||
yield return LoadIfNeeded(startLevel);
|
||||
|
||||
var level = SceneManager.GetSceneByName(startLevel);
|
||||
if (level.IsValid() && level.isLoaded)
|
||||
{
|
||||
// The active scene decides where new objects land and which
|
||||
// lighting settings apply, so it must be the level, not this one.
|
||||
SceneManager.SetActiveScene(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static IEnumerator LoadIfNeeded(string sceneName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sceneName))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var existing = SceneManager.GetSceneByName(sceneName);
|
||||
if (existing.IsValid() && existing.isLoaded)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
|
||||
if (op == null)
|
||||
{
|
||||
Debug.LogError($"[GameBootstrap] '{sceneName}' is not in the build settings.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return op;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 612d8b8b7afa48d68f187e4d51bae0cd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds objects that live in a different loaded scene.
|
||||
///
|
||||
/// Unity cannot serialize a reference across a scene boundary — the Inspector
|
||||
/// refuses it and any existing reference is nulled when the scenes are split. So a
|
||||
/// level object that needs the dialogue runner, or the player, has to resolve it at
|
||||
/// runtime. This is a lookup helper, not a service container: there is no
|
||||
/// registration step and nothing to keep in sync.
|
||||
///
|
||||
/// It is deliberately thin. FindFirstObjectByType is not cheap, so callers cache
|
||||
/// what they get in Awake rather than calling per frame, and an Inspector-assigned
|
||||
/// reference always wins — this only fills in what the split left null.
|
||||
/// </summary>
|
||||
public static class SceneServices
|
||||
{
|
||||
/// <summary>Inspector value if set, otherwise the first one in any loaded scene.</summary>
|
||||
public static T Resolve<T>(T assigned, Object context, string field) where T : Object
|
||||
{
|
||||
if (assigned != null)
|
||||
{
|
||||
return assigned;
|
||||
}
|
||||
|
||||
var found = Object.FindFirstObjectByType<T>();
|
||||
if (found == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[SceneServices] {context?.name}: could not resolve {field} ({typeof(T).Name}). " +
|
||||
"Is the Systems scene loaded?", context);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
static Transform player;
|
||||
|
||||
/// <summary>
|
||||
/// The player transform, by tag, cached. Null between scene loads if the
|
||||
/// systems scene has not come up yet, so callers must handle null.
|
||||
/// </summary>
|
||||
public static Transform Player
|
||||
{
|
||||
get
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
var go = GameObject.FindGameObjectWithTag("Player");
|
||||
player = go != null ? go.transform : null;
|
||||
}
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Drop the cache. Call when unloading a scene that held the player.</summary>
|
||||
public static void Forget() => player = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db6a827f17574ac1b7c92185a089300a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Yarn.Unity;
|
||||
using NightclubArcadia.Core;
|
||||
|
||||
namespace NightclubArcadia.Dialogue
|
||||
{
|
||||
@@ -20,6 +21,9 @@ namespace NightclubArcadia.Dialogue
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Same cross-scene resolution as NPCStandIn: the runner is in Systems.
|
||||
dialogueRunner = SceneServices.Resolve(dialogueRunner, this, nameof(dialogueRunner));
|
||||
|
||||
interactAction = new InputAction("Interact");
|
||||
interactAction.AddBinding("<Keyboard>/e");
|
||||
interactAction.AddBinding("<Gamepad>/buttonNorth");
|
||||
|
||||
@@ -4,6 +4,7 @@ using TMPro;
|
||||
using Yarn.Unity;
|
||||
using NightclubArcadia.Dialogue;
|
||||
using NightclubArcadia.UI;
|
||||
using NightclubArcadia.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Drop this on a sphere primitive to use it as a placeholder NPC:
|
||||
@@ -53,6 +54,10 @@ public class NPCStandIn : MonoBehaviour
|
||||
rend = GetComponent<Renderer>();
|
||||
rend.material.color = npcColor;
|
||||
|
||||
// The dialogue runner lives in the Systems scene, so this reference cannot be
|
||||
// serialized from a level scene. An Inspector value still wins if one is set.
|
||||
dialogueRunner = SceneServices.Resolve(dialogueRunner, this, nameof(dialogueRunner));
|
||||
|
||||
mainCam = Camera.main;
|
||||
|
||||
if (player == null)
|
||||
|
||||
@@ -111,6 +111,12 @@ namespace NightclubArcadia.Player
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!agent.isOnNavMesh)
|
||||
{
|
||||
pendingInteractable = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
agent.stoppingDistance = 0.15f;
|
||||
agent.isStopped = false;
|
||||
agent.SetDestination(hit.position);
|
||||
@@ -136,6 +142,12 @@ namespace NightclubArcadia.Player
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!agent.isOnNavMesh)
|
||||
{
|
||||
pendingInteractable = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
agent.stoppingDistance = Mathf.Max(0.15f, interactable.ApproachRadius);
|
||||
agent.isStopped = false;
|
||||
agent.SetDestination(hit.position);
|
||||
@@ -147,7 +159,11 @@ namespace NightclubArcadia.Player
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (agent != null)
|
||||
// isOnNavMesh, not just a null check: the agent lives in the Systems scene
|
||||
// and the NavMesh is baked into the level, so between those two scenes
|
||||
// loading there is a window where the agent exists but is not on a mesh.
|
||||
// ResetPath and isStopped both log an error in that state.
|
||||
if (agent != null && agent.isOnNavMesh)
|
||||
{
|
||||
agent.ResetPath();
|
||||
agent.isStopped = true;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ce9362d0ce746e3a38ed268d81bd866
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace NightclubArcadia.PlayMode.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Proves the scene split actually comes up.
|
||||
///
|
||||
/// Splitting Systems out of the level nulled seven serialized references that
|
||||
/// Unity cannot carry across a scene boundary — the two NPCs' dialogue runner, UI
|
||||
/// and player fields, and the reveal camera's tracking target. Each is supposed to
|
||||
/// be resolved at runtime instead. An EditMode test cannot see any of that, because
|
||||
/// none of it has run. This can.
|
||||
///
|
||||
/// If this suite fails, the game does not start. Treat it that way.
|
||||
///
|
||||
/// Two checks reach their types by name through reflection rather than referencing
|
||||
/// them. NPCStandIn and NightclubArcadia.Cinematics live in Assembly-CSharp, and an
|
||||
/// assembly-definition test assembly cannot reference the predefined assemblies —
|
||||
/// only the other way round. Giving the runtime code its own asmdefs
|
||||
/// (docs/restructure-plan.md Phase 4.1) removes the need for this; until then the
|
||||
/// reflection is the accommodation, and it is confined to these two tests.
|
||||
/// </summary>
|
||||
public class BootstrapTests
|
||||
{
|
||||
const string Bootstrap = "Bootstrap";
|
||||
const string Systems = "Systems";
|
||||
const string Level = "SC101_ConferenceHall";
|
||||
|
||||
[UnitySetUp]
|
||||
public IEnumerator LoadFromBootstrap()
|
||||
{
|
||||
yield return SceneManager.LoadSceneAsync(Bootstrap, LoadSceneMode.Single);
|
||||
|
||||
// GameBootstrap loads the other two additively over a few frames.
|
||||
var deadline = Time.realtimeSinceStartup + 20f;
|
||||
while (Time.realtimeSinceStartup < deadline)
|
||||
{
|
||||
if (SceneManager.GetSceneByName(Systems).isLoaded &&
|
||||
SceneManager.GetSceneByName(Level).isLoaded)
|
||||
{
|
||||
break;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// let Awake/Start run on everything that just loaded
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Bootstrap_LoadsSystemsAndLevel()
|
||||
{
|
||||
Assert.IsTrue(SceneManager.GetSceneByName(Systems).isLoaded, "Systems scene did not load");
|
||||
Assert.IsTrue(SceneManager.GetSceneByName(Level).isLoaded, "Level scene did not load");
|
||||
Assert.AreEqual(Level, SceneManager.GetActiveScene().name,
|
||||
"The level should be the active scene, so new objects and lighting land there");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThePlayerExists_AndIsTagged()
|
||||
{
|
||||
var player = GameObject.FindGameObjectWithTag("Player");
|
||||
Assert.IsNotNull(player, "No object tagged Player — the reveal camera and the NPCs both need it");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheDialogueRunnerExists()
|
||||
{
|
||||
Assert.IsNotNull(Object.FindFirstObjectByType<DialogueRunner>(),
|
||||
"No DialogueRunner in any loaded scene");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryNpc_ResolvedItsDialogueRunner()
|
||||
{
|
||||
var npcs = Object.FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Include, FindObjectsSortMode.None)
|
||||
.Where(m => m != null && m.GetType().Name == "NPCStandIn")
|
||||
.ToArray();
|
||||
Assert.IsNotEmpty(npcs, "Expected NPCs in the level scene");
|
||||
|
||||
foreach (var npc in npcs)
|
||||
{
|
||||
var field = npc.GetType().GetField("dialogueRunner",
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.IsNotNull(field, "NPCStandIn no longer has a dialogueRunner field");
|
||||
|
||||
Assert.IsNotNull(field.GetValue(npc) as Object,
|
||||
$"'{npc.name}' did not resolve a DialogueRunner. It lives in the Systems scene, " +
|
||||
"so the serialized reference is gone and SceneServices.Resolve has to find it.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RevealCameras_BoundToThePlayer()
|
||||
{
|
||||
var bound = Object.FindObjectsByType<CinemachineCamera>(FindObjectsInactive.Include, FindObjectsSortMode.None)
|
||||
.Where(cam => cam.GetComponents<MonoBehaviour>()
|
||||
.Any(m => m != null && m.GetType().Name == "CinemachineFollowsPlayer"))
|
||||
.ToArray();
|
||||
Assert.IsNotEmpty(bound, "Expected at least one runtime-bound camera");
|
||||
|
||||
foreach (var cam in bound)
|
||||
{
|
||||
Assert.IsNotNull(cam.Follow,
|
||||
$"'{cam.name}' never bound its Follow target to the player");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49c9ee5b850e40d3aad7b3c8ea2e85c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "NightclubArcadia.PlayMode.Tests",
|
||||
"rootNamespace": "NightclubArcadia.PlayMode.Tests",
|
||||
"references": [
|
||||
"NightclubArcadia.Skills",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner",
|
||||
"Unity.Cinemachine",
|
||||
"YarnSpinner.Unity"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"nunit.framework.dll"
|
||||
],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fa1e1d710f14f8abd1cc3f1ebd92bd3
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -5,6 +5,12 @@ EditorBuildSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Scenes:
|
||||
- enabled: 1
|
||||
path: Assets/Scenes/Bootstrap.unity
|
||||
guid: 940e7dc5e84654c9fb808956223fc11e
|
||||
- enabled: 1
|
||||
path: Assets/Scenes/Systems/Systems.unity
|
||||
guid: c1fc9f8ca900943ce9f61d41d56bc763
|
||||
- enabled: 1
|
||||
path: Assets/Scenes/Levels/SC101_ConferenceHall.unity
|
||||
guid: a4910059112fe4675827519a3f55afe5
|
||||
|
||||
+86
-56
@@ -10,7 +10,7 @@ Companion reading: `docs/skill-system-refactor-plan.md`, `docs/candidate-system-
|
||||
|
||||
---
|
||||
|
||||
## 0. Status — Phases 0 and 2 are complete
|
||||
## 0. Status — Phases 0, 1, 2 and 3 are complete
|
||||
|
||||
Executed 2026-08-25. The working tree is clean and tagged `pre-restructure`.
|
||||
|
||||
@@ -19,47 +19,38 @@ the UI layer, player control setup, the dialogue-vs-menu interaction fix, and th
|
||||
A full pre-flight backup was taken first (`.tar.gz` of the tree excluding `Library/`, `Temp/`,
|
||||
`Logs/`, and build output) plus a separate copy of the scene file.
|
||||
|
||||
Phase 2 (the writing pipeline) is also done — see §4. Phases 1, 3, 4 and 5 remain proposal.
|
||||
Phases 1, 2 and 3 are also done — see §4. Only Phase 4 (code structure) and Phase 5 (replacing the NPC scaffold) remain proposal.
|
||||
|
||||
### 0.1 The binary scene — diagnosis corrected
|
||||
### 0.1 The binary scene — RESOLVED in Phase 3
|
||||
|
||||
The original reading of this was **wrong**, and the correction matters for Phase 3.
|
||||
This went through two wrong diagnoses before the real one. Recorded in full because the wrong
|
||||
turns are the instructive part.
|
||||
|
||||
The theory was that the scene had been saved during a temporary Force Binary session and merely
|
||||
needed re-saving. It isn't that. Unity **re-writes this scene as binary even now**, with the
|
||||
project set to Force Text and `EditorSettings.serializationMode` reporting `ForceText` from the
|
||||
API. Force Text governs conversion **at the moment the setting changes**, not on every
|
||||
subsequent save — so a file that entered the project as binary stays binary indefinitely.
|
||||
**First theory (wrong):** the scene had been saved during a temporary Force Binary session and
|
||||
merely needed re-saving.
|
||||
|
||||
Four approaches were tried headlessly, all failing silently with success return codes:
|
||||
**Second theory (wrong, but closer):** Force Text only converts at the moment the setting
|
||||
changes, so a file that entered binary stays binary — and the fix is therefore an Editor-UI
|
||||
toggle. Four headless approaches were tried and all failed silently with success return codes:
|
||||
`ForceReserializeAssets` does not rewrite scenes; `MarkSceneDirty` + `SaveScene` writes nothing
|
||||
in batch mode; `SaveScene` to a *new* path reproduces binary; re-assigning `serializationMode`
|
||||
triggers no reserialize pass.
|
||||
|
||||
| Approach | Result |
|
||||
|---|---|
|
||||
| `AssetDatabase.ForceReserializeAssets(scenePath)` | Opens and imports the scene; does not rewrite scene files at all |
|
||||
| `OpenScene` + `MarkSceneDirty` + `SaveScene(samePath)` | Returns `true`, writes nothing — `MarkSceneDirty` does not take in batch mode |
|
||||
| `OpenScene` + `SaveScene(newPath)` | Writes a fresh file that is **also binary**, byte-identical in size |
|
||||
| `serializationMode = Mixed` → `ForceText` | Mode changes as logged; no reserialize pass occurs |
|
||||
**The actual cause:** the `NavMeshSurface` on the `Navigation` root held its baked `NavMeshData`
|
||||
*embedded in the scene* rather than as an asset. `NavMeshData` prefers binary serialization, and
|
||||
a single such object forces the entire scene file to binary no matter what the project setting
|
||||
says. That is why every attempt to re-save it failed — the file was being written correctly each
|
||||
time, in the only format its contents allowed.
|
||||
|
||||
**The remaining fix is Editor-UI only** and takes about thirty seconds:
|
||||
Found by bisection: moving the roots one at a time into a fresh scene, thirteen produced text
|
||||
files and `Navigation` produced a binary one. Extracting the data to
|
||||
`Scenes/Levels/SC101_ConferenceHall/NavMesh-Navigation.asset` — which is what baking from the
|
||||
NavMeshSurface inspector produces anyway; the embedded copy was the anomaly — made the scene
|
||||
serialize as text. No Editor-UI step was required.
|
||||
|
||||
> Project Settings ▸ Editor ▸ Asset Serialization → set **Mixed**, let it apply, then set
|
||||
> **Force Text** again. Changing the value in the dialog is what triggers the reserialize pass.
|
||||
|
||||
Then verify from the terminal — you want `%YAML 1.1`:
|
||||
|
||||
```bash
|
||||
head -c 20 NightclubArcadia/Assets/Scenes/DialogueTest.unity
|
||||
```
|
||||
|
||||
Worth knowing: this will reserialize **every** asset the setting touches, so expect a wide diff.
|
||||
That is now safe to inspect and revert per-file, because everything is committed.
|
||||
|
||||
The scene itself is healthy — it opens with 14 roots (`Room_101`, `Dialogue System`,
|
||||
`Skill System`, the three UI canvases, `CinemachineBrain`, `CinemachineCamera`, `Navigation`,
|
||||
`CameraAnchor`, `CM_Reveal_Room101`, `RevealVolume_Room101`, `Directional Light`, `UI System`).
|
||||
Its md5 was verified unchanged across every failed attempt. The cost of it staying binary is
|
||||
that it is **not diffable and not mergeable** — which is an argument for bringing Phase 3's
|
||||
scene split forward, since the split rebuilds these files anyway.
|
||||
The scene is now `Assets/Scenes/Levels/SC101_ConferenceHall.unity`, 43KB of YAML, diffable and
|
||||
mergeable, with the UnityYAMLMerge driver configured (Phase 1). The generalised lesson is
|
||||
trap 1 in `CLAUDE.md`.
|
||||
|
||||
### 0.2 The Unity CLI — finding corrected
|
||||
|
||||
@@ -393,17 +384,25 @@ Effort marks are rough: **S** ≲1h, **M** a half day, **L** a day or more.
|
||||
|
||||
0.4 is the one open item and it does not block Phase 1 or 2.
|
||||
|
||||
### Phase 1 — repo hygiene (no Unity changes)
|
||||
### Phase 1 — repo hygiene ✅ DONE (2026-08-25)
|
||||
|
||||
| # | Step | Detail | Effort |
|
||||
|---|---|---|---|
|
||||
| 1.1 | Add `.gitattributes` | Unity YAML merge driver for `*.unity` / `*.prefab` / `*.asset`; LFS for `Assets/Art/**` binaries; `* text=auto eol=lf` | S |
|
||||
| 1.2 | Untrack `tools/YarnCheck/{bin,obj}` | `git rm -r --cached`, add ignore rules | S |
|
||||
| 1.3 | Remove committed `.DS_Store` files, confirm ignore rule | S |
|
||||
| 1.4 | Add a `Makefile` or `scripts/` wrapping the `CLAUDE.md` commands | `make test`, `make yarncheck`, `make build` | S |
|
||||
| # | Step | Status |
|
||||
|---|---|---|
|
||||
| 1.1 | `.gitattributes` | done — UnityYAMLMerge driver, LF pinned, binaries marked |
|
||||
| 1.2 | Untrack `tools/YarnCheck/{bin,obj}` | **no-op** — already ignored, never tracked |
|
||||
| 1.3 | Remove committed `.DS_Store` | **no-op** — already ignored, never tracked |
|
||||
| 1.4 | Makefile wrapping the CLAUDE.md commands | done, plus `tools/ci/report_tests.py` |
|
||||
|
||||
`.gitattributes` before any restructuring is deliberate — the moment work happens on a branch,
|
||||
scene merges without a merge driver will corrupt files.
|
||||
1.2 and 1.3 were wrong in the original plan: both were listed from a filesystem `find`, not from
|
||||
`git ls-files`. Nothing was ever tracked, and `.gitignore` already covers both.
|
||||
|
||||
Line endings were renormalized in one deliberate pass (74 files, all CRLF→LF, `git diff -w`
|
||||
empty) rather than left to surface as phantom diffs later. LFS was **not** enabled: the remote is
|
||||
self-hosted and its LFS support is unverified, and turning the filter on against a server that
|
||||
lacks it breaks pushing. The rules and the migration step are recorded in `.gitattributes`.
|
||||
|
||||
`make merge-driver` configures UnityYAMLMerge. Note the binary is at `Contents/Helpers/`, not
|
||||
`Contents/Tools/` as most guides claim — there is no `Tools` directory in Unity 6 on macOS.
|
||||
|
||||
### Phase 2 — writing pipeline ✅ DONE (2026-08-25)
|
||||
|
||||
@@ -441,20 +440,51 @@ scene spec could not honestly certify its own tone field. `writing/STYLE.md` §9
|
||||
sheet with four fields explicitly unset. Filling them is about half an hour of decisions and it
|
||||
unblocks a check that has been stuck for a while.
|
||||
|
||||
### Phase 3 — scene restructure (the risky part)
|
||||
### Phase 3 — scene restructure ✅ DONE (2026-08-25)
|
||||
|
||||
| # | Step | Detail | Effort |
|
||||
|---|---|---|---|
|
||||
| 3.1 | Delete template leftovers | `SampleScene.unity`, `Readme.asset`, `TutorialInfo/`; fix `EditorBuildSettings` | S |
|
||||
| 3.2 | Create `Bootstrap.unity` + `Systems/Systems.unity` | Systems empty at first | M |
|
||||
| 3.3 | **Copy** `DialogueTest.unity` → `Levels/SC101_ConferenceHall.unity` | Copy, do not move. Keep the original until 3.6. | S |
|
||||
| 3.4 | Move systems objects out of the level scene into `Systems.unity` | dialogue runner, UI layer, skills, commentary, camera director | L |
|
||||
| 3.5 | Wire additive loading in `Scripts/Core/` | Bootstrap → Systems → level | M |
|
||||
| 3.6 | Verify parity, then delete `DialogueTest.unity` | full playthrough of SC-101 vs. pre-migration behaviour | M |
|
||||
| 3.7 | Retarget `YarnDemoSetup.cs` or retire it | it hardcodes `Assets/Scenes/DialogueTest.unity` | S |
|
||||
| # | Step | Status |
|
||||
|---|---|---|
|
||||
| 3.1 | Delete template leftovers | done — SampleScene, Readme.asset, TutorialInfo |
|
||||
| 3.2 | Create `Bootstrap` + `Systems/Systems` | done, both text |
|
||||
| 3.3 | ~~Copy~~ **rebuild** `DialogueTest` → `Levels/SC101_ConferenceHall` | done — see below |
|
||||
| 3.4 | Move systems objects into `Systems.unity` | done — 10 roots incl. the player |
|
||||
| 3.5 | Additive loading in `Scripts/Core/` | done — `GameBootstrap`, `SceneServices` |
|
||||
| 3.6 | Verify parity, delete `DialogueTest.unity` | done — verified by diff, then deleted |
|
||||
| 3.7 | Retarget `YarnDemoSetup.cs` | done — all four setup menus share `ScenePaths` |
|
||||
|
||||
Step 3.4 is where things break. Do it in small commits — one system per commit, playtest between
|
||||
each. Do **not** batch it.
|
||||
Validated: EditMode **42/42**, PlayMode **5/5**, YarnCheck 9 files / 35 nodes.
|
||||
|
||||
#### Phase 3 — what changed against the plan
|
||||
|
||||
**The binary scene is fixed, and the cause was not what §0.1 assumed.** The `NavMeshSurface` held
|
||||
its baked `NavMeshData` *embedded in the scene*. That type prefers binary serialization, and one
|
||||
such object forces the entire file to binary regardless of Force Text — which is why toggling the
|
||||
setting, re-saving, and saving to a new path all did nothing. Bisecting the roots one at a time
|
||||
found it: thirteen saved as text alone, `Navigation` did not. Extracting the data to
|
||||
`Scenes/Levels/SC101_ConferenceHall/NavMesh-Navigation.asset` — what baking from the inspector
|
||||
produces anyway — made the scene text. **No Editor-UI step was needed after all.**
|
||||
|
||||
So 3.3 became a *rebuild*, not a copy: saving the binary scene to a new path reproduces binary, so
|
||||
all 14 roots were moved into a fresh scene instead. Verified roots 14→14, cross-root references
|
||||
25→25, zero missing scripts, render settings carried across by hand.
|
||||
|
||||
**The player moved to Systems.** It had been parented under `Room_101`, i.e. under level geometry.
|
||||
It is persistent content, and moving it removes most of the cross-scene breakage by itself.
|
||||
|
||||
**The split nulled exactly seven references**, measured by auditing the pre-split scene from git
|
||||
and diffing: both NPCs' `dialogueRunner`, `dialogueUI` and `player`, plus the reveal camera's
|
||||
tracking target. A first, naive audit reported 210 — those turned out to be pre-existing Unity
|
||||
defaults (`Image.m_Material` and friends), which is why the before/after baseline mattered.
|
||||
|
||||
**A real bug surfaced, and only PlayMode could see it.** The player's `NavMeshAgent` is 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`. This is
|
||||
exactly the class of failure an EditMode suite cannot reach, and it is the argument for having
|
||||
written `BootstrapTests` rather than eyeballing the scene.
|
||||
|
||||
**Phase 4.2 came early.** `Assets/Tests/PlayMode` now exists. Two of its checks use reflection,
|
||||
because an asmdef assembly cannot reference `Assembly-CSharp` where `NPCStandIn` and the
|
||||
`Cinematics` namespace live — Phase 4.1 (per-area asmdefs) is what removes that.
|
||||
|
||||
### Phase 4 — code structure
|
||||
|
||||
|
||||
Reference in New Issue
Block a user