Skip to content

Commit d2033eb

Browse files
author
DavidQ
committed
feat(metadata): generate games engineClassesUsed from full game JS tree (scene/world/controller), excluding Theme*
chore(games): add standard header/run instructions to normalize-games-presentation script; class dropdown remains metadata-driven from engineClassesUsed
1 parent 495fdb1 commit d2033eb

3 files changed

Lines changed: 309 additions & 9 deletions

File tree

games/metadata/games.index.metadata.json

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
],
1313
"engineClassesUsed": [
1414
"engine/core/Engine",
15-
"engine/input/index/InputService"
15+
"engine/input/index/GamepadInputAdapter",
16+
"engine/input/index/InputService",
17+
"engine/scene/index/Scene",
18+
"engine/utils/math/clamp"
1619
],
1720
"tags": [
1821
"arcade",
@@ -58,7 +61,10 @@
5861
],
5962
"engineClassesUsed": [
6063
"engine/core/Engine",
61-
"engine/input/index/InputService"
64+
"engine/input/index/GamepadInputAdapter",
65+
"engine/input/index/InputService",
66+
"engine/scene/index/Scene",
67+
"engine/utils/math/clamp"
6268
]
6369
},
6470
{
@@ -88,7 +94,8 @@
8894
],
8995
"engineClassesUsed": [
9096
"engine/core/Engine",
91-
"engine/input/index/InputService"
97+
"engine/input/index/InputService",
98+
"engine/scene/index/Scene"
9299
]
93100
},
94101
{
@@ -117,7 +124,14 @@
117124
],
118125
"engineClassesUsed": [
119126
"engine/core/Engine",
120-
"engine/input/index/InputService"
127+
"engine/debug/index/drawPanel",
128+
"engine/input/index/InputService",
129+
"engine/rendering/VectorDrawing/drawVectorShape",
130+
"engine/rendering/VectorDrawing/transformPoints",
131+
"engine/replay/index/ReplaySystem",
132+
"engine/scene/index/Scene",
133+
"engine/utils/index/clamp",
134+
"engine/utils/index/distance"
121135
]
122136
},
123137
{
@@ -147,7 +161,10 @@
147161
],
148162
"engineClassesUsed": [
149163
"engine/core/Engine",
150-
"engine/input/index/InputService"
164+
"engine/input/index/GamepadInputAdapter",
165+
"engine/input/index/InputService",
166+
"engine/scene/index/Scene",
167+
"engine/utils/math/clamp"
151168
]
152169
},
153170
{
@@ -173,6 +190,25 @@
173190
"vector-asset-studio",
174191
"vector-map-editor",
175192
"asset-pipeline-tool"
193+
],
194+
"engineClassesUsed": [
195+
"engine/audio/index/GaplessLoopPlayer",
196+
"engine/audio/index/HtmlAudioMediaBackend",
197+
"engine/audio/index/MediaTrackService",
198+
"engine/collision/index/arePolygonsColliding",
199+
"engine/core/Engine",
200+
"engine/debug/inspectors/shared/inspectorUtils/asArray",
201+
"engine/debug/inspectors/shared/inspectorUtils/asObject",
202+
"engine/fx/index/ParticleSystem",
203+
"engine/input/index/InputService",
204+
"engine/persistence/index/StorageService",
205+
"engine/rendering/index/transformPoints",
206+
"engine/scene/index/AttractModeController",
207+
"engine/scene/index/Scene",
208+
"engine/utils/index/distance",
209+
"engine/utils/index/randomRange",
210+
"engine/utils/index/wrap",
211+
"engine/utils/math/clamp"
176212
]
177213
},
178214
{
@@ -201,7 +237,12 @@
201237
],
202238
"engineClassesUsed": [
203239
"engine/core/Engine",
204-
"engine/input/index/InputService"
240+
"engine/input/index/GamepadInputAdapter",
241+
"engine/input/index/InputService",
242+
"engine/persistence/index/StorageService",
243+
"engine/scene/index/AttractModeController",
244+
"engine/scene/index/Scene",
245+
"engine/utils/math/clamp"
205246
]
206247
},
207248
{
@@ -231,7 +272,11 @@
231272
"engineClassesUsed": [
232273
"engine/core/Engine",
233274
"engine/input/index/ActionInputMap",
234-
"engine/input/index/ActionInputService"
275+
"engine/input/index/ActionInputService",
276+
"engine/persistence/index/StorageService",
277+
"engine/scene/index/AttractModeController",
278+
"engine/scene/index/Scene",
279+
"engine/utils/math/clamp"
235280
]
236281
},
237282
{
@@ -260,7 +305,10 @@
260305
],
261306
"engineClassesUsed": [
262307
"engine/core/Engine",
263-
"engine/input/index/InputService"
308+
"engine/input/index/GamepadInputAdapter",
309+
"engine/input/index/InputService",
310+
"engine/scene/index/Scene",
311+
"engine/utils/math/clamp"
264312
]
265313
},
266314
{
@@ -289,7 +337,9 @@
289337
],
290338
"engineClassesUsed": [
291339
"engine/core/Engine",
292-
"engine/input/index/InputService"
340+
"engine/input/index/GamepadInputAdapter",
341+
"engine/input/index/InputService",
342+
"engine/scene/index/Scene"
293343
]
294344
},
295345
{

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"pretest": "node tools/dev/checkSharedExtractionGuard.mjs",
55
"test": "node ./scripts/run-node-tests.mjs",
66
"build:manifest": "node ./scripts/generate-sample-manifest.mjs",
7+
"normalize:games-presentation": "node ./scripts/normalize-games-presentation.mjs",
78
"check:shared-extraction-guard": "node tools/dev/checkSharedExtractionGuard.mjs",
89
"check:phase24-closeout-guard": "node tools/dev/checkPhase24CloseoutExecutionGuard.mjs",
910
"check:style-system-guard": "node tools/dev/checkStyleSystemGuard.mjs",
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/25/2026
5+
6+
normalize-games-presentation.mjs
7+
8+
How to run:
9+
1) npm run normalize:games-presentation
10+
2) Reload /games/index.html to refresh the Class dropdown from engineClassesUsed
11+
*/
12+
13+
import fs from "node:fs";
14+
import path from "node:path";
15+
16+
const ROOT = process.cwd();
17+
const GAMES_DIR = path.join(ROOT, "games");
18+
const METADATA_PATH = path.join(GAMES_DIR, "metadata", "games.index.metadata.json");
19+
20+
function readFile(filePath) {
21+
return fs.readFileSync(filePath, "utf8");
22+
}
23+
24+
function writeFile(filePath, content) {
25+
fs.writeFileSync(filePath, content, "utf8");
26+
}
27+
28+
function normalizeWhitespace(value) {
29+
return String(value || "")
30+
.replace(/\s+/g, " ")
31+
.trim();
32+
}
33+
34+
function normalizeEngineReference(rawRef) {
35+
const text = normalizeWhitespace(rawRef).replace(/\\/g, "/");
36+
if (!text) {
37+
return "";
38+
}
39+
40+
const marker = "src/engine/";
41+
const markerIndex = text.indexOf(marker);
42+
const normalized = markerIndex >= 0 ? text.slice(markerIndex + marker.length) : text;
43+
const cleaned = normalized
44+
.replace(/\.js$/i, "")
45+
.replace(/^\/+|\/+$/g, "")
46+
.replace(/\/+/g, "/");
47+
if (!cleaned) {
48+
return "";
49+
}
50+
51+
const segments = cleaned.split("/").filter(Boolean);
52+
if (segments.length >= 2 && segments[segments.length - 1] === segments[segments.length - 2]) {
53+
segments.pop();
54+
}
55+
56+
return `engine/${segments.join("/")}`;
57+
}
58+
59+
function isThemeEngineClass(value) {
60+
const normalized = normalizeEngineReference(value);
61+
if (!normalized) {
62+
return false;
63+
}
64+
const leaf = normalized.split("/").pop() || "";
65+
return /^theme/i.test(leaf);
66+
}
67+
68+
function walkJsFiles(dirPath) {
69+
const out = [];
70+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
71+
for (const entry of entries) {
72+
const fullPath = path.join(dirPath, entry.name);
73+
if (entry.isDirectory()) {
74+
if (entry.name === "assets" || entry.name === "node_modules") {
75+
continue;
76+
}
77+
out.push(...walkJsFiles(fullPath));
78+
continue;
79+
}
80+
if (entry.isFile() && /\.(m?js)$/i.test(entry.name)) {
81+
out.push(fullPath);
82+
}
83+
}
84+
return out;
85+
}
86+
87+
function parseImportSymbols(specifierText) {
88+
const text = normalizeWhitespace(specifierText);
89+
if (!text) {
90+
return [];
91+
}
92+
93+
if (text.startsWith("{") && text.endsWith("}")) {
94+
return text
95+
.slice(1, -1)
96+
.split(",")
97+
.map((part) => normalizeWhitespace(part))
98+
.filter(Boolean)
99+
.map((part) => normalizeWhitespace(part.split(/\s+as\s+/i)[0]))
100+
.filter(Boolean);
101+
}
102+
103+
if (text.startsWith("*")) {
104+
const match = text.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
105+
return match ? [match[1]] : [];
106+
}
107+
108+
if (text.includes(",")) {
109+
const [defaultPart, namedPart] = text.split(",", 2);
110+
const symbols = [];
111+
const defaultSymbol = normalizeWhitespace(defaultPart);
112+
if (defaultSymbol) {
113+
symbols.push(defaultSymbol);
114+
}
115+
if (namedPart && namedPart.includes("{")) {
116+
symbols.push(...parseImportSymbols(namedPart.slice(namedPart.indexOf("{"))));
117+
}
118+
return symbols;
119+
}
120+
121+
return [text];
122+
}
123+
124+
function collectFromImportSource(importSource, symbols, refs) {
125+
const source = String(importSource || "").replace(/\\/g, "/");
126+
const marker = "src/engine/";
127+
const markerIndex = source.indexOf(marker);
128+
if (markerIndex < 0) {
129+
return;
130+
}
131+
132+
const afterMarker = source.slice(markerIndex + marker.length).replace(/^\//, "");
133+
if (!afterMarker) {
134+
return;
135+
}
136+
137+
const modulePath = afterMarker.replace(/\.js$/i, "");
138+
if (!modulePath) {
139+
return;
140+
}
141+
142+
if (!symbols || symbols.length === 0) {
143+
refs.add(normalizeEngineReference(`src/engine/${modulePath}`));
144+
return;
145+
}
146+
147+
for (const symbol of symbols) {
148+
const cleanSymbol = normalizeWhitespace(symbol);
149+
if (!cleanSymbol) {
150+
continue;
151+
}
152+
refs.add(normalizeEngineReference(`src/engine/${modulePath}/${cleanSymbol}`));
153+
}
154+
}
155+
156+
function collectEngineClassReferencesFromJs(gameDir) {
157+
const files = walkJsFiles(gameDir);
158+
const refs = new Set();
159+
160+
for (const filePath of files) {
161+
const source = readFile(filePath);
162+
const importRegex = /(?:^|\n)\s*import\s+([\s\S]*?)\s+from\s+["']([^"']+)["']/g;
163+
const exportRegex = /(?:^|\n)\s*export\s+[\s\S]*?\s+from\s+["']([^"']+)["']/g;
164+
const bareImportRegex = /(?:^|\n)\s*import\s+["']([^"']+)["']/g;
165+
166+
let match;
167+
while ((match = importRegex.exec(source)) !== null) {
168+
const specifier = match[1];
169+
const importSource = match[2];
170+
collectFromImportSource(importSource, parseImportSymbols(specifier), refs);
171+
}
172+
173+
while ((match = exportRegex.exec(source)) !== null) {
174+
const importSource = match[1];
175+
collectFromImportSource(importSource, [], refs);
176+
}
177+
178+
while ((match = bareImportRegex.exec(source)) !== null) {
179+
const importSource = match[1];
180+
collectFromImportSource(importSource, [], refs);
181+
}
182+
}
183+
184+
return [...refs]
185+
.filter(Boolean)
186+
.filter((value) => !isThemeEngineClass(value))
187+
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
188+
}
189+
190+
function loadMetadata() {
191+
if (!fs.existsSync(METADATA_PATH)) {
192+
throw new Error("Missing metadata file: " + path.relative(ROOT, METADATA_PATH));
193+
}
194+
195+
let parsed;
196+
try {
197+
parsed = JSON.parse(readFile(METADATA_PATH));
198+
} catch (error) {
199+
throw new Error("Invalid metadata JSON: " + error.message);
200+
}
201+
202+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.games)) {
203+
throw new Error('Metadata must contain a "games" array.');
204+
}
205+
return parsed;
206+
}
207+
208+
function toGameDirFromHref(href) {
209+
const normalizedHref = String(href || "").replace(/\\/g, "/").trim();
210+
if (!normalizedHref.startsWith("/games/") || !normalizedHref.endsWith("/index.html")) {
211+
return "";
212+
}
213+
return path.join(ROOT, normalizedHref.slice(1, -"/index.html".length));
214+
}
215+
216+
function normalizeGamesPresentation() {
217+
const metadata = loadMetadata();
218+
let updatedCount = 0;
219+
220+
for (const game of metadata.games) {
221+
const gameDir = toGameDirFromHref(game?.href);
222+
if (!gameDir || !fs.existsSync(gameDir)) {
223+
continue;
224+
}
225+
226+
const engineClassesUsed = collectEngineClassReferencesFromJs(gameDir);
227+
const previous = JSON.stringify(Array.isArray(game.engineClassesUsed) ? game.engineClassesUsed : []);
228+
const next = JSON.stringify(engineClassesUsed);
229+
if (previous !== next) {
230+
game.engineClassesUsed = engineClassesUsed;
231+
updatedCount += 1;
232+
}
233+
}
234+
235+
writeFile(METADATA_PATH, JSON.stringify(metadata, null, 2) + "\n");
236+
return { updatedCount, totalGames: metadata.games.length };
237+
}
238+
239+
function main() {
240+
const result = normalizeGamesPresentation();
241+
console.log(`OK updated=${result.updatedCount} games=${result.totalGames}`);
242+
}
243+
244+
try {
245+
main();
246+
} catch (error) {
247+
console.error("FAIL " + error.message);
248+
process.exit(1);
249+
}

0 commit comments

Comments
 (0)