Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/ROOM-PIPELINE-RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,15 @@ room inside the player while the build's root strip read the clean in-memory sce
byte-scans the scene file it builds and refuses on any `KitRoom_` string — if a build refuses, `git -C
/Users/m1/worldos-unity checkout -- Assets/Scenes/M1CombatV1_canonical.unity`, reopen the scene, rebuild. ⚠ In a HEADED Editor that restore raises the modal *"The open scene(s) have been modified externally — Ignore / Reload"*, which blocks the main thread (the MCP bridge heartbeat freezes and every `execute_code` times out) until it is answered. Answer **Reload** (memory == the restored file) via the cua driver: `list_windows(pid)` → the untitled ~260×320 window → `get_window_state` → click the `Reload` button. The "Load Input and Layer Presets" utility window (an animation pack's nag, re-shown on every script reload) is harmless.

### 9c. Live rooms — "the room is the scene" (Day 2-3, 2026-09-03)
A live room is the kit build of a room's walked geometry, textured with painted tileables through the `WorldOS/PainterlyRoom` shader, baked to a prefab and instantiated by the player under `LiveRoom_<id>`. The picture, the collision and the occlusion are the same meshes: the seg-registration gate on the live render reads 100 % by construction.
1. **Textures** (once per room class, ~16 CU per surface best-of-2): six seamless tileables under `Assets/painterly/textures/<room>/{floor,wall,pillar,tomb,rubble,wood}.png` (wrap Repeat). Kind → file is resolved from the GEOMETRY kind (`_propKindById`), placed names only as a fallback.
2. **Build + look**: Editor env `WORLDOS_ROOM_GEO / WORLDOS_KIT_ROOM_ID / WORLDOS_ROOM_TEXTURES` → `BuildRoomKit.BuildRoom()` → apply the room's knob set (materials are in-memory; the knob set is the record — `session-notes/.../day-2/record.knobs` for the crypt) → `Tools/WorldOS/Kit/Capture Kit Room` writes the contract frame + the flat `_score.png`.
3. **Gates before any bake**: `qa/registration_score.py check <_score.png> <geometry> --ortho <pin> --boxes <kit sidecar>` = 100 %; the blind two-anchor panel (`qa/panel_workflow.mjs`, control = the PoE2 frame, calibration ref = a shipped plate) within 0.5 of the ref. Hide any actor proxies before a score capture.
4. **Bake**: `WORLDOS_KIT_ROOM_ID=<id>` + `Tools/WorldOS/Kit/Bake Live Room Prefab` → `Assets/Resources/LiveRooms/<id>.prefab` (+ `<id>.json` = fire globals **and** the camera grade). It duplicates the QA root (never mutates it), drops `Cast`, renames every `KitRoom_*` → `LiveRoom_*`, and writes EVERY in-memory material to `LiveRooms/<id>/mat_NN_<name>.mat` — the kit builds its materials at runtime, so an unsaved one ships as a missing (magenta) material.
5. **Ship**: manifest entry `"liveRoom": "<id>"` + `"liveRoomMode": "visible"|"occluder"` (keep `plate`/`boxes`/`cameraPin`). `visible` = strategy B, the room is the picture and re-applies the baked grade. `occluder` = strategy D, the PLATE is the picture and the same prefab is re-materialled to `WorldOS/OccluderDepth` with its lights off, its floor renderers off and the sidecar box proxies skipped (one occluder per wall, never two). Then build; `qa/packaged_pins.py`; `strings level0 | grep -c KitRoom_` = 0 (the build now byte-scans `resources.assets`/`sharedassets*` too, so a dirty prefab reds the build); sandbox `walk_test --exhaustive --visual 4` + `player_cert --live` + `/debug` (`liveRoom`/`liveRoomMode`/`liveRoomRenderers`/`grade`/`fps`).
Traps: Unity demotes ForcePixel lights beyond `pixelLightCount` to vertex lights — the shader takes explicit fire globals and has no vertex-light term, so the bake captures those globals and a `PainterlyRoomLights` component re-arms them in the player; `CaptureRoom` hides everything outside the kit root; `GameObject.Find` skips inactive objects; the Beautify grade is EDITOR-ONLY scene state — the bake copies its values onto the prefab and `BuildMacOSPlayer` strips the component from the build scene copy, so the plate rooms stay ungraded; and **`BuildPipeline.BuildPlayer` raises Unity's own save-modified-scenes modal whenever the active scene is dirty, which deadlocks a remotely-driven editor** — bake first, then build from a clean scene (the build now refuses up front rather than hanging).

### 10. Player pickup — hot-load for ITERATION, local rebuild for SHIP

**★ Hot-load (empirically confirmed 2026-07-16, dwing gates):** on macOS, `StreamingAssets` is
Expand Down
2 changes: 2 additions & 0 deletions extensions/renderers/unity/plates_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"yaw": 45
},
"boxes": "boxes/crypt_kit_v1_boxes.json",
"liveRoom": "crypt",
"liveRoomMode": "occluder",
"effects": [
{
"type": "fire_medium",
Expand Down
112 changes: 111 additions & 1 deletion extensions/renderers/unity/scripts/BuildMacOSPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,25 @@ public static void EnsurePackaged()
[MenuItem("Tools/WorldOS/Build/macOS Player (Universal)")]
public static void Build()
{
// #1793 Day 3, MEASURED 2026-09-03: BuildPipeline.BuildPlayer raises Unity's OWN "Scene(s) Have Been
// Modified — Save / Don't Save / Cancel" prompt whenever the ACTIVE scene is dirty. It does that
// regardless of BuildPlayerOptions.scenes, so the temp-copy strip below does NOT avoid it. In this
// headed-but-remotely-driven editor a modal DEADLOCKS the session (BOX.md #1196): it blocks the main
// thread, which is where the MCP bridge pumps, so every subsequent call times out and the only exits
// are a human clicking Save (which writes the canonical scene — the exact thing the strip exists to
// prevent) or Don't Save (which silently discards live editor work). Refuse BEFORE the modal instead.
{
var act = EditorSceneManager.GetActiveScene();
if (act.IsValid() && act.isDirty)
FailBuild("refusing to build: the active scene '"
+ (string.IsNullOrEmpty(act.path) ? "Untitled" : act.path)
+ "' has UNSAVED changes, and BuildPlayer would raise a modal save prompt that deadlocks a "
+ "remotely-driven editor. Resolve it deliberately first: either save that scene yourself, "
+ "or bake what you need out of it (Tools/WorldOS/Kit/Bake Live Room Prefab writes the live "
+ "room to Assets/Resources, so the build no longer needs the dirty scene) and reopen the "
+ "scene clean, then rebuild.");
}

// #1436: package the runtime actor bundle + registry into StreamingAssets FIRST so the built
// player can runtime-spawn actors for any campaign (not just the baked scene's cast).
EnsurePackaged();
Expand Down Expand Up @@ -267,6 +286,14 @@ public static void Build()
finally { if (buildScenePath != SceneToBuild) AssetDatabase.DeleteAsset(buildScenePath); }
var s = report.summary;

// #1793 Day 3 (rooms-are-the-scene): the scene byte-scan above only proves the SCENE is clean. A live
// room now ships as a PREFAB under Assets/Resources (build_room_kit.BakeLiveRoomPrefab), which lands in
// resources.assets/sharedassets*, NOT in the scene — so the exact contamination class the scan exists to
// stop (a `KitRoom_*` construction inside the player) could walk straight back in through Resources.
// Run the SAME token scan over the built data files, i.e. over what qa_sandbox/owner_install read later.
string[] builtKitNames = s.result == BuildResult.Succeeded
? KitNamesInBuiltData(s.outputPath) : new string[0];

string reportPath = Path.Combine(outDir, ReportFileName);
File.WriteAllText(reportPath,
"result=" + s.result + "\n" +
Expand All @@ -281,7 +308,14 @@ public static void Build()
"architecture=" + archResult + "\n" +
"alwaysIncludedShaders=" + string.Join(",", includedShaders) + "\n" +
"strippedQARoots=" + (strippedQARoots.Length == 0 ? "(none)" : string.Join(",", strippedQARoots)) + "\n" +
"scenesBuilt=" + string.Join(",", options.scenes) + "\n");
"scenesBuilt=" + string.Join(",", options.scenes) + "\n" +
"builtDataKitNames=" + (builtKitNames.Length == 0 ? "(none)" : string.Join(",", builtKitNames)) + "\n");

if (builtKitNames.Length > 0)
FailBuild("refusing to ship: the BUILT player data contains QA kit names "
+ string.Join(",", builtKitNames) + " (a KitRoom_* construction reached the player through the "
+ "scene or through Assets/Resources). Re-bake the live room prefab so nothing in it is named "
+ QARootPrefix + "* (Tools/WorldOS/Kit/Bake Live Room Prefab renames every descendant to LiveRoom_).");

Debug.Log("[BuildMacOSPlayer] DONE result=" + s.result + " errors=" + s.totalErrors
+ " warnings=" + s.totalWarnings + " size=" + s.totalSize + " time=" + s.totalTime
Expand Down Expand Up @@ -319,6 +353,74 @@ static string[] KitNamesOnDisk(string scenePath)
return new List<string>(foreign).ToArray();
}

// #1793 Day 3: the same whole-token scan run over the BUILT player's serialized data — level0 (the scene),
// resources.assets (everything under Assets/Resources, i.e. the live-room prefab + its materials) and the
// sharedassets*.assets the scene/prefab pull in. Byte-level (these files are binary; a text decode would
// corrupt the surrounding bytes and could split a token). Empty array = clean.
static string[] KitNamesInBuiltData(string appPath)
{
var foreign = new SortedSet<string>();
string dataDir = Path.Combine(appPath, "Contents", "Resources", "Data");
if (!Directory.Exists(dataDir))
{
Debug.LogWarning("[BuildMacOSPlayer] built data dir not found (" + dataDir + ") — kit scan skipped");
return new string[0];
}
var helpers = new HashSet<string> { BuildRoomKit.KitHelperFire, BuildRoomKit.KitHelperTombGlow, BuildRoomKit.KitHelperCoolKey };
var targets = new List<string>();
foreach (var f in Directory.GetFiles(dataDir))
{
string n = Path.GetFileName(f);
if (n.StartsWith("level", StringComparison.Ordinal)
|| n.StartsWith("resources.assets", StringComparison.Ordinal)
|| n.StartsWith("sharedassets", StringComparison.Ordinal)) targets.Add(f);
}
foreach (var f in targets)
foreach (var tok in KitTokensInBytes(File.ReadAllBytes(f)))
if (!helpers.Contains(tok)) foreign.Add(tok);
Comment on lines +379 to +380
Debug.Log("[BuildMacOSPlayer] built-data kit scan: " + targets.Count + " file(s), "
+ (foreign.Count == 0 ? "clean" : "FOREIGN " + string.Join(",", new List<string>(foreign).ToArray())));
return new List<string>(foreign).ToArray();
}

// Every whole `KitRoom_<token>` occurrence in a byte buffer (token chars = A-Za-z0-9_-), matching the
// regex KitNamesOnDisk applies to the scene text.
static List<string> KitTokensInBytes(byte[] bytes)
{
var hits = new List<string>();
var pre = System.Text.Encoding.ASCII.GetBytes(QARootPrefix);
for (int i = 0; i + pre.Length <= bytes.Length; i++)
{
bool match = true;
for (int k = 0; k < pre.Length; k++) if (bytes[i + k] != pre[k]) { match = false; break; }
if (!match) continue;
var sb = new System.Text.StringBuilder(QARootPrefix);
int j = i + pre.Length;
while (j < bytes.Length)
{
char c = (char)bytes[j];
if (!(char.IsLetterOrDigit(c) || c == '_' || c == '-')) break;
sb.Append(c); j++;
}
hits.Add(sb.ToString());
i = j - 1;
}
return hits;
}

// Remove every BeautifyEffect.Beautify from a scene (reflection: the asset may be absent in a given
// project, and this file must still compile). Returns how many were removed.
static int StripCameraGrade(Scene scene)
{
var t = PainterlyRoomLights.BeautifyType();
if (t == null) return 0;
int n = 0;
foreach (var go in scene.GetRootGameObjects())
foreach (var c in go.GetComponentsInChildren(t, true))
{ UnityEngine.Object.DestroyImmediate(c, true); n++; }
return n;
}

static string[] StripQAConstructions(ref string buildScenePath)
{
var active = EditorSceneManager.GetActiveScene();
Expand Down Expand Up @@ -368,6 +470,14 @@ static string[] StripQAConstructions(ref string buildScenePath)
foreach (var go in tmp.GetRootGameObjects())
if (go != null && go.name.StartsWith(QARootPrefix, StringComparison.Ordinal))
{ UnityEngine.Object.DestroyImmediate(go); destroyed++; }
// #1793 Day 3b: the room-authoring session adds a Beautify post component to Main Camera to grade
// the LOOK capture. It lives only in memory — but this copy is taken FROM memory, so it would ship
// and grade every painted plate, none of which was authored or panel-judged under it. The grade is
// a property of a live room now (baked on its prefab, applied by CombatSurfaceClient.ApplyRoom),
// so strip it from the shipped scene: grade-free by construction, not by a runtime disable alone.
int graded = StripCameraGrade(tmp);
if (graded > 0) Debug.LogWarning("[BuildMacOSPlayer] removed " + graded + " editor-only "
+ PainterlyRoomLights.BeautifyTypeName.Split(',')[0] + " component(s) from the build scene copy");
bool saved = EditorSceneManager.SaveScene(tmp);
EditorSceneManager.CloseScene(tmp, true);
if (!saved || destroyed != qaRoots.Count)
Expand Down
Loading
Loading