add an Open Game Scenes menu, and fix the Editor-lock check

The scenes were never destroyed. Pressing Play with only Bootstrap open means
GameBootstrap loads Systems and the level at runtime, and Unity discards
runtime-loaded scenes when Play ends, restoring whatever the Editor had open —
Bootstrap alone. The Hierarchy comes back nearly empty. Every scene file was
byte-identical to HEAD throughout, still text.

Tools > Nightclub Arcadia > Open Game Scenes opens all three with the level
active, which is the authoring setup. With them already open, GameBootstrap's
LoadIfNeeded skips them and they survive Play. GameSceneWorkflow also captures
the scene setup before Play and restores it afterwards as a safety net, skipping
the restore when Unity already got it right and ignoring scenes deleted meanwhile
— an exception in that callback would leave the Editor in a worse state than the
problem it fixes. Toggleable under the same menu.

Also fixes `make lock`, which had a self-matching bug since Phase 1: `pgrep -f`
on the full binary path matched the very shell running the check, because the
pattern appears in its own command line. It reported the Editor as running when
nothing was, and would have blocked every make test/build once anything else
matched. Now `pgrep -x Unity`, which matches the process name and cannot match
the shell.

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

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 21:35:36 +02:00
co-authored by Claude Opus 5
parent 4f48d258b5
commit 2a20bd68fe
4 changed files with 156 additions and 1 deletions
+10
View File
@@ -233,6 +233,16 @@ Assets/Scenes/
`Bootstrap` must stay index 0 in the build settings. The level is made the active scene after `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. load, so new objects and lighting land there rather than in Bootstrap.
**Working in the Editor.** `Tools ▸ Nightclub Arcadia ▸ Open Game Scenes` opens all three at
once, with the level active. Do that before pressing Play.
If you press Play with only `Bootstrap` open, `GameBootstrap` loads the other two *at runtime*,
and Unity discards runtime-loaded scenes when Play ends — the Hierarchy comes back with Bootstrap
alone and it looks as though the scenes were destroyed. **Nothing is written to disk**; it is the
Hierarchy, not the files. With all three already open, `GameBootstrap` skips loading them
(`LoadIfNeeded`) and they are still there afterwards. `GameSceneWorkflow` also captures and
restores the scene setup around Play as a safety net; toggle it under the same menu.
``` ```
writing/ writing/
├── README.md session entry point ├── README.md session entry point
+6 -1
View File
@@ -52,7 +52,12 @@ build: lock ## Build a macOS player into build/
-o $(CURDIR)/build/NightclubArcadia.app -o $(CURDIR)/build/NightclubArcadia.app
lock: ## Fail if the Unity Editor has the project open lock: ## Fail if the Unity Editor has the project open
@if pgrep -fl "Unity.app/Contents/MacOS/Unity" | grep -v AssetImportWorker | grep -q .; then \ # pgrep -x matches the process NAME only. The previous `pgrep -f` on the full
# binary path matched this very shell, because the pattern appears in its own
# command line — the check reported the Editor running when nothing was.
# AssetImportWorkers share the name but only exist alongside an Editor, so
# matching them is correct rather than a false positive.
@if pgrep -x Unity > /dev/null 2>&1; then \
echo "The Unity Editor is running — close it first (only one instance per project)."; \ echo "The Unity Editor is running — close it first (only one instance per project)."; \
exit 1; \ exit 1; \
fi fi
@@ -0,0 +1,129 @@
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace NightclubArcadia.EditorTools
{
/// <summary>
/// Makes the split-scene layout usable in the Editor.
///
/// The game runs from Bootstrap, which loads Systems and a level additively AT
/// RUNTIME. Unity discards runtime-loaded scenes when Play ends and restores
/// whatever the Editor had open, which for someone who pressed Play in Bootstrap is
/// Bootstrap alone. The Hierarchy comes back nearly empty and it looks as though the
/// scenes were destroyed. They are not — nothing is written to disk — but it is an
/// unpleasant way to work.
///
/// Two fixes, both opt-outable:
/// - Open Game Scenes puts all three in the Hierarchy for authoring.
/// - The Editor's scene setup is captured before Play and restored after, so what
/// you had open is what you get back.
///
/// GameBootstrap skips any scene that is already loaded, so having all three open
/// and pressing Play does the right thing rather than loading anything twice.
/// </summary>
[InitializeOnLoad]
public static class GameSceneWorkflow
{
const string RestorePrefKey = "NightclubArcadia.RestoreSceneSetup";
const string RestoreMenu = "Tools/Nightclub Arcadia/Restore Scene Setup After Play";
static SceneSetup[] setupBeforePlay;
static GameSceneWorkflow()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
[MenuItem("Tools/Nightclub Arcadia/Open Game Scenes")]
public static void OpenGameScenes()
{
// The prompt cannot be shown in batch mode, where it returns false and would
// make this menu a silent no-op.
if (!Application.isBatchMode && !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
return;
}
foreach (var path in new[] { ScenePaths.Bootstrap, ScenePaths.Systems, ScenePaths.WorkingLevel })
{
if (!File.Exists(path))
{
Debug.LogError($"[Scenes] Missing: {path}");
return;
}
}
EditorSceneManager.OpenScene(ScenePaths.Bootstrap, OpenSceneMode.Single);
EditorSceneManager.OpenScene(ScenePaths.Systems, OpenSceneMode.Additive);
var level = EditorSceneManager.OpenScene(ScenePaths.WorkingLevel, OpenSceneMode.Additive);
// The level owns the lighting and is where new objects should land.
EditorSceneManager.SetActiveScene(level);
Debug.Log("[Scenes] Bootstrap + Systems + SC101_ConferenceHall open. Level is active.");
}
[MenuItem(RestoreMenu)]
static void ToggleRestore()
{
var on = !EditorPrefs.GetBool(RestorePrefKey, true);
EditorPrefs.SetBool(RestorePrefKey, on);
Debug.Log($"[Scenes] Restore scene setup after Play: {(on ? "on" : "off")}");
}
[MenuItem(RestoreMenu, true)]
static bool ToggleRestoreValidate()
{
Menu.SetChecked(RestoreMenu, EditorPrefs.GetBool(RestorePrefKey, true));
return true;
}
static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (!EditorPrefs.GetBool(RestorePrefKey, true))
{
return;
}
if (state == PlayModeStateChange.ExitingEditMode)
{
setupBeforePlay = EditorSceneManager.GetSceneManagerSetup();
return;
}
if (state != PlayModeStateChange.EnteredEditMode || setupBeforePlay == null)
{
return;
}
var setup = setupBeforePlay;
setupBeforePlay = null;
// A scene deleted or renamed while Play was running would throw here, and
// an exception in this callback leaves the Editor in a confusing state.
var usable = setup.Where(s => File.Exists(s.path)).ToArray();
if (usable.Length == 0)
{
return;
}
var current = EditorSceneManager.GetSceneManagerSetup();
if (current.Length == usable.Length &&
current.Select(s => s.path).SequenceEqual(usable.Select(s => s.path)))
{
return; // already what we wanted; do not disturb it
}
try
{
EditorSceneManager.RestoreSceneManagerSetup(usable);
}
catch (System.Exception e)
{
Debug.LogWarning($"[Scenes] Could not restore the pre-Play scene setup: {e.Message}");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e05a1f05ef74ff7a53f6e24ab37e51e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: