Skip to content

Commit 8ec5f28

Browse files
author
DavidQ
committed
Lock game asset loading to manifest-declared assets only - PR 11.83
1 parent d5a398f commit 8ec5f28

9 files changed

Lines changed: 442 additions & 53 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
1-
# Codex Commands - PR 11.82
1+
# Codex Commands PR 11.83
22

3-
## Purpose
4-
Add CI-safe utils rule enforcement after the `src/engine/utils` to `src/shared/utils` consolidation lane.
3+
Model: GPT-5.4
4+
Reasoning: high
55

6-
## Command
7-
Run this from the repository root:
6+
```text
7+
Run BUILD_PR_LEVEL_11_83_LOCK_ASSET_LOADING_TO_MANIFEST_ONLY.
88
9-
```powershell
10-
powershell -ExecutionPolicy Bypass -File .\scripts\PS\enforce-utils-rules.ps1 -Details
11-
```
9+
Implement a targeted fix so Workspace Manager/game launch asset loading uses game.manifest.json as the only source of truth for game chrome assets. Do not guess /games/<Game>/assets/images/bezel.png or background.png. Do not add fallback assets. Do not add aliases or pass-through shims.
1210
13-
For CI/regression mode:
11+
Tasks:
12+
1. Find code that derives game chrome image paths by convention, especially bezel/background.
13+
2. Replace convention-derived URLs with explicit manifest asset lookup.
14+
3. If a manifest does not declare an optional chrome asset, skip rendering/requesting that image and show safe empty state.
15+
4. Preserve games that already declare chrome assets in game.manifest.json.
16+
5. Search the repo for remaining hardcoded or derived references to /assets/images/bezel.png and /assets/images/background.png and remove convention-only loading paths.
17+
6. Run targeted validation only:
18+
- launch SolarSystem and verify no bezel/background 404s
19+
- launch a game with declared manifest chrome assets, such as Asteroids if available, and verify declared assets still load
20+
- run syntax/import checks for changed JS files
21+
7. Write evidence to docs/dev/reports/asset_manifest_only_validation.md.
22+
8. If roadmap status is touched, status-only update only.
1423
15-
```powershell
16-
powershell -ExecutionPolicy Bypass -File .\scripts\PS\enforce-utils-rules.ps1 -Ci
24+
Return a ZIP artifact at <project folder>/tmp/PR_11_83_LOCK_ASSET_LOADING_TO_MANIFEST_ONLY.zip.
1725
```
18-
19-
## Acceptance
20-
- Script runs without parser errors.
21-
- CSV report is written to `docs/dev/reports/utils_rules_audit.csv`.
22-
- `-Details` shows findings only when requested.
23-
- `-Ci` exits nonzero when findings exist.
24-
- No files are deleted or moved by this PR.

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Enforce utility consolidation rules with CI-safe audit script - PR 11.82
1+
Lock game asset loading to manifest-declared assets only - PR 11.83
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# PR 11.83 Asset Manifest-Only Validation
2+
3+
## Scope
4+
Lock game chrome asset loading (background/bezel) to explicit `game.manifest.json` declarations only.
5+
6+
## Files changed
7+
- `src/engine/runtime/gameImageConvention.js`
8+
- `src/engine/runtime/backgroundImage.js`
9+
- `src/engine/runtime/fullscreenBezel.js`
10+
11+
## Implementation summary
12+
- Removed convention-only chrome path derivation for:
13+
- `games/<Game>/assets/images/background.png`
14+
- `games/<Game>/assets/images/bezel.png`
15+
- Added manifest-driven chrome resolution via `resolveManifestChromeAssetPaths(...)`.
16+
- Background and bezel runtime layers now:
17+
- resolve chrome image paths from `game.manifest.json`
18+
- skip image request/render when manifest does not declare the optional asset
19+
- keep loading declared chrome assets when present (for example Asteroids bezel)
20+
21+
## Validation commands and evidence
22+
23+
### 1) Syntax/import checks for changed JS files
24+
- `node --check src/engine/runtime/gameImageConvention.js`
25+
- `node --check src/engine/runtime/backgroundImage.js`
26+
- `node --check src/engine/runtime/fullscreenBezel.js`
27+
- Result: PASS
28+
29+
### 2) Runtime convention-path removal scan (runtime code)
30+
- Command:
31+
- `rg -n "assets/images/bezel\\.png|assets/images/background\\.png|toImagePath\\(|games/.*/assets/images/(bezel|background)\\.png" src/engine/runtime`
32+
- Result: no matches in `src/engine/runtime`
33+
34+
### 3) Targeted SolarSystem/Asteroids behavior check
35+
- Executed targeted runtime validation script (inline Node ESM) that:
36+
- loads `games/SolarSystem/game.manifest.json`
37+
- loads `games/Asteroids/game.manifest.json`
38+
- verifies SolarSystem makes no chrome image request when manifest has no chrome image entry
39+
- verifies Asteroids requests declared manifest bezel image
40+
- Result output:
41+
- `VALIDATION_PASS SolarSystem no chrome image request; Asteroids manifest bezel request present.`
42+
43+
### 4) Browser launch smoke (games) for regression signal
44+
- `node tests/runtime/LaunchSmokeAllEntries.test.mjs --games`
45+
- Result: PASS (`12/12` games)
46+
- Includes PASS for:
47+
- `Asteroids`
48+
- `SolarSystem`
49+
50+
## Remaining literal bezel/background path references
51+
- Remaining matches are in tests/docs/manifest data and not convention-loader runtime code.
52+
- Command used:
53+
- `rg -n "assets/images/bezel\\.png|assets/images/background\\.png" src games tools samples tests`
54+
55+
## Acceptance check
56+
- SolarSystem no longer relies on guessed `background.png`/`bezel.png` runtime convention loading.
57+
- Asteroids still loads declared chrome asset from manifest (`image.asteroids.bezel`).
58+
- No fallback assets, aliases, shims, or guessed chrome-path loader logic added.
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-29T20:44:47.603Z
3+
Generated: 2026-04-29T23:54:15.633Z
44

5-
Filters: games=false, samples=true, tools=false, sampleRange=1208-1208
5+
Filters: games=true, samples=false, tools=false, sampleRange=all
66

77
| Status | Type | Label | Path | Notes | Steps |
88
| --- | --- | --- | --- | --- | --- |
9-
| PASS | sample | 1208 | samples\phase-12\1208\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
9+
| PASS | game | _template | games\_template\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
10+
| PASS | game | AITargetDummy | games\AITargetDummy\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
11+
| PASS | game | Asteroids | games\Asteroids\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
12+
| PASS | game | Bouncing-ball | games\Bouncing-ball\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
13+
| PASS | game | Breakout | games\Breakout\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
14+
| PASS | game | GravityWell | games\GravityWell\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
15+
| PASS | game | Pacman | games\Pacman\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
16+
| PASS | game | Pong | games\Pong\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
17+
| PASS | game | SolarSystem | games\SolarSystem\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
18+
| PASS | game | SpaceDuel | games\SpaceDuel\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
19+
| PASS | game | SpaceInvaders | games\SpaceInvaders\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
20+
| PASS | game | vector-arcade-sample | games\vector-arcade-sample\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# PR 11.83 — Lock Asset Loading To Manifest Only
2+
3+
## Purpose
4+
Prevent game/tool launch code from requesting guessed asset paths such as `bezel.png`, `background.png`, sample defaults, or hidden fallback assets that are not explicitly declared in the owning manifest.
5+
6+
## Scope
7+
- Game launch/chrome image loading
8+
- Workspace Manager game/tool asset display paths
9+
- Manifest-driven asset lookup helpers if present
10+
- Targeted validation only
11+
12+
## Required behavior
13+
1. Treat `game.manifest.json` as the source of truth for game assets.
14+
2. Only request an asset URL when the manifest explicitly declares that asset.
15+
3. If an optional asset is missing from the manifest, render a safe empty state.
16+
4. Do not hardcode or guess these paths:
17+
- `/games/<Game>/assets/images/bezel.png`
18+
- `/games/<Game>/assets/images/background.png`
19+
- any other convention-only asset path
20+
5. Do not add silent fallback data or hidden default assets.
21+
6. Do not create aliases, bridge helpers, or compatibility shims.
22+
23+
## Specific known failure to fix
24+
SolarSystem currently logs 404 requests similar to:
25+
26+
```text
27+
GET /games/SolarSystem/assets/images/bezel.png 404
28+
GET /games/SolarSystem/assets/images/background.png 404
29+
```
30+
31+
Those requests must stop unless SolarSystem declares those assets in its `game.manifest.json`.
32+
33+
## Implementation direction
34+
- Locate the launch/chrome code that currently derives bezel/background image URLs by convention.
35+
- Replace convention-derived path construction with manifest lookup.
36+
- Support existing manifest asset entries such as:
37+
38+
```json
39+
"image.asteroids.bezel": {
40+
"path": "/games/Asteroids/assets/images/bezel.png",
41+
"kind": "image",
42+
"source": "workspace-manager"
43+
}
44+
```
45+
46+
- Prefer an explicit manifest key lookup when keys are known.
47+
- Otherwise scan manifest asset entries for `kind: "image"` and a semantic key suffix/name such as `bezel` or `background`.
48+
- If no matching manifest asset exists, return `null` and skip image rendering/requesting.
49+
50+
## Out of scope
51+
- Do not create missing image files.
52+
- Do not add placeholder images.
53+
- Do not change game art.
54+
- Do not refactor the manifest schema unless required for the fix.
55+
- Do not modify unrelated games/tools.
56+
57+
## Acceptance
58+
- SolarSystem no longer requests nonexistent `bezel.png` or `background.png`.
59+
- Asteroids or any game with manifest-declared chrome assets still displays those assets.
60+
- No hardcoded convention fallback remains for game chrome assets.
61+
- Targeted browser validation shows no missing image 404s for affected game launch path.

src/engine/runtime/backgroundImage.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { resolveGameImageConventionPaths, resolveRuntimeAssetUrl } from "./gameImageConvention.js";
1+
import {
2+
resolveGameImageConventionPaths,
3+
resolveManifestChromeAssetPaths,
4+
resolveRuntimeAssetUrl
5+
} from "./gameImageConvention.js";
26

37
const NON_GAMEPLAY_MODE_TOKENS = Object.freeze([
48
"menu",
@@ -82,20 +86,54 @@ export default class backgroundImage {
8286
documentRef: this.documentRef
8387
});
8488
this.gameId = resolved.gameId;
89+
this.manifestPath = resolved.manifestPath;
8590
this.layer = createLayerState(resolved.backgroundPath);
8691
this.imageFactory = typeof options.imageFactory === "function"
8792
? options.imageFactory
8893
: (typeof Image === "function" ? () => new Image() : null);
94+
this.manifestResolved = false;
95+
this.manifestResolvePromise = null;
8996
}
9097

9198
getState() {
9299
return {
93100
gameId: this.gameId,
94101
path: this.layer.path,
95-
status: this.layer.status
102+
status: this.layer.status,
103+
manifestPath: this.manifestPath,
104+
manifestResolved: this.manifestResolved
96105
};
97106
}
98107

108+
ensureManifestResolved() {
109+
if (this.manifestResolved) {
110+
return;
111+
}
112+
if (this.manifestResolvePromise) {
113+
return;
114+
}
115+
116+
this.manifestResolvePromise = resolveManifestChromeAssetPaths({
117+
gameId: this.gameId,
118+
manifestPath: this.manifestPath,
119+
documentRef: this.documentRef
120+
})
121+
.then((resolved) => {
122+
this.gameId = resolved.gameId || this.gameId;
123+
this.manifestPath = resolved.manifestPath || this.manifestPath;
124+
this.layer.path = typeof resolved.backgroundPath === "string" ? resolved.backgroundPath.trim() : "";
125+
this.layer.status = this.layer.path ? "idle" : "unavailable";
126+
})
127+
.catch(() => {
128+
this.layer.path = "";
129+
this.layer.status = "unavailable";
130+
})
131+
.finally(() => {
132+
this.manifestResolved = true;
133+
this.manifestResolvePromise = null;
134+
});
135+
}
136+
99137
isGameplayState(scene) {
100138
if (!scene || typeof scene !== "object") {
101139
return true;
@@ -129,8 +167,12 @@ export default class backgroundImage {
129167
}
130168

131169
ensureLoaded() {
170+
this.ensureManifestResolved();
171+
132172
if (!this.layer.path) {
133-
this.layer.status = "unavailable";
173+
if (this.manifestResolved) {
174+
this.layer.status = "unavailable";
175+
}
134176
return;
135177
}
136178
if (this.layer.status === "ready" || this.layer.status === "missing" || this.layer.status === "loading") {

src/engine/runtime/fullscreenBezel.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
resolveBezelStretchOverridePath,
33
resolveGameImageConventionPaths,
4+
resolveManifestChromeAssetPaths,
45
resolveRuntimeAssetUrl
56
} from "./gameImageConvention.js";
67

@@ -183,10 +184,11 @@ export function sanitizeUniformEdgeStretchPx(value) {
183184
return Math.max(0, safeNumber(value, 0));
184185
}
185186

186-
export function resolveBezelStretchConfigPath(bezelPath, fileName = DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME) {
187+
export function resolveBezelStretchConfigPath(bezelPath, fileName = DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME, manifestPath = "") {
187188
return resolveBezelStretchOverridePath({
188189
bezelPath,
189-
fileName
190+
fileName,
191+
manifestPath
190192
});
191193
}
192194

@@ -670,8 +672,9 @@ export default class fullscreenBezel {
670672
this.canvas = options.canvas || null;
671673
this.defaultHost = options.host || null;
672674
this.gameId = resolved.gameId;
675+
this.manifestPath = resolved.manifestPath;
673676
this.path = resolved.bezelPath;
674-
this.stretchConfigPath = resolveBezelStretchConfigPath(this.path);
677+
this.stretchConfigPath = resolveBezelStretchConfigPath(this.path, DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME, this.manifestPath);
675678
this.host = null;
676679
this.element = null;
677680
this.ready = false;
@@ -686,6 +689,8 @@ export default class fullscreenBezel {
686689
this.transparentWindowRect = null;
687690
this.imageSize = null;
688691
this.canvasLayoutMode = "fallback";
692+
this.manifestResolved = false;
693+
this.manifestResolvePromise = null;
689694
}
690695

691696
getState() {
@@ -699,10 +704,47 @@ export default class fullscreenBezel {
699704
transparentWindowRect: this.transparentWindowRect,
700705
stretchConfigPath: this.stretchConfigPath,
701706
stretchConfigInitialized: this.stretchConfigInitialized,
702-
uniformEdgeStretchPx: this.uniformEdgeStretchPx
707+
uniformEdgeStretchPx: this.uniformEdgeStretchPx,
708+
manifestPath: this.manifestPath,
709+
manifestResolved: this.manifestResolved
703710
};
704711
}
705712

713+
ensureManifestResolved() {
714+
if (this.manifestResolved) {
715+
return;
716+
}
717+
if (this.manifestResolvePromise) {
718+
return;
719+
}
720+
721+
this.manifestResolvePromise = resolveManifestChromeAssetPaths({
722+
gameId: this.gameId,
723+
manifestPath: this.manifestPath,
724+
documentRef: this.documentRef
725+
})
726+
.then((resolved) => {
727+
this.gameId = resolved.gameId || this.gameId;
728+
this.manifestPath = resolved.manifestPath || this.manifestPath;
729+
this.path = typeof resolved.bezelPath === "string" ? resolved.bezelPath.trim() : "";
730+
this.missing = !this.path;
731+
this.stretchConfigPath = resolveBezelStretchConfigPath(
732+
this.path,
733+
DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME,
734+
this.manifestPath
735+
);
736+
})
737+
.catch(() => {
738+
this.path = "";
739+
this.missing = true;
740+
this.stretchConfigPath = "";
741+
})
742+
.finally(() => {
743+
this.manifestResolved = true;
744+
this.manifestResolvePromise = null;
745+
});
746+
}
747+
706748
resolveHost() {
707749
if (this.defaultHost && isAppendable(this.defaultHost)) {
708750
return this.defaultHost;
@@ -787,6 +829,8 @@ export default class fullscreenBezel {
787829
}
788830

789831
attach() {
832+
this.ensureManifestResolved();
833+
790834
if (!this.documentRef || !this.path || this.element) {
791835
return;
792836
}
@@ -983,11 +1027,13 @@ export default class fullscreenBezel {
9831027
}
9841028

9851029
sync(options = {}) {
1030+
this.ensureManifestResolved();
1031+
9861032
if (!this.path) {
9871033
this.applyCanvasFallbackLayout();
9881034
return {
9891035
visible: false,
990-
reason: "unavailable",
1036+
reason: this.manifestResolved ? "unavailable" : "manifest-pending",
9911037
path: this.path
9921038
};
9931039
}

0 commit comments

Comments
 (0)