@genex-ai/cli-demo 1.36.5-dev.858 → 1.36.6-dev.861
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +890 -397
- package/package.json +1 -1
- package/templates/controllers/character/aim-cue.ts +2 -1
- package/templates/controllers/character/follow-camera.ts +4 -3
- package/templates/skills/genex-ai-video/SKILL.md +8 -3
- package/templates/skills/genex-game-director/SKILL.md +1 -0
- package/templates/skills/genex-llm-in-games/SKILL.md +9 -0
- package/templates/skills/genex-monetization/SKILL.md +26 -0
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +3 -3
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +2 -2
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +158 -36
- package/templates/skills/genex-threejs-embed-auth/references/shells.md +207 -0
- package/templates/skills/genex-threejs-game-ui/SKILL.md +22 -14
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +46 -8
- package/templates/skills/genex-threejs-touch-controls/SKILL.md +63 -1
package/dist/index.js
CHANGED
|
@@ -2149,6 +2149,401 @@ async function countOutcomes(kind, cwd = process.cwd()) {
|
|
|
2149
2149
|
import fs13 from "fs/promises";
|
|
2150
2150
|
import path14 from "path";
|
|
2151
2151
|
|
|
2152
|
+
// ../../packages/mobile-scan/src/image-dims.ts
|
|
2153
|
+
function u32be(b, o) {
|
|
2154
|
+
return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
|
|
2155
|
+
}
|
|
2156
|
+
function u32le(b, o) {
|
|
2157
|
+
return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0;
|
|
2158
|
+
}
|
|
2159
|
+
function u16be(b, o) {
|
|
2160
|
+
return b[o] << 8 | b[o + 1];
|
|
2161
|
+
}
|
|
2162
|
+
function u16le(b, o) {
|
|
2163
|
+
return b[o] | b[o + 1] << 8;
|
|
2164
|
+
}
|
|
2165
|
+
function isPng(b) {
|
|
2166
|
+
return b.length > 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71 && b[4] === 13 && b[5] === 10 && b[6] === 26 && b[7] === 10;
|
|
2167
|
+
}
|
|
2168
|
+
var KTX2_MAGIC = [171, 75, 84, 88, 32, 50, 48, 187, 13, 10, 26, 10];
|
|
2169
|
+
function parseImageDims(bytes) {
|
|
2170
|
+
if (bytes.length < 30) return null;
|
|
2171
|
+
if (isPng(bytes)) {
|
|
2172
|
+
return { width: u32be(bytes, 16), height: u32be(bytes, 20) };
|
|
2173
|
+
}
|
|
2174
|
+
if (bytes[0] === 255 && bytes[1] === 216) {
|
|
2175
|
+
let i = 2;
|
|
2176
|
+
while (i + 9 < bytes.length) {
|
|
2177
|
+
if (bytes[i] !== 255) {
|
|
2178
|
+
i++;
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
const marker = bytes[i + 1];
|
|
2182
|
+
if (marker === 255) {
|
|
2183
|
+
i++;
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
if (marker === 216 || marker >= 208 && marker <= 217) {
|
|
2187
|
+
i += 2;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const isSof = marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207;
|
|
2191
|
+
if (isSof) {
|
|
2192
|
+
return { width: u16be(bytes, i + 7), height: u16be(bytes, i + 5) };
|
|
2193
|
+
}
|
|
2194
|
+
i += 2 + u16be(bytes, i + 2);
|
|
2195
|
+
}
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
if (bytes[0] === 82 && // RIFF….WEBP
|
|
2199
|
+
bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
2200
|
+
const fourcc = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
|
|
2201
|
+
if (fourcc === "VP8X" && bytes.length >= 30) {
|
|
2202
|
+
const w = 1 + (bytes[24] | bytes[25] << 8 | bytes[26] << 16);
|
|
2203
|
+
const h = 1 + (bytes[27] | bytes[28] << 8 | bytes[29] << 16);
|
|
2204
|
+
return { width: w, height: h };
|
|
2205
|
+
}
|
|
2206
|
+
if (fourcc === "VP8 " && bytes.length >= 30) {
|
|
2207
|
+
return { width: u16le(bytes, 26) & 16383, height: u16le(bytes, 28) & 16383 };
|
|
2208
|
+
}
|
|
2209
|
+
if (fourcc === "VP8L" && bytes.length >= 25 && bytes[20] === 47) {
|
|
2210
|
+
const b0 = bytes[21];
|
|
2211
|
+
const b1 = bytes[22];
|
|
2212
|
+
const b2 = bytes[23];
|
|
2213
|
+
const b3 = bytes[24];
|
|
2214
|
+
const width = 1 + ((b1 & 63) << 8 | b0);
|
|
2215
|
+
const height = 1 + ((b3 & 15) << 10 | b2 << 2 | (b1 & 192) >> 6);
|
|
2216
|
+
return { width, height };
|
|
2217
|
+
}
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
if (KTX2_MAGIC.every((v, idx) => bytes[idx] === v)) {
|
|
2221
|
+
return { width: u32le(bytes, 20), height: u32le(bytes, 24), ktx2: true };
|
|
2222
|
+
}
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// ../../packages/mobile-scan/src/glb.ts
|
|
2227
|
+
function u32le2(b, o) {
|
|
2228
|
+
return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0;
|
|
2229
|
+
}
|
|
2230
|
+
function scanGlb(bytes) {
|
|
2231
|
+
if (bytes.length < 20 || u32le2(bytes, 0) !== 1179937895) return null;
|
|
2232
|
+
try {
|
|
2233
|
+
const jsonLen = u32le2(bytes, 12);
|
|
2234
|
+
if (u32le2(bytes, 16) !== 1313821514) return null;
|
|
2235
|
+
const json = JSON.parse(new TextDecoder().decode(bytes.subarray(20, 20 + jsonLen)));
|
|
2236
|
+
let bin = null;
|
|
2237
|
+
const binHeader = 20 + jsonLen;
|
|
2238
|
+
if (binHeader + 8 <= bytes.length && u32le2(bytes, binHeader + 4) === 5130562) {
|
|
2239
|
+
bin = bytes.subarray(binHeader + 8, binHeader + 8 + u32le2(bytes, binHeader));
|
|
2240
|
+
}
|
|
2241
|
+
const images = [];
|
|
2242
|
+
for (const img of json.images ?? []) {
|
|
2243
|
+
if (img.bufferView == null || !bin) continue;
|
|
2244
|
+
const view = json.bufferViews?.[img.bufferView];
|
|
2245
|
+
if (!view?.byteLength) continue;
|
|
2246
|
+
const slice = bin.subarray(view.byteOffset ?? 0, (view.byteOffset ?? 0) + view.byteLength);
|
|
2247
|
+
const dims = parseImageDims(slice);
|
|
2248
|
+
if (dims) images.push({ ...dims, mimeType: img.mimeType, bytes: view.byteLength });
|
|
2249
|
+
}
|
|
2250
|
+
let triangles = 0;
|
|
2251
|
+
for (const mesh of json.meshes ?? []) {
|
|
2252
|
+
for (const prim of mesh.primitives ?? []) {
|
|
2253
|
+
const accessor = prim.indices != null ? json.accessors?.[prim.indices] : json.accessors?.[prim.attributes?.POSITION ?? -1];
|
|
2254
|
+
if (accessor?.count) triangles += Math.floor(accessor.count / 3);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return {
|
|
2258
|
+
triangles,
|
|
2259
|
+
binBytes: bin?.byteLength ?? 0,
|
|
2260
|
+
images,
|
|
2261
|
+
extensions: json.extensionsUsed ?? []
|
|
2262
|
+
};
|
|
2263
|
+
} catch {
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// ../../packages/mobile-scan/src/scan.ts
|
|
2269
|
+
var GENEX_GENERATION_URL_RE = /https:\/\/[A-Za-z0-9.-]+\/generations\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._@-]+)/g;
|
|
2270
|
+
var SKYBOX_ROLE = "skybox-equirect";
|
|
2271
|
+
var MOBILE_RUNG_SUFFIX_RE = /@(\d+)$/;
|
|
2272
|
+
var MOBILE_BUDGET_MB = { broad: 300, modern: 700 };
|
|
2273
|
+
var KNOWN_CAPABILITIES = ["microphone", "camera", "geolocation"];
|
|
2274
|
+
var CAPABILITY_LITERALS = {
|
|
2275
|
+
microphone: ["__GENEX_VOICE__", "__GENEX_CAP_MICROPHONE__"],
|
|
2276
|
+
camera: ["__GENEX_CAP_CAMERA__"],
|
|
2277
|
+
geolocation: ["__GENEX_CAP_GEOLOCATION__"]
|
|
2278
|
+
};
|
|
2279
|
+
var CONSTRAINTS_MAX_CHARS = 4e3;
|
|
2280
|
+
var MEDIA_CALL = "getUserMedia";
|
|
2281
|
+
var KEY_START = /[A-Za-z_$]/;
|
|
2282
|
+
var KEY_PART = /[\w$]/;
|
|
2283
|
+
var SPACE = /\s/;
|
|
2284
|
+
function mediaRequestsInText(text) {
|
|
2285
|
+
const out = [];
|
|
2286
|
+
let from = 0;
|
|
2287
|
+
for (let at = text.indexOf(MEDIA_CALL); at !== -1; at = text.indexOf(MEDIA_CALL, from)) {
|
|
2288
|
+
from = at + MEDIA_CALL.length;
|
|
2289
|
+
if (at > 0 && KEY_PART.test(text[at - 1])) continue;
|
|
2290
|
+
const paren = skipSpace(text, from, text.length);
|
|
2291
|
+
if (text[paren] !== "(") continue;
|
|
2292
|
+
const open = skipSpace(text, paren + 1, text.length);
|
|
2293
|
+
if (text[open] !== "{") {
|
|
2294
|
+
out.push({ index: at, audio: false, video: false, read: false });
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
const { stop: stop2, ...constraints } = readConstraints(text, open);
|
|
2298
|
+
out.push({ index: at, ...constraints });
|
|
2299
|
+
from = Math.max(from, stop2);
|
|
2300
|
+
}
|
|
2301
|
+
return out;
|
|
2302
|
+
}
|
|
2303
|
+
function skipSpace(text, from, end) {
|
|
2304
|
+
let at = from;
|
|
2305
|
+
while (at < end && SPACE.test(text[at])) at++;
|
|
2306
|
+
return at;
|
|
2307
|
+
}
|
|
2308
|
+
function indexWithin(text, needle, from, end) {
|
|
2309
|
+
for (let at = from; at + needle.length <= end; at++) {
|
|
2310
|
+
if (text.startsWith(needle, at)) return at;
|
|
2311
|
+
}
|
|
2312
|
+
return -1;
|
|
2313
|
+
}
|
|
2314
|
+
function readConstraints(text, open) {
|
|
2315
|
+
const end = Math.min(text.length, open + 1 + CONSTRAINTS_MAX_CHARS);
|
|
2316
|
+
const seen = { audio: false, video: false, read: true };
|
|
2317
|
+
const afterKey = (key, at) => {
|
|
2318
|
+
const colon = skipSpace(text, at, end);
|
|
2319
|
+
if (text[colon] !== ":") {
|
|
2320
|
+
seen.read = false;
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (key !== "audio" && key !== "video") return;
|
|
2324
|
+
const value = skipSpace(text, colon + 1, end);
|
|
2325
|
+
if (text[value] === "{" || text.startsWith("!0", value) || /^true\b/.test(text.slice(value, value + 5))) {
|
|
2326
|
+
seen[key] = true;
|
|
2327
|
+
} else if (!text.startsWith("!1", value) && !/^false\b/.test(text.slice(value, value + 6))) {
|
|
2328
|
+
seen.read = false;
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
let depth = 1;
|
|
2332
|
+
let keyNext = true;
|
|
2333
|
+
for (let i = open + 1; i < end; i++) {
|
|
2334
|
+
const ch = text[i];
|
|
2335
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2336
|
+
let close = i + 1;
|
|
2337
|
+
while (close < end && text[close] !== ch) close += text[close] === "\\" ? 2 : 1;
|
|
2338
|
+
if (close >= end) break;
|
|
2339
|
+
if (depth === 1 && keyNext) {
|
|
2340
|
+
keyNext = false;
|
|
2341
|
+
afterKey(text.slice(i + 1, close), close + 1);
|
|
2342
|
+
}
|
|
2343
|
+
i = close;
|
|
2344
|
+
} else if (ch === "/" && (text[i + 1] === "/" || text[i + 1] === "*")) {
|
|
2345
|
+
const line = text[i + 1] === "/";
|
|
2346
|
+
const close = indexWithin(text, line ? "\n" : "*/", i + 2, end);
|
|
2347
|
+
if (close === -1) break;
|
|
2348
|
+
i = line ? close : close + 1;
|
|
2349
|
+
} else if (ch === "{" || ch === "[" || ch === "(") {
|
|
2350
|
+
depth++;
|
|
2351
|
+
} else if (ch === "}" || ch === "]" || ch === ")") {
|
|
2352
|
+
if (--depth === 0) return { ...seen, stop: i + 1 };
|
|
2353
|
+
} else if (depth === 1 && ch === ",") {
|
|
2354
|
+
keyNext = true;
|
|
2355
|
+
} else if (depth === 1 && keyNext && !SPACE.test(ch)) {
|
|
2356
|
+
keyNext = false;
|
|
2357
|
+
if (!KEY_START.test(ch)) {
|
|
2358
|
+
seen.read = false;
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
let stop2 = i + 1;
|
|
2362
|
+
while (stop2 < end && KEY_PART.test(text[stop2])) stop2++;
|
|
2363
|
+
afterKey(text.slice(i, stop2), stop2);
|
|
2364
|
+
i = stop2 - 1;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return { ...seen, read: false, stop: end };
|
|
2368
|
+
}
|
|
2369
|
+
var CAPABILITY_PATTERNS = {
|
|
2370
|
+
camera: [/\bfacingMode\b/],
|
|
2371
|
+
geolocation: [
|
|
2372
|
+
/\bnavigator\??\.geolocation\b/,
|
|
2373
|
+
/\.geolocation\??\.(?:getCurrentPosition|watchPosition)\b/
|
|
2374
|
+
]
|
|
2375
|
+
};
|
|
2376
|
+
function capabilitiesInText(text) {
|
|
2377
|
+
const opensCamera = mediaRequestsInText(text).some((request) => request.video);
|
|
2378
|
+
return KNOWN_CAPABILITIES.filter(
|
|
2379
|
+
(capability) => CAPABILITY_LITERALS[capability].some((literal) => text.includes(literal)) || (CAPABILITY_PATTERNS[capability] ?? []).some((pattern) => pattern.test(text)) || capability === "camera" && opensCamera
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
function knownCapabilities(input) {
|
|
2383
|
+
if (!Array.isArray(input)) return [];
|
|
2384
|
+
return KNOWN_CAPABILITIES.filter((capability) => input.includes(capability));
|
|
2385
|
+
}
|
|
2386
|
+
var TEXT_EXT = /\.(js|mjs|cjs|html|css|json|txt)$/i;
|
|
2387
|
+
var CAPABILITY_FILE_RE = /\.(js|mjs|cjs|html)$/i;
|
|
2388
|
+
var TEXT_HEAD_BYTES = 8 * 1024 * 1024;
|
|
2389
|
+
var CAPABILITY_WINDOW_BYTES = TEXT_HEAD_BYTES;
|
|
2390
|
+
var CAPABILITY_OVERLAP_BYTES = 64 * 1024;
|
|
2391
|
+
var IMAGE_EXT = /\.(png|jpe?g|webp|ktx2)$/i;
|
|
2392
|
+
var VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
|
2393
|
+
var AUDIO_EXT = /\.(mp3|ogg|wav|m4a)$/i;
|
|
2394
|
+
var GLB_EXT = /\.(glb|gltf)$/i;
|
|
2395
|
+
function scanBundle(files) {
|
|
2396
|
+
const images = [];
|
|
2397
|
+
const glbs = [];
|
|
2398
|
+
const videos = [];
|
|
2399
|
+
const audio = [];
|
|
2400
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2401
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2402
|
+
let capabilitiesIncomplete = false;
|
|
2403
|
+
const decoder = new TextDecoder();
|
|
2404
|
+
let text = "";
|
|
2405
|
+
let totalBytes = 0;
|
|
2406
|
+
for (const f of files) {
|
|
2407
|
+
totalBytes += f.bytes.byteLength;
|
|
2408
|
+
if (IMAGE_EXT.test(f.relPath)) {
|
|
2409
|
+
const dims = parseImageDims(f.bytes);
|
|
2410
|
+
if (dims) images.push({ path: f.relPath, bytes: f.bytes.byteLength, ...dims });
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (GLB_EXT.test(f.relPath)) {
|
|
2414
|
+
glbs.push({ path: f.relPath, bytes: f.bytes.byteLength, scan: scanGlb(f.bytes) });
|
|
2415
|
+
continue;
|
|
2416
|
+
}
|
|
2417
|
+
if (VIDEO_EXT.test(f.relPath)) {
|
|
2418
|
+
videos.push({ path: f.relPath, bytes: f.bytes.byteLength });
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
if (AUDIO_EXT.test(f.relPath)) {
|
|
2422
|
+
audio.push({ path: f.relPath, bytes: f.bytes.byteLength });
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (TEXT_EXT.test(f.relPath)) {
|
|
2426
|
+
const head = decoder.decode(f.bytes.subarray(0, TEXT_HEAD_BYTES));
|
|
2427
|
+
text += head;
|
|
2428
|
+
text += "\n";
|
|
2429
|
+
if (!CAPABILITY_FILE_RE.test(f.relPath)) continue;
|
|
2430
|
+
try {
|
|
2431
|
+
for (const capability of capabilitiesInText(head)) declared.add(capability);
|
|
2432
|
+
for (let start = CAPABILITY_WINDOW_BYTES - CAPABILITY_OVERLAP_BYTES; start < f.bytes.byteLength - CAPABILITY_OVERLAP_BYTES && declared.size < KNOWN_CAPABILITIES.length; start += CAPABILITY_WINDOW_BYTES) {
|
|
2433
|
+
const window = f.bytes.subarray(start, start + CAPABILITY_WINDOW_BYTES + CAPABILITY_OVERLAP_BYTES);
|
|
2434
|
+
for (const capability of capabilitiesInText(decoder.decode(window))) declared.add(capability);
|
|
2435
|
+
}
|
|
2436
|
+
} catch {
|
|
2437
|
+
capabilitiesIncomplete = true;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
for (const m of text.matchAll(GENEX_GENERATION_URL_RE)) urls.add(m[0]);
|
|
2442
|
+
const capabilities = KNOWN_CAPABILITIES.filter((capability) => declared.has(capability));
|
|
2443
|
+
const dprMatch = text.match(/setPixelRatio\(\s*Math\.min\([^,)]+,\s*([\d.]+)\s*\)/);
|
|
2444
|
+
const markers = {
|
|
2445
|
+
rendererBackend: text.includes("WebGPURenderer") || text.includes("getContext('webgpu')") || text.includes('"webgpu"') ? "webgpu" : "webgl",
|
|
2446
|
+
postStack: /EffectComposer|postprocessing|PostProcessing|RenderPipeline/.test(text),
|
|
2447
|
+
dprCapLiteral: dprMatch?.[1] ? Number(dprMatch[1]) : null,
|
|
2448
|
+
contextLossHandler: text.includes("webglcontextlost") || text.includes("device.lost"),
|
|
2449
|
+
ktx2Loader: text.includes("KTX2Loader") || text.includes(".ktx2"),
|
|
2450
|
+
touchHints: /joystick/i.test(text) || text.includes("touchstart") && text.includes("touch-action"),
|
|
2451
|
+
adaptiveQuality: text.includes("__GENEX_QUALITY__"),
|
|
2452
|
+
modelRungs: text.includes("__GENEX_MODEL_RUNGS__"),
|
|
2453
|
+
voice: capabilities.includes("microphone")
|
|
2454
|
+
};
|
|
2455
|
+
return {
|
|
2456
|
+
images,
|
|
2457
|
+
glbs,
|
|
2458
|
+
videos,
|
|
2459
|
+
audio,
|
|
2460
|
+
genexAssetUrls: [...urls],
|
|
2461
|
+
markers,
|
|
2462
|
+
capabilities,
|
|
2463
|
+
capabilitiesIncomplete,
|
|
2464
|
+
totalBytes,
|
|
2465
|
+
fileCount: files.length
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// ../../packages/mobile-scan/src/estimate.ts
|
|
2470
|
+
var MB = 1024 * 1024;
|
|
2471
|
+
var CSS_W = 430;
|
|
2472
|
+
var CSS_H = 932;
|
|
2473
|
+
var WORST_SKYBOX_BYTES = 8192 * 4096 * 4 * 1.33;
|
|
2474
|
+
var WORST_IMAGE_BYTES = 2048 * 2048 * 4 * 1.33;
|
|
2475
|
+
var KTX2_DISCOUNT = 6;
|
|
2476
|
+
function textureBytes(w, h, ktx2) {
|
|
2477
|
+
const raw = w * h * 4 * 1.33;
|
|
2478
|
+
return ktx2 ? raw / KTX2_DISCOUNT : raw;
|
|
2479
|
+
}
|
|
2480
|
+
function estimateVram(scan, externalDims) {
|
|
2481
|
+
let bundled = 0;
|
|
2482
|
+
let maxDim = 0;
|
|
2483
|
+
for (const img of scan.images) {
|
|
2484
|
+
bundled += textureBytes(img.width, img.height, img.ktx2);
|
|
2485
|
+
maxDim = Math.max(maxDim, img.width, img.height);
|
|
2486
|
+
}
|
|
2487
|
+
let glbTex = 0;
|
|
2488
|
+
let geometry = 0;
|
|
2489
|
+
for (const glb of scan.glbs) {
|
|
2490
|
+
if (!glb.scan) {
|
|
2491
|
+
geometry += glb.bytes;
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
const ktx2 = glb.scan.extensions.some((e) => e.includes("KHR_texture_basisu"));
|
|
2495
|
+
for (const img of glb.scan.images) {
|
|
2496
|
+
glbTex += textureBytes(img.width, img.height, img.ktx2 || ktx2);
|
|
2497
|
+
maxDim = Math.max(maxDim, img.width, img.height);
|
|
2498
|
+
}
|
|
2499
|
+
geometry += Math.max(0, glb.scan.binBytes - glb.scan.images.reduce((s, i) => s + i.bytes, 0));
|
|
2500
|
+
}
|
|
2501
|
+
let external = 0;
|
|
2502
|
+
const unresolved = [];
|
|
2503
|
+
for (const url of scan.genexAssetUrls) {
|
|
2504
|
+
const role = url.split("/").pop() ?? "";
|
|
2505
|
+
if (MOBILE_RUNG_SUFFIX_RE.test(role)) continue;
|
|
2506
|
+
const dims = externalDims.get(url);
|
|
2507
|
+
if (dims) {
|
|
2508
|
+
const ktx2 = dims.ktx2 || dims.ktx2Available === true && scan.markers.ktx2Loader;
|
|
2509
|
+
if (dims.glb) {
|
|
2510
|
+
external += textureBytes(dims.width, dims.height, ktx2) + (dims.bytes ?? 0);
|
|
2511
|
+
maxDim = Math.max(maxDim, dims.width);
|
|
2512
|
+
} else {
|
|
2513
|
+
external += textureBytes(dims.width, dims.height, ktx2);
|
|
2514
|
+
maxDim = Math.max(maxDim, dims.width, dims.height);
|
|
2515
|
+
}
|
|
2516
|
+
} else {
|
|
2517
|
+
unresolved.push(url);
|
|
2518
|
+
external += role === SKYBOX_ROLE ? WORST_SKYBOX_BYTES : WORST_IMAGE_BYTES;
|
|
2519
|
+
maxDim = Math.max(maxDim, role === SKYBOX_ROLE ? 8192 : 2048);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const dpr = Math.min(scan.markers.dprCapLiteral ?? 2, 3);
|
|
2523
|
+
const surface = CSS_W * CSS_H * dpr * dpr * 4;
|
|
2524
|
+
const framebuffers = surface * (3 + (scan.markers.postStack ? 4 : 0));
|
|
2525
|
+
const shadow = 2048 * 2048 * 4;
|
|
2526
|
+
const total = bundled + glbTex + external + framebuffers + shadow + geometry;
|
|
2527
|
+
return {
|
|
2528
|
+
estVramMb: Math.round(total / MB),
|
|
2529
|
+
breakdown: {
|
|
2530
|
+
bundledTexturesMb: Math.round(bundled / MB),
|
|
2531
|
+
glbTexturesMb: Math.round(glbTex / MB),
|
|
2532
|
+
externalTexturesMb: Math.round(external / MB),
|
|
2533
|
+
framebuffersMb: Math.round(framebuffers / MB),
|
|
2534
|
+
shadowDefaultMb: Math.round(shadow / MB),
|
|
2535
|
+
geometryMb: Math.round(geometry / MB)
|
|
2536
|
+
},
|
|
2537
|
+
unresolvedExternals: unresolved,
|
|
2538
|
+
textureMaxDim: maxDim
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
function tierFor(estVramMb) {
|
|
2542
|
+
if (estVramMb < MOBILE_BUDGET_MB.broad) return "mobile-broad";
|
|
2543
|
+
if (estVramMb < MOBILE_BUDGET_MB.modern) return "mobile-modern";
|
|
2544
|
+
return "over-budget";
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2152
2547
|
// src/lib/download-assets.ts
|
|
2153
2548
|
import fs12 from "fs/promises";
|
|
2154
2549
|
import path13 from "path";
|
|
@@ -2345,7 +2740,54 @@ function splitArgs(text) {
|
|
|
2345
2740
|
}
|
|
2346
2741
|
var lineOf = (content, index) => content.slice(0, index).split("\n").length;
|
|
2347
2742
|
function blankComments(raw) {
|
|
2348
|
-
|
|
2743
|
+
const out = [];
|
|
2744
|
+
let kept = 0;
|
|
2745
|
+
const blank = (from, to) => {
|
|
2746
|
+
out.push(raw.slice(kept, from), raw.slice(from, to).replace(/[^\n]/g, " "));
|
|
2747
|
+
kept = to;
|
|
2748
|
+
};
|
|
2749
|
+
const templates = [];
|
|
2750
|
+
let depth = 0;
|
|
2751
|
+
let i = 0;
|
|
2752
|
+
while (i < raw.length) {
|
|
2753
|
+
const ch = raw[i];
|
|
2754
|
+
if (ch === '"' || ch === "'") {
|
|
2755
|
+
i++;
|
|
2756
|
+
while (i < raw.length && raw[i] !== ch && raw[i] !== "\n") i += raw[i] === "\\" ? 2 : 1;
|
|
2757
|
+
i++;
|
|
2758
|
+
} else if (ch === "`" || ch === "}" && templates.at(-1) === depth) {
|
|
2759
|
+
if (ch === "}") templates.pop();
|
|
2760
|
+
i++;
|
|
2761
|
+
while (i < raw.length && raw[i] !== "`" && !(raw[i] === "$" && raw[i + 1] === "{")) {
|
|
2762
|
+
i += raw[i] === "\\" ? 2 : 1;
|
|
2763
|
+
}
|
|
2764
|
+
if (raw[i] === "$") {
|
|
2765
|
+
templates.push(depth);
|
|
2766
|
+
i += 2;
|
|
2767
|
+
} else {
|
|
2768
|
+
i++;
|
|
2769
|
+
}
|
|
2770
|
+
} else if (ch === "{") {
|
|
2771
|
+
depth++;
|
|
2772
|
+
i++;
|
|
2773
|
+
} else if (ch === "}") {
|
|
2774
|
+
depth--;
|
|
2775
|
+
i++;
|
|
2776
|
+
} else if (ch === "/" && raw[i + 1] === "*" && raw.indexOf("*/", i + 2) !== -1) {
|
|
2777
|
+
const end = raw.indexOf("*/", i + 2) + 2;
|
|
2778
|
+
blank(i, end);
|
|
2779
|
+
i = end;
|
|
2780
|
+
} else if (ch === "/" && raw[i + 1] === "/" && raw[i - 1] !== ":") {
|
|
2781
|
+
const eol = raw.indexOf("\n", i);
|
|
2782
|
+
const end = eol === -1 ? raw.length : eol;
|
|
2783
|
+
blank(i, end);
|
|
2784
|
+
i = end;
|
|
2785
|
+
} else {
|
|
2786
|
+
i++;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
out.push(raw.slice(kept));
|
|
2790
|
+
return out.join("");
|
|
2349
2791
|
}
|
|
2350
2792
|
var IDENTITY_AWAIT = /\bawait\s+(waitForPlayer|waitForAuth)\s*\(/;
|
|
2351
2793
|
var RENDERER_CTOR = /\bnew\s+(?:THREE\.)?(?:WebGLRenderer|WebGPURenderer)\s*\(/;
|
|
@@ -2406,8 +2848,10 @@ async function detectEmbedBoot(cwd = process.cwd()) {
|
|
|
2406
2848
|
}
|
|
2407
2849
|
var CONTROL_HEADS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
|
|
2408
2850
|
function isFunctionBrace(tail) {
|
|
2409
|
-
|
|
2851
|
+
let t = tail.trimEnd();
|
|
2410
2852
|
if (t.endsWith("=>")) return true;
|
|
2853
|
+
const typed = /^([\s\S]*\))[ \t]*:[ \t]*[^\s;=?()][^;=?()\n]*$/.exec(t);
|
|
2854
|
+
if (typed) t = typed[1];
|
|
2411
2855
|
if (!t.endsWith(")")) return false;
|
|
2412
2856
|
let depth = 0;
|
|
2413
2857
|
let i = t.length - 1;
|
|
@@ -2618,6 +3062,199 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
2618
3062
|
musicNoSlider: audited.some((e) => e.kind === "music" && e.status === "completed") && !/type\s*=\s*["'`]range["'`]/.test(haystack)
|
|
2619
3063
|
};
|
|
2620
3064
|
}
|
|
3065
|
+
async function readOwnSource(cwd) {
|
|
3066
|
+
const out = [];
|
|
3067
|
+
const page = await fs13.readFile(path14.join(cwd, "index.html"), "utf8").catch(() => "");
|
|
3068
|
+
if (page) {
|
|
3069
|
+
out.push({
|
|
3070
|
+
rel: "index.html",
|
|
3071
|
+
content: page.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " "))
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
const srcDir = path14.join(cwd, "src");
|
|
3075
|
+
let entries;
|
|
3076
|
+
try {
|
|
3077
|
+
entries = await fs13.readdir(srcDir, { recursive: true });
|
|
3078
|
+
} catch {
|
|
3079
|
+
return out;
|
|
3080
|
+
}
|
|
3081
|
+
for (const nativeRel of entries.sort()) {
|
|
3082
|
+
if (nativeRel.includes("node_modules")) continue;
|
|
3083
|
+
if (nativeRel.split(path14.sep)[0] === "controllers") continue;
|
|
3084
|
+
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
|
|
3085
|
+
const raw = await fs13.readFile(path14.join(srcDir, nativeRel), "utf8").catch(() => "");
|
|
3086
|
+
if (!raw) continue;
|
|
3087
|
+
out.push({ rel: nativeRel.split(path14.sep).join("/"), content: blankComments(raw) });
|
|
3088
|
+
}
|
|
3089
|
+
return out;
|
|
3090
|
+
}
|
|
3091
|
+
async function readManifestCapabilities(cwd) {
|
|
3092
|
+
try {
|
|
3093
|
+
const pkg = JSON.parse(await fs13.readFile(path14.join(cwd, "package.json"), "utf8"));
|
|
3094
|
+
const raw = pkg.genex?.capabilities;
|
|
3095
|
+
if (raw === void 0 || raw === null) return { entries: [], notAList: false };
|
|
3096
|
+
return Array.isArray(raw) ? { entries: raw, notAList: false } : { entries: [], notAList: true };
|
|
3097
|
+
} catch {
|
|
3098
|
+
return { entries: [], notAList: false };
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
var SOURCE_MICROPHONE = [
|
|
3102
|
+
/\.voice\s*\??\.\s*join\s*\(\s*(?:\)|\{)/,
|
|
3103
|
+
/\b(?:webkit)?SpeechRecognition\b/
|
|
3104
|
+
];
|
|
3105
|
+
function opensMicrophone(content) {
|
|
3106
|
+
return SOURCE_MICROPHONE.some((evidence) => evidence.test(content)) || mediaRequestsInText(content).some((request) => request.audio);
|
|
3107
|
+
}
|
|
3108
|
+
async function detectCapabilities(cwd = process.cwd()) {
|
|
3109
|
+
const found = [...(await readManifestCapabilities(cwd)).entries];
|
|
3110
|
+
for (const { content } of await readOwnSource(cwd)) {
|
|
3111
|
+
found.push(...capabilitiesInText(content));
|
|
3112
|
+
if (opensMicrophone(content)) found.push("microphone");
|
|
3113
|
+
}
|
|
3114
|
+
return knownCapabilities(found);
|
|
3115
|
+
}
|
|
3116
|
+
var SHARE_CALL = /\bnavigator\s*\??\.\s*share\s*\(/g;
|
|
3117
|
+
var CLIPBOARD_USE = /\bnavigator\s*\??\.\s*clipboard\b/g;
|
|
3118
|
+
var WAKE_LOCK_USE = /\bnavigator\s*\??\.\s*wakeLock\b/g;
|
|
3119
|
+
var NEW_WINDOW = /\bwindow\s*\??\.\s*open\s*\(|["'`]_blank["'`]/g;
|
|
3120
|
+
var DOWNLOAD_ASSIGN = /\.download\s*=(?!=)/g;
|
|
3121
|
+
var DOWNLOAD_ATTR = /<a\b[^>]*\sdownload\b|\bsetAttribute\(\s*["'`]download["'`]/g;
|
|
3122
|
+
var MAKES_A_LINK = /\bcreateElement\(\s*["'`]a["'`]\s*\)|\bcreateObjectURL\b|\btoDataURL\b|\btoBlob\b|<a\b/;
|
|
3123
|
+
var SERVICE_WORKER = /\bserviceWorker\s*\??\.\s*register\s*\(/g;
|
|
3124
|
+
var MOTION_USE = /\b(?:DeviceOrientationEvent|DeviceMotionEvent)\b|["'`]device(?:orientation(?:absolute)?|motion)["'`]|\bondevice(?:orientation(?:absolute)?|motion)\b/g;
|
|
3125
|
+
var HASH_READ = /(?<![\w$.])(?:(?:window|document|globalThis|self)\s*\.\s*)?location\s*\.\s*hash\b(?!\s*(?:=(?!=)|\+=))/g;
|
|
3126
|
+
var DIALOG_CALL = /(?<![\w$.])((?:window|globalThis|self)\s*\.\s*)?(alert|confirm|prompt)\(/g;
|
|
3127
|
+
var MEMBER_HEAD = /^(?:(?:export|declare|abstract|async|public|private|protected|static|readonly|override|get|set)\s+|\*\s*)*$/;
|
|
3128
|
+
function afterCloseParen(content, from) {
|
|
3129
|
+
let depth = 1;
|
|
3130
|
+
let i = from;
|
|
3131
|
+
while (i < content.length && depth > 0) {
|
|
3132
|
+
const ch = content[i];
|
|
3133
|
+
if (ch === "(") depth++;
|
|
3134
|
+
else if (ch === ")") depth--;
|
|
3135
|
+
i++;
|
|
3136
|
+
}
|
|
3137
|
+
return i;
|
|
3138
|
+
}
|
|
3139
|
+
function dialogCalls(content) {
|
|
3140
|
+
const out = [];
|
|
3141
|
+
for (const m of content.matchAll(DIALOG_CALL)) {
|
|
3142
|
+
const index = m.index;
|
|
3143
|
+
const name = m[2];
|
|
3144
|
+
if (!m[1]) {
|
|
3145
|
+
const own = new RegExp(
|
|
3146
|
+
[
|
|
3147
|
+
`\\b(?:function\\*?|const|let|var|class)\\s+${name}\\b`,
|
|
3148
|
+
`\\bimport\\s+${name}\\s+from\\b`,
|
|
3149
|
+
// `{ prompt, toast }`, `(text, confirm)`, `(confirm: () => void)`,
|
|
3150
|
+
// `{ prompt: text }`: a named import, a destructuring, a parameter.
|
|
3151
|
+
`[{,(]\\s*${name}\\s*(?:[,}):]|=(?!=))`
|
|
3152
|
+
].join("|")
|
|
3153
|
+
);
|
|
3154
|
+
if (own.test(content)) continue;
|
|
3155
|
+
const head = content.slice(content.lastIndexOf("\n", index) + 1, index).trimStart();
|
|
3156
|
+
const next = content.slice(afterCloseParen(content, index + m[0].length)).trimStart()[0];
|
|
3157
|
+
if (MEMBER_HEAD.test(head) && (next === "{" || next === ":")) continue;
|
|
3158
|
+
}
|
|
3159
|
+
out.push({ index, name });
|
|
3160
|
+
}
|
|
3161
|
+
return out;
|
|
3162
|
+
}
|
|
3163
|
+
function inBracelessArrow(content, index) {
|
|
3164
|
+
let start = index;
|
|
3165
|
+
while (start > 0 && !";{}".includes(content[start - 1])) start--;
|
|
3166
|
+
const span = content.slice(start, index);
|
|
3167
|
+
const arrow = span.lastIndexOf("=>");
|
|
3168
|
+
if (arrow === -1) return false;
|
|
3169
|
+
let depth = 0;
|
|
3170
|
+
let begun = false;
|
|
3171
|
+
for (const ch of span.slice(arrow + 2)) {
|
|
3172
|
+
if (ch === "(" || ch === "[") depth++;
|
|
3173
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
3174
|
+
if (ch === "\n") {
|
|
3175
|
+
if (begun && depth <= 0) return false;
|
|
3176
|
+
} else if (ch.trim()) {
|
|
3177
|
+
begun = true;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return true;
|
|
3181
|
+
}
|
|
3182
|
+
function hashReadsBeforeInit(content) {
|
|
3183
|
+
const reads = [...content.matchAll(HASH_READ)].map((m) => m.index);
|
|
3184
|
+
if (reads.length === 0) return [];
|
|
3185
|
+
const inits = [...content.matchAll(new RegExp(INIT_EMBED_CALL.source, "g"))].map((m) => ({
|
|
3186
|
+
index: m.index,
|
|
3187
|
+
flowEnd: enclosingFlowEnd(content, m.index)
|
|
3188
|
+
}));
|
|
3189
|
+
let depth = 0;
|
|
3190
|
+
let scanned = 0;
|
|
3191
|
+
return reads.filter((at) => {
|
|
3192
|
+
for (; scanned < at; scanned++) {
|
|
3193
|
+
const ch = content[scanned];
|
|
3194
|
+
if (ch === "{") depth++;
|
|
3195
|
+
else if (ch === "}") depth--;
|
|
3196
|
+
}
|
|
3197
|
+
if (depth <= 0) {
|
|
3198
|
+
return !inBracelessArrow(content, at) && !inits.some((init2) => init2.index < at);
|
|
3199
|
+
}
|
|
3200
|
+
const flowEnd = enclosingFlowEnd(content, at);
|
|
3201
|
+
return flowEnd !== content.length && inits.some((init2) => init2.flowEnd === flowEnd && init2.index > at);
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
async function detectShellApis(cwd = process.cwd()) {
|
|
3205
|
+
const manifest = await readManifestCapabilities(cwd);
|
|
3206
|
+
const found = {
|
|
3207
|
+
share: [],
|
|
3208
|
+
clipboard: [],
|
|
3209
|
+
wakeLock: [],
|
|
3210
|
+
newWindow: [],
|
|
3211
|
+
download: [],
|
|
3212
|
+
dialogs: [],
|
|
3213
|
+
serviceWorker: [],
|
|
3214
|
+
motionNoPermission: [],
|
|
3215
|
+
hashBeforeInit: [],
|
|
3216
|
+
capture: [],
|
|
3217
|
+
manifestCapabilities: knownCapabilities(manifest.entries),
|
|
3218
|
+
unknownCapabilities: manifest.entries.filter((entry) => !KNOWN_CAPABILITIES.includes(entry)).map((entry) => {
|
|
3219
|
+
const quoted = JSON.stringify(entry).replace(
|
|
3220
|
+
/[^\x20-\x7e]/g,
|
|
3221
|
+
(ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
3222
|
+
);
|
|
3223
|
+
return quoted.length > 40 ? `${quoted.slice(0, 40)}\u2026` : quoted;
|
|
3224
|
+
}),
|
|
3225
|
+
capabilitiesNotAList: manifest.notAList
|
|
3226
|
+
};
|
|
3227
|
+
let asksMotionPermission = false;
|
|
3228
|
+
for (const file of await readOwnSource(cwd)) {
|
|
3229
|
+
const { rel, content } = file;
|
|
3230
|
+
const add = (into, index, detail) => {
|
|
3231
|
+
const where = `${rel}:${lineOf(content, index)}`;
|
|
3232
|
+
if (!into.some((d) => d.where === where)) into.push({ where, detail });
|
|
3233
|
+
};
|
|
3234
|
+
for (const m of content.matchAll(SHARE_CALL)) add(found.share, m.index, "navigator.share");
|
|
3235
|
+
for (const m of content.matchAll(CLIPBOARD_USE)) add(found.clipboard, m.index, "navigator.clipboard");
|
|
3236
|
+
for (const m of content.matchAll(WAKE_LOCK_USE)) add(found.wakeLock, m.index, "navigator.wakeLock");
|
|
3237
|
+
for (const m of content.matchAll(NEW_WINDOW)) {
|
|
3238
|
+
add(found.newWindow, m.index, m[0].includes("open") ? "window.open" : 'target "_blank"');
|
|
3239
|
+
}
|
|
3240
|
+
if (MAKES_A_LINK.test(content)) {
|
|
3241
|
+
for (const m of content.matchAll(DOWNLOAD_ASSIGN)) add(found.download, m.index, "a.download");
|
|
3242
|
+
}
|
|
3243
|
+
for (const m of content.matchAll(DOWNLOAD_ATTR)) add(found.download, m.index, "a download link");
|
|
3244
|
+
for (const call of dialogCalls(content)) add(found.dialogs, call.index, `${call.name}()`);
|
|
3245
|
+
for (const m of content.matchAll(SERVICE_WORKER)) {
|
|
3246
|
+
add(found.serviceWorker, m.index, "serviceWorker.register");
|
|
3247
|
+
}
|
|
3248
|
+
for (const m of content.matchAll(MOTION_USE)) add(found.motionNoPermission, m.index, "device motion");
|
|
3249
|
+
if (/\brequestPermission\b/.test(content)) asksMotionPermission = true;
|
|
3250
|
+
for (const at of hashReadsBeforeInit(content)) add(found.hashBeforeInit, at, "location.hash");
|
|
3251
|
+
for (const request of mediaRequestsInText(content)) {
|
|
3252
|
+
if (!request.read) add(found.capture, request.index, "getUserMedia");
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
if (asksMotionPermission) found.motionNoPermission = [];
|
|
3256
|
+
return found;
|
|
3257
|
+
}
|
|
2621
3258
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
2622
3259
|
return {
|
|
2623
3260
|
embedSdkVersion: await detectEmbedSdkVersion(cwd),
|
|
@@ -2627,7 +3264,9 @@ async function detectFeatures(log, cwd = process.cwd()) {
|
|
|
2627
3264
|
mobileControls: await detectMobileControls(cwd),
|
|
2628
3265
|
surfaces: await detectSurfaceScan(cwd),
|
|
2629
3266
|
generations: await detectGenerationAudit(cwd),
|
|
2630
|
-
embedBoot: await detectEmbedBoot(cwd)
|
|
3267
|
+
embedBoot: await detectEmbedBoot(cwd),
|
|
3268
|
+
capabilities: await detectCapabilities(cwd),
|
|
3269
|
+
shellApis: await detectShellApis(cwd)
|
|
2631
3270
|
};
|
|
2632
3271
|
}
|
|
2633
3272
|
function advisoryNudges(log, d) {
|
|
@@ -2641,14 +3280,98 @@ function advisoryNudges(log, d) {
|
|
|
2641
3280
|
"Multiplayer will NOT work: @genex-ai/multiplayer needs the embed-sdk token (getColyseusAuth) \u2014 the relay rejects tokenless joins. Wire genex-threejs-embed-auth first."
|
|
2642
3281
|
);
|
|
2643
3282
|
}
|
|
2644
|
-
if (d.matchmaking && !d.multiplayer) {
|
|
3283
|
+
if (d.matchmaking && !d.multiplayer) {
|
|
3284
|
+
log.warn(
|
|
3285
|
+
"package.json declares genex.matchmaking but @genex-ai/multiplayer is not installed \u2014 matchmaking cannot work. Install the SDK and use matchmake() (see genex-threejs-multiplayer)."
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
embedBootNudges(log, d);
|
|
3289
|
+
shellNudges(log, d);
|
|
3290
|
+
surfaceNudges(log, d.surfaces);
|
|
3291
|
+
generationNudges(log, d.generations);
|
|
3292
|
+
}
|
|
3293
|
+
function places(found, withDetail = false) {
|
|
3294
|
+
const shown = found.slice(0, 4).map((d) => withDetail ? `${d.where} (${d.detail})` : d.where);
|
|
3295
|
+
const rest = found.length - shown.length;
|
|
3296
|
+
return shown.join(", ") + (rest > 0 ? ` (+${rest} more)` : "");
|
|
3297
|
+
}
|
|
3298
|
+
function shellNudges(log, d) {
|
|
3299
|
+
const s = d.shellApis;
|
|
3300
|
+
const helper = "from @genex-ai/embed-sdk";
|
|
3301
|
+
let namedHelper = false;
|
|
3302
|
+
if (s.capabilitiesNotAList) {
|
|
3303
|
+
log.warn(
|
|
3304
|
+
`package.json \`genex.capabilities\` is not a list, so it declares nothing. Write the names in a list: "genex": { "capabilities": ["camera"] } \u2014 the names are ${KNOWN_CAPABILITIES.join(", ")}.`
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
if (s.unknownCapabilities.length) {
|
|
3308
|
+
log.warn(
|
|
3309
|
+
`package.json \`genex.capabilities\` lists ${s.unknownCapabilities.slice(0, 6).join(", ")}, which Genex does not know, so it gives the game nothing. The names are ${KNOWN_CAPABILITIES.join(", ")}, spelled exactly so.`
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
const mediaDeclared = ["microphone", "camera"].filter((name) => d.capabilities.includes(name));
|
|
3313
|
+
if (s.capture.length && mediaDeclared.length < 2 && !s.manifestCapabilities.some((name) => name === "microphone" || name === "camera")) {
|
|
3314
|
+
const declared = mediaDeclared.length ? `only the ${mediaDeclared[0]} is declared for this build` : "nothing in this game declares the microphone or the camera";
|
|
3315
|
+
log.warn(
|
|
3316
|
+
`${places(s.capture)} calls getUserMedia, and ${declared}: the constraints are not written out at the call, so the scan cannot tell which it opens. On the game's Genex page an undeclared microphone or camera is refused without the player being asked, while the same build works on its own link. Say which the game uses in package.json: "genex": { "capabilities": ["microphone"] } (or "camera", or both).`
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
if (s.share.length) {
|
|
3320
|
+
namedHelper = true;
|
|
3321
|
+
log.warn(
|
|
3322
|
+
`${places(s.share)} calls navigator.share. It shares whatever the game hands it, and on the game's Genex page \`location.href\` is the bare game address, never the page players should be sent to; some shells have no share sheet at all. share({ text, params }) ${helper} builds the link to the game's page itself, opens the sheet where there is one and copies the link where there is not. Call it inside the click or tap.`
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
if (s.clipboard.length) {
|
|
3326
|
+
namedHelper = true;
|
|
3327
|
+
log.warn(
|
|
3328
|
+
`${places(s.clipboard)} uses navigator.clipboard. The game's Genex page gives a framed game clipboard writing and never reading, and a write is still refused without a tap behind it, or in a frame on a page that does not pass it on. copyText(text) ${helper} tries it and falls back to the older copy a frame is allowed after a tap; call it inside the click. To read text, let the player paste into an <input>.`
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
if (s.wakeLock.length) {
|
|
3332
|
+
namedHelper = true;
|
|
3333
|
+
log.warn(
|
|
3334
|
+
`${places(s.wakeLock)} uses navigator.wakeLock. The game's Genex page gives it to every framed game, but the browser drops the lock every time the tab is hidden and some browsers have none to give. keepAwake(true) ${helper} takes the lock again when the player comes back, says so where there is none, and answers ok in the Genex app, where the screen already stays on.`
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
if (s.newWindow.length) {
|
|
3338
|
+
namedHelper = true;
|
|
3339
|
+
log.warn(
|
|
3340
|
+
`${places(s.newWindow, true)} opens a new window. The game's Genex page gives a game no new windows and the Genex app blocks links out of a game, so there the click does nothing. openExternal(url) ${helper}, called inside the click, is the one call for every shell: on the game's page Genex asks the player and opens the link, on the game's own link it is a new tab, and where links out are not available it answers \`unsupported\` \u2014 show the address instead.`
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
if (s.download.length) {
|
|
3344
|
+
namedHelper = true;
|
|
3345
|
+
log.warn(
|
|
3346
|
+
`${places(s.download)} saves a file through a download link. The game's Genex page drops a download started inside a game, with no error. saveFile(blob, name) ${helper}, called inside the click, saves an image, a video, JSON or plain text wherever a shell can: on the game's page Genex asks the player first, and where saving is not available it answers \`unsupported\`.`
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
if (s.hashBeforeInit.length && d.embedSdkVersion) {
|
|
3350
|
+
namedHelper = true;
|
|
3351
|
+
log.warn(
|
|
3352
|
+
`${places(s.hashBeforeInit)} reads location.hash where it can run before initEmbed(). On the game's own link the address bar still holds Genex's sign-in fragment at that point, and initEmbed() puts the game's own fragment back without a hashchange event, so that first read is wrong and nothing corrects it. Read the link with getEntry() ${helper}: getEntry().hash and getEntry().params answer the same in every shell, whenever they are asked.`
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
if (namedHelper) {
|
|
3356
|
+
log.dim(
|
|
3357
|
+
" share, copyText, keepAwake, openExternal, saveFile and getEntry ship in the current @genex-ai/embed-sdk. If an import is missing: npm i @genex-ai/embed-sdk@latest"
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
if (s.dialogs.length) {
|
|
3361
|
+
log.warn(
|
|
3362
|
+
`${places(s.dialogs, true)} calls a browser dialog. The game's Genex page shows none for a game: alert() does nothing, confirm() answers false and prompt() answers null, with no error, while the same build works on its own link. Draw the dialog in the game's own interface (genex-threejs-game-ui).`
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
if (s.motionNoPermission.length) {
|
|
3366
|
+
log.warn(
|
|
3367
|
+
`${places(s.motionNoPermission)} uses device tilt or motion, and nothing in the game calls requestPermission(). An iPhone sends no motion events until the player allows them, and it asks only when DeviceOrientationEvent.requestPermission() is called from a tap inside the game. Ask in the Start tap, treat a refusal or no events at all as "no sensor", and keep the on-screen controls as the fallback (genex-threejs-touch-controls).`
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
if (s.serviceWorker.length) {
|
|
2645
3371
|
log.warn(
|
|
2646
|
-
|
|
3372
|
+
`${places(s.serviceWorker)} registers a service worker. A returning player's browser then serves the copy it kept: an older build after an update, or a game that has since been taken down, and no deploy reaches it. A Genex game does not register one.`
|
|
2647
3373
|
);
|
|
2648
3374
|
}
|
|
2649
|
-
embedBootNudges(log, d);
|
|
2650
|
-
surfaceNudges(log, d.surfaces);
|
|
2651
|
-
generationNudges(log, d.generations);
|
|
2652
3375
|
}
|
|
2653
3376
|
function embedBootNudges(log, d) {
|
|
2654
3377
|
const boot = d.embedBoot;
|
|
@@ -2868,403 +3591,142 @@ function auditGenerationPlan(input) {
|
|
|
2868
3591
|
if (design.trim().length === 0) return [];
|
|
2869
3592
|
const assetRows = sectionRows(design, /^##\s+Assets\b/i);
|
|
2870
3593
|
const proceduralRows = sectionRows(design, /^##\s+Procedural assets\b/i);
|
|
2871
|
-
const rows = [
|
|
2872
|
-
...assetRows.map(classify),
|
|
2873
|
-
...proceduralRows.map((c2) => {
|
|
2874
|
-
const r = classify(c2);
|
|
2875
|
-
return r === null ? null : { ...r, lane: null, procedural: true };
|
|
2876
|
-
})
|
|
2877
|
-
].filter((r) => r !== null);
|
|
2878
|
-
const declaredLanes = /* @__PURE__ */ new Set();
|
|
2879
|
-
for (const m of design.matchAll(LANE_TOKEN)) {
|
|
2880
|
-
const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
|
|
2881
|
-
if (lane) declaredLanes.add(lane);
|
|
2882
|
-
}
|
|
2883
|
-
const lanesRun = new Set(
|
|
2884
|
-
ledger.filter((e) => e.status === "completed").map((e) => LANE_OF_KIND[e.kind] ?? e.kind)
|
|
2885
|
-
);
|
|
2886
|
-
const lanesPending = new Set(
|
|
2887
|
-
ledger.filter((e) => e.status === "queued").map((e) => LANE_OF_KIND[e.kind] ?? e.kind).filter((lane) => !lanesRun.has(lane))
|
|
2888
|
-
);
|
|
2889
|
-
const warnings = [];
|
|
2890
|
-
const noneMarker = GENERATION_NONE.exec(design);
|
|
2891
|
-
const explained = noneMarker !== null && isFilledReason(noneMarker[1]);
|
|
2892
|
-
const reconciled = /* @__PURE__ */ new Set();
|
|
2893
|
-
for (const row of rows) {
|
|
2894
|
-
const reason = /\bReason:\s*(.+)/i.exec(row.detail)?.[1];
|
|
2895
|
-
if (row.procedural) {
|
|
2896
|
-
if (/\b(replaced|substitut\w*|fallback|instead of)\b/i.test(row.detail) && !explained && !isFilledReason(reason)) {
|
|
2897
|
-
warnings.push(`DESIGN.md substitutes procedural work for ${row.name} without a recorded reason. Record why this choice fits (including budget limits or declined generation); no purchase is required.`);
|
|
2898
|
-
}
|
|
2899
|
-
continue;
|
|
2900
|
-
}
|
|
2901
|
-
const tokens2 = new Set(row.detail.split(/[^a-zA-Z0-9_-]+/));
|
|
2902
|
-
const explicit = ledger.filter((entry) => tokens2.has(entry.id));
|
|
2903
|
-
const exact = explicit.length ? explicit : ledger.filter((entry) => entry.prompt.trim().toLowerCase() === row.name.toLowerCase() && (LANE_OF_KIND[entry.kind] ?? entry.kind) === row.lane);
|
|
2904
|
-
if (!exact.length) continue;
|
|
2905
|
-
reconciled.add(row);
|
|
2906
|
-
for (const entry of exact) {
|
|
2907
|
-
if (entry.status === "queued") warnings.push(`DESIGN.md asset ${row.name} (${entry.id}) is pending. Check the existing request; do not enqueue a duplicate.`);
|
|
2908
|
-
else if (entry.status === "failed") warnings.push(`DESIGN.md asset ${row.name} (${entry.id}) failed. Record a recovery or substitution decision; budget limits and declined generation are valid reasons.`);
|
|
2909
|
-
else if (input.unreferencedIds?.has(entry.id)) warnings.push(`DESIGN.md asset ${row.name} (${entry.id}) completed but its output is not referenced by the source audit. Check delivery/integration or record why it was omitted. A reference alone does not prove visible use.`);
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
const owed = /* @__PURE__ */ new Map();
|
|
2913
|
-
for (const row of rows) {
|
|
2914
|
-
if (row.procedural || row.lane === null || reconciled.has(row)) continue;
|
|
2915
|
-
if (row.status !== "planned" && row.status !== "generating") continue;
|
|
2916
|
-
if (lanesRun.has(row.lane)) {
|
|
2917
|
-
if (rows.filter((other) => other.lane === row.lane).length > 1) warnings.push(`DESIGN.md asset ${row.name} has no matched generation id. Other ${row.lane} work completed, but that does not establish this asset's delivery. Record its id or the substitution reason.`);
|
|
2918
|
-
continue;
|
|
2919
|
-
}
|
|
2920
|
-
if (lanesPending.has(row.lane)) {
|
|
2921
|
-
warnings.push(`DESIGN.md plans ${row.name}; its ${row.lane} lane has pending work. Check the existing request and record its id; do not enqueue a duplicate.`);
|
|
2922
|
-
continue;
|
|
2923
|
-
}
|
|
2924
|
-
if (row.lane === "skybox") continue;
|
|
2925
|
-
const held = owed.get(row.lane) ?? [];
|
|
2926
|
-
held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
|
|
2927
|
-
owed.set(row.lane, held);
|
|
2928
|
-
}
|
|
2929
|
-
for (const lane of declaredLanes) {
|
|
2930
|
-
if (lanesRun.has(lane) || lanesPending.has(lane) || owed.has(lane) || lane === "skybox") continue;
|
|
2931
|
-
if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
|
|
2932
|
-
}
|
|
2933
|
-
for (const [lane, names2] of owed) {
|
|
2934
|
-
const shown = names2.slice(0, 3).join(", ") + (names2.length > 3 ? `, +${names2.length - 3} more` : "");
|
|
2935
|
-
warnings.push(
|
|
2936
|
-
`DESIGN.md plans generated ${lane} work (${shown}) and this project has no ${lane} generation that landed \u2014 the plan says so and the generation ledger has nothing completed. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
|
|
2937
|
-
);
|
|
2938
|
-
}
|
|
2939
|
-
const paidRows = rows.filter((r) => !r.procedural);
|
|
2940
|
-
const codeRows = rows.filter((r) => r.procedural);
|
|
2941
|
-
if (paidRows.length === 0 && codeRows.length >= 1 && ledger.length === 0 && !explained) {
|
|
2942
|
-
warnings.push(
|
|
2943
|
-
"DESIGN.md's asset plan is all code-built rows and this project has never run a generation. What the player sees must read as the thing it is (build contract, law 22): a coloured box or a capsule is not a finished building, person, or prop. Generate what the request names and what the player looks at up close (npx genex model / texture / music \u2026 --no-wait), or write a `Generation: none \u2014` line beside the table ending in your own sentence about why code alone reaches the bar in THIS game. " + (noneMarker ? "The line is there but its reason is still the unfilled template \u2014 replace it with the actual reason." : "")
|
|
2944
|
-
);
|
|
2945
|
-
}
|
|
2946
|
-
return warnings;
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
// src/lib/deploy.ts
|
|
2950
|
-
import crypto2 from "crypto";
|
|
2951
|
-
import fs17 from "fs/promises";
|
|
2952
|
-
import os5 from "os";
|
|
2953
|
-
import path17 from "path";
|
|
2954
|
-
|
|
2955
|
-
// src/lib/source-size.ts
|
|
2956
|
-
import fs14 from "fs/promises";
|
|
2957
|
-
import path15 from "path";
|
|
2958
|
-
var FALLBACK_SOURCE_LIMITS = {
|
|
2959
|
-
maxTotalBytes: 2 * 1024 * 1024 * 1024,
|
|
2960
|
-
maxFiles: 5e3
|
|
2961
|
-
};
|
|
2962
|
-
function isOversized(p, limits) {
|
|
2963
|
-
return p.bytes >= limits.maxTotalBytes || p.files >= limits.maxFiles;
|
|
2964
|
-
}
|
|
2965
|
-
function formatBytes2(n) {
|
|
2966
|
-
if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(1)} GB`;
|
|
2967
|
-
if (n >= 1024 ** 2) return `${Math.round(n / 1024 ** 2)} MB`;
|
|
2968
|
-
if (n >= 1024) return `${Math.round(n / 1024)} KB`;
|
|
2969
|
-
return `${n} B`;
|
|
2970
|
-
}
|
|
2971
|
-
function groupByDir(entries, limit = 5) {
|
|
2972
|
-
const acc = /* @__PURE__ */ new Map();
|
|
2973
|
-
for (const [file, bytes] of entries) {
|
|
2974
|
-
const dirParts = file.split("/").slice(0, -1).slice(0, 2);
|
|
2975
|
-
const dir = dirParts.length > 0 ? dirParts.join("/") : "(root)";
|
|
2976
|
-
const row = acc.get(dir) ?? { dir, files: 0, bytes: 0 };
|
|
2977
|
-
row.files += 1;
|
|
2978
|
-
row.bytes += bytes;
|
|
2979
|
-
acc.set(dir, row);
|
|
2980
|
-
}
|
|
2981
|
-
return [...acc.values()].sort((a, b) => b.bytes - a.bytes).slice(0, limit);
|
|
2982
|
-
}
|
|
2983
|
-
function oversizedLines(p, limits) {
|
|
2984
|
-
return [
|
|
2985
|
-
`This is ${formatBytes2(p.bytes)} across ${p.files} files, past the ${formatBytes2(limits.maxTotalBytes)} / ${limits.maxFiles} file limit on this account.`,
|
|
2986
|
-
"Heaviest directories:",
|
|
2987
|
-
...p.top.map((d) => ` ${formatBytes2(d.bytes).padStart(8)} ${String(d.files).padStart(6)} files ${d.dir}`),
|
|
2988
|
-
"Screenshots, traces and reports made while building are not part of the game \u2014",
|
|
2989
|
-
"put them in .genex/scratch/ (already ignored), or add the folders above to .gitignore.",
|
|
2990
|
-
"If this really is the game, ask an admin to raise the source limit on your account."
|
|
2991
|
-
];
|
|
2992
|
-
}
|
|
2993
|
-
async function measurePayload(cwd, staged) {
|
|
2994
|
-
const entries = [];
|
|
2995
|
-
let bytes = 0;
|
|
2996
|
-
for (const file of staged) {
|
|
2997
|
-
let size = 0;
|
|
2998
|
-
try {
|
|
2999
|
-
size = (await fs14.stat(path15.join(cwd, file))).size;
|
|
3000
|
-
} catch {
|
|
3001
|
-
size = 0;
|
|
3002
|
-
}
|
|
3003
|
-
bytes += size;
|
|
3004
|
-
entries.push([file, size]);
|
|
3005
|
-
}
|
|
3006
|
-
return { files: staged.length, bytes, top: groupByDir(entries) };
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
// ../../packages/mobile-scan/src/image-dims.ts
|
|
3010
|
-
function u32be(b, o) {
|
|
3011
|
-
return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
|
|
3012
|
-
}
|
|
3013
|
-
function u32le(b, o) {
|
|
3014
|
-
return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0;
|
|
3015
|
-
}
|
|
3016
|
-
function u16be(b, o) {
|
|
3017
|
-
return b[o] << 8 | b[o + 1];
|
|
3018
|
-
}
|
|
3019
|
-
function u16le(b, o) {
|
|
3020
|
-
return b[o] | b[o + 1] << 8;
|
|
3021
|
-
}
|
|
3022
|
-
function isPng(b) {
|
|
3023
|
-
return b.length > 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71 && b[4] === 13 && b[5] === 10 && b[6] === 26 && b[7] === 10;
|
|
3024
|
-
}
|
|
3025
|
-
var KTX2_MAGIC = [171, 75, 84, 88, 32, 50, 48, 187, 13, 10, 26, 10];
|
|
3026
|
-
function parseImageDims(bytes) {
|
|
3027
|
-
if (bytes.length < 30) return null;
|
|
3028
|
-
if (isPng(bytes)) {
|
|
3029
|
-
return { width: u32be(bytes, 16), height: u32be(bytes, 20) };
|
|
3030
|
-
}
|
|
3031
|
-
if (bytes[0] === 255 && bytes[1] === 216) {
|
|
3032
|
-
let i = 2;
|
|
3033
|
-
while (i + 9 < bytes.length) {
|
|
3034
|
-
if (bytes[i] !== 255) {
|
|
3035
|
-
i++;
|
|
3036
|
-
continue;
|
|
3037
|
-
}
|
|
3038
|
-
const marker = bytes[i + 1];
|
|
3039
|
-
if (marker === 255) {
|
|
3040
|
-
i++;
|
|
3041
|
-
continue;
|
|
3042
|
-
}
|
|
3043
|
-
if (marker === 216 || marker >= 208 && marker <= 217) {
|
|
3044
|
-
i += 2;
|
|
3045
|
-
continue;
|
|
3046
|
-
}
|
|
3047
|
-
const isSof = marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207;
|
|
3048
|
-
if (isSof) {
|
|
3049
|
-
return { width: u16be(bytes, i + 7), height: u16be(bytes, i + 5) };
|
|
3050
|
-
}
|
|
3051
|
-
i += 2 + u16be(bytes, i + 2);
|
|
3052
|
-
}
|
|
3053
|
-
return null;
|
|
3054
|
-
}
|
|
3055
|
-
if (bytes[0] === 82 && // RIFF….WEBP
|
|
3056
|
-
bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
3057
|
-
const fourcc = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
|
|
3058
|
-
if (fourcc === "VP8X" && bytes.length >= 30) {
|
|
3059
|
-
const w = 1 + (bytes[24] | bytes[25] << 8 | bytes[26] << 16);
|
|
3060
|
-
const h = 1 + (bytes[27] | bytes[28] << 8 | bytes[29] << 16);
|
|
3061
|
-
return { width: w, height: h };
|
|
3062
|
-
}
|
|
3063
|
-
if (fourcc === "VP8 " && bytes.length >= 30) {
|
|
3064
|
-
return { width: u16le(bytes, 26) & 16383, height: u16le(bytes, 28) & 16383 };
|
|
3065
|
-
}
|
|
3066
|
-
if (fourcc === "VP8L" && bytes.length >= 25 && bytes[20] === 47) {
|
|
3067
|
-
const b0 = bytes[21];
|
|
3068
|
-
const b1 = bytes[22];
|
|
3069
|
-
const b2 = bytes[23];
|
|
3070
|
-
const b3 = bytes[24];
|
|
3071
|
-
const width = 1 + ((b1 & 63) << 8 | b0);
|
|
3072
|
-
const height = 1 + ((b3 & 15) << 10 | b2 << 2 | (b1 & 192) >> 6);
|
|
3073
|
-
return { width, height };
|
|
3074
|
-
}
|
|
3075
|
-
return null;
|
|
3076
|
-
}
|
|
3077
|
-
if (KTX2_MAGIC.every((v, idx) => bytes[idx] === v)) {
|
|
3078
|
-
return { width: u32le(bytes, 20), height: u32le(bytes, 24), ktx2: true };
|
|
3079
|
-
}
|
|
3080
|
-
return null;
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3083
|
-
// ../../packages/mobile-scan/src/glb.ts
|
|
3084
|
-
function u32le2(b, o) {
|
|
3085
|
-
return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0;
|
|
3086
|
-
}
|
|
3087
|
-
function scanGlb(bytes) {
|
|
3088
|
-
if (bytes.length < 20 || u32le2(bytes, 0) !== 1179937895) return null;
|
|
3089
|
-
try {
|
|
3090
|
-
const jsonLen = u32le2(bytes, 12);
|
|
3091
|
-
if (u32le2(bytes, 16) !== 1313821514) return null;
|
|
3092
|
-
const json = JSON.parse(new TextDecoder().decode(bytes.subarray(20, 20 + jsonLen)));
|
|
3093
|
-
let bin = null;
|
|
3094
|
-
const binHeader = 20 + jsonLen;
|
|
3095
|
-
if (binHeader + 8 <= bytes.length && u32le2(bytes, binHeader + 4) === 5130562) {
|
|
3096
|
-
bin = bytes.subarray(binHeader + 8, binHeader + 8 + u32le2(bytes, binHeader));
|
|
3097
|
-
}
|
|
3098
|
-
const images = [];
|
|
3099
|
-
for (const img of json.images ?? []) {
|
|
3100
|
-
if (img.bufferView == null || !bin) continue;
|
|
3101
|
-
const view = json.bufferViews?.[img.bufferView];
|
|
3102
|
-
if (!view?.byteLength) continue;
|
|
3103
|
-
const slice = bin.subarray(view.byteOffset ?? 0, (view.byteOffset ?? 0) + view.byteLength);
|
|
3104
|
-
const dims = parseImageDims(slice);
|
|
3105
|
-
if (dims) images.push({ ...dims, mimeType: img.mimeType, bytes: view.byteLength });
|
|
3106
|
-
}
|
|
3107
|
-
let triangles = 0;
|
|
3108
|
-
for (const mesh of json.meshes ?? []) {
|
|
3109
|
-
for (const prim of mesh.primitives ?? []) {
|
|
3110
|
-
const accessor = prim.indices != null ? json.accessors?.[prim.indices] : json.accessors?.[prim.attributes?.POSITION ?? -1];
|
|
3111
|
-
if (accessor?.count) triangles += Math.floor(accessor.count / 3);
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
return {
|
|
3115
|
-
triangles,
|
|
3116
|
-
binBytes: bin?.byteLength ?? 0,
|
|
3117
|
-
images,
|
|
3118
|
-
extensions: json.extensionsUsed ?? []
|
|
3119
|
-
};
|
|
3120
|
-
} catch {
|
|
3121
|
-
return null;
|
|
3594
|
+
const rows = [
|
|
3595
|
+
...assetRows.map(classify),
|
|
3596
|
+
...proceduralRows.map((c2) => {
|
|
3597
|
+
const r = classify(c2);
|
|
3598
|
+
return r === null ? null : { ...r, lane: null, procedural: true };
|
|
3599
|
+
})
|
|
3600
|
+
].filter((r) => r !== null);
|
|
3601
|
+
const declaredLanes = /* @__PURE__ */ new Set();
|
|
3602
|
+
for (const m of design.matchAll(LANE_TOKEN)) {
|
|
3603
|
+
const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
|
|
3604
|
+
if (lane) declaredLanes.add(lane);
|
|
3122
3605
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
const audio = [];
|
|
3140
|
-
const urls = /* @__PURE__ */ new Set();
|
|
3141
|
-
let text = "";
|
|
3142
|
-
let totalBytes = 0;
|
|
3143
|
-
for (const f of files) {
|
|
3144
|
-
totalBytes += f.bytes.byteLength;
|
|
3145
|
-
if (IMAGE_EXT.test(f.relPath)) {
|
|
3146
|
-
const dims = parseImageDims(f.bytes);
|
|
3147
|
-
if (dims) images.push({ path: f.relPath, bytes: f.bytes.byteLength, ...dims });
|
|
3606
|
+
const lanesRun = new Set(
|
|
3607
|
+
ledger.filter((e) => e.status === "completed").map((e) => LANE_OF_KIND[e.kind] ?? e.kind)
|
|
3608
|
+
);
|
|
3609
|
+
const lanesPending = new Set(
|
|
3610
|
+
ledger.filter((e) => e.status === "queued").map((e) => LANE_OF_KIND[e.kind] ?? e.kind).filter((lane) => !lanesRun.has(lane))
|
|
3611
|
+
);
|
|
3612
|
+
const warnings = [];
|
|
3613
|
+
const noneMarker = GENERATION_NONE.exec(design);
|
|
3614
|
+
const explained = noneMarker !== null && isFilledReason(noneMarker[1]);
|
|
3615
|
+
const reconciled = /* @__PURE__ */ new Set();
|
|
3616
|
+
for (const row of rows) {
|
|
3617
|
+
const reason = /\bReason:\s*(.+)/i.exec(row.detail)?.[1];
|
|
3618
|
+
if (row.procedural) {
|
|
3619
|
+
if (/\b(replaced|substitut\w*|fallback|instead of)\b/i.test(row.detail) && !explained && !isFilledReason(reason)) {
|
|
3620
|
+
warnings.push(`DESIGN.md substitutes procedural work for ${row.name} without a recorded reason. Record why this choice fits (including budget limits or declined generation); no purchase is required.`);
|
|
3621
|
+
}
|
|
3148
3622
|
continue;
|
|
3149
3623
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3624
|
+
const tokens2 = new Set(row.detail.split(/[^a-zA-Z0-9_-]+/));
|
|
3625
|
+
const explicit = ledger.filter((entry) => tokens2.has(entry.id));
|
|
3626
|
+
const exact = explicit.length ? explicit : ledger.filter((entry) => entry.prompt.trim().toLowerCase() === row.name.toLowerCase() && (LANE_OF_KIND[entry.kind] ?? entry.kind) === row.lane);
|
|
3627
|
+
if (!exact.length) continue;
|
|
3628
|
+
reconciled.add(row);
|
|
3629
|
+
for (const entry of exact) {
|
|
3630
|
+
if (entry.status === "queued") warnings.push(`DESIGN.md asset ${row.name} (${entry.id}) is pending. Check the existing request; do not enqueue a duplicate.`);
|
|
3631
|
+
else if (entry.status === "failed") warnings.push(`DESIGN.md asset ${row.name} (${entry.id}) failed. Record a recovery or substitution decision; budget limits and declined generation are valid reasons.`);
|
|
3632
|
+
else if (input.unreferencedIds?.has(entry.id)) warnings.push(`DESIGN.md asset ${row.name} (${entry.id}) completed but its output is not referenced by the source audit. Check delivery/integration or record why it was omitted. A reference alone does not prove visible use.`);
|
|
3153
3633
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3634
|
+
}
|
|
3635
|
+
const owed = /* @__PURE__ */ new Map();
|
|
3636
|
+
for (const row of rows) {
|
|
3637
|
+
if (row.procedural || row.lane === null || reconciled.has(row)) continue;
|
|
3638
|
+
if (row.status !== "planned" && row.status !== "generating") continue;
|
|
3639
|
+
if (lanesRun.has(row.lane)) {
|
|
3640
|
+
if (rows.filter((other) => other.lane === row.lane).length > 1) warnings.push(`DESIGN.md asset ${row.name} has no matched generation id. Other ${row.lane} work completed, but that does not establish this asset's delivery. Record its id or the substitution reason.`);
|
|
3156
3641
|
continue;
|
|
3157
3642
|
}
|
|
3158
|
-
if (
|
|
3159
|
-
|
|
3643
|
+
if (lanesPending.has(row.lane)) {
|
|
3644
|
+
warnings.push(`DESIGN.md plans ${row.name}; its ${row.lane} lane has pending work. Check the existing request and record its id; do not enqueue a duplicate.`);
|
|
3160
3645
|
continue;
|
|
3161
3646
|
}
|
|
3162
|
-
if (
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3647
|
+
if (row.lane === "skybox") continue;
|
|
3648
|
+
const held = owed.get(row.lane) ?? [];
|
|
3649
|
+
held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
|
|
3650
|
+
owed.set(row.lane, held);
|
|
3166
3651
|
}
|
|
3167
|
-
for (const
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
genexAssetUrls: [...urls],
|
|
3186
|
-
markers,
|
|
3187
|
-
totalBytes,
|
|
3188
|
-
fileCount: files.length
|
|
3189
|
-
};
|
|
3652
|
+
for (const lane of declaredLanes) {
|
|
3653
|
+
if (lanesRun.has(lane) || lanesPending.has(lane) || owed.has(lane) || lane === "skybox") continue;
|
|
3654
|
+
if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
|
|
3655
|
+
}
|
|
3656
|
+
for (const [lane, names2] of owed) {
|
|
3657
|
+
const shown = names2.slice(0, 3).join(", ") + (names2.length > 3 ? `, +${names2.length - 3} more` : "");
|
|
3658
|
+
warnings.push(
|
|
3659
|
+
`DESIGN.md plans generated ${lane} work (${shown}) and this project has no ${lane} generation that landed \u2014 the plan says so and the generation ledger has nothing completed. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
const paidRows = rows.filter((r) => !r.procedural);
|
|
3663
|
+
const codeRows = rows.filter((r) => r.procedural);
|
|
3664
|
+
if (paidRows.length === 0 && codeRows.length >= 1 && ledger.length === 0 && !explained) {
|
|
3665
|
+
warnings.push(
|
|
3666
|
+
"DESIGN.md's asset plan is all code-built rows and this project has never run a generation. What the player sees must read as the thing it is (build contract, law 22): a coloured box or a capsule is not a finished building, person, or prop. Generate what the request names and what the player looks at up close (npx genex model / texture / music \u2026 --no-wait), or write a `Generation: none \u2014` line beside the table ending in your own sentence about why code alone reaches the bar in THIS game. " + (noneMarker ? "The line is there but its reason is still the unfilled template \u2014 replace it with the actual reason." : "")
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
return warnings;
|
|
3190
3670
|
}
|
|
3191
3671
|
|
|
3192
|
-
//
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3672
|
+
// src/lib/deploy.ts
|
|
3673
|
+
import crypto2 from "crypto";
|
|
3674
|
+
import fs17 from "fs/promises";
|
|
3675
|
+
import os5 from "os";
|
|
3676
|
+
import path17 from "path";
|
|
3677
|
+
|
|
3678
|
+
// src/lib/source-size.ts
|
|
3679
|
+
import fs14 from "fs/promises";
|
|
3680
|
+
import path15 from "path";
|
|
3681
|
+
var FALLBACK_SOURCE_LIMITS = {
|
|
3682
|
+
maxTotalBytes: 2 * 1024 * 1024 * 1024,
|
|
3683
|
+
maxFiles: 5e3
|
|
3684
|
+
};
|
|
3685
|
+
function isOversized(p, limits) {
|
|
3686
|
+
return p.bytes >= limits.maxTotalBytes || p.files >= limits.maxFiles;
|
|
3202
3687
|
}
|
|
3203
|
-
function
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
for (const img of glb.scan.images) {
|
|
3219
|
-
glbTex += textureBytes(img.width, img.height, img.ktx2 || ktx2);
|
|
3220
|
-
maxDim = Math.max(maxDim, img.width, img.height);
|
|
3221
|
-
}
|
|
3222
|
-
geometry += Math.max(0, glb.scan.binBytes - glb.scan.images.reduce((s, i) => s + i.bytes, 0));
|
|
3688
|
+
function formatBytes2(n) {
|
|
3689
|
+
if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(1)} GB`;
|
|
3690
|
+
if (n >= 1024 ** 2) return `${Math.round(n / 1024 ** 2)} MB`;
|
|
3691
|
+
if (n >= 1024) return `${Math.round(n / 1024)} KB`;
|
|
3692
|
+
return `${n} B`;
|
|
3693
|
+
}
|
|
3694
|
+
function groupByDir(entries, limit = 5) {
|
|
3695
|
+
const acc = /* @__PURE__ */ new Map();
|
|
3696
|
+
for (const [file, bytes] of entries) {
|
|
3697
|
+
const dirParts = file.split("/").slice(0, -1).slice(0, 2);
|
|
3698
|
+
const dir = dirParts.length > 0 ? dirParts.join("/") : "(root)";
|
|
3699
|
+
const row = acc.get(dir) ?? { dir, files: 0, bytes: 0 };
|
|
3700
|
+
row.files += 1;
|
|
3701
|
+
row.bytes += bytes;
|
|
3702
|
+
acc.set(dir, row);
|
|
3223
3703
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3704
|
+
return [...acc.values()].sort((a, b) => b.bytes - a.bytes).slice(0, limit);
|
|
3705
|
+
}
|
|
3706
|
+
function oversizedLines(p, limits) {
|
|
3707
|
+
return [
|
|
3708
|
+
`This is ${formatBytes2(p.bytes)} across ${p.files} files, past the ${formatBytes2(limits.maxTotalBytes)} / ${limits.maxFiles} file limit on this account.`,
|
|
3709
|
+
"Heaviest directories:",
|
|
3710
|
+
...p.top.map((d) => ` ${formatBytes2(d.bytes).padStart(8)} ${String(d.files).padStart(6)} files ${d.dir}`),
|
|
3711
|
+
"Screenshots, traces and reports made while building are not part of the game \u2014",
|
|
3712
|
+
"put them in .genex/scratch/ (already ignored), or add the folders above to .gitignore.",
|
|
3713
|
+
"If this really is the game, ask an admin to raise the source limit on your account."
|
|
3714
|
+
];
|
|
3715
|
+
}
|
|
3716
|
+
async function measurePayload(cwd, staged) {
|
|
3717
|
+
const entries = [];
|
|
3718
|
+
let bytes = 0;
|
|
3719
|
+
for (const file of staged) {
|
|
3720
|
+
let size = 0;
|
|
3721
|
+
try {
|
|
3722
|
+
size = (await fs14.stat(path15.join(cwd, file))).size;
|
|
3723
|
+
} catch {
|
|
3724
|
+
size = 0;
|
|
3243
3725
|
}
|
|
3726
|
+
bytes += size;
|
|
3727
|
+
entries.push([file, size]);
|
|
3244
3728
|
}
|
|
3245
|
-
|
|
3246
|
-
const surface = CSS_W * CSS_H * dpr * dpr * 4;
|
|
3247
|
-
const framebuffers = surface * (3 + (scan.markers.postStack ? 4 : 0));
|
|
3248
|
-
const shadow = 2048 * 2048 * 4;
|
|
3249
|
-
const total = bundled + glbTex + external + framebuffers + shadow + geometry;
|
|
3250
|
-
return {
|
|
3251
|
-
estVramMb: Math.round(total / MB),
|
|
3252
|
-
breakdown: {
|
|
3253
|
-
bundledTexturesMb: Math.round(bundled / MB),
|
|
3254
|
-
glbTexturesMb: Math.round(glbTex / MB),
|
|
3255
|
-
externalTexturesMb: Math.round(external / MB),
|
|
3256
|
-
framebuffersMb: Math.round(framebuffers / MB),
|
|
3257
|
-
shadowDefaultMb: Math.round(shadow / MB),
|
|
3258
|
-
geometryMb: Math.round(geometry / MB)
|
|
3259
|
-
},
|
|
3260
|
-
unresolvedExternals: unresolved,
|
|
3261
|
-
textureMaxDim: maxDim
|
|
3262
|
-
};
|
|
3263
|
-
}
|
|
3264
|
-
function tierFor(estVramMb) {
|
|
3265
|
-
if (estVramMb < MOBILE_BUDGET_MB.broad) return "mobile-broad";
|
|
3266
|
-
if (estVramMb < MOBILE_BUDGET_MB.modern) return "mobile-modern";
|
|
3267
|
-
return "over-budget";
|
|
3729
|
+
return { files: staged.length, bytes, top: groupByDir(entries) };
|
|
3268
3730
|
}
|
|
3269
3731
|
|
|
3270
3732
|
// src/commands/ui.ts
|
|
@@ -5093,9 +5555,25 @@ async function uiAudit(opts, log) {
|
|
|
5093
5555
|
}
|
|
5094
5556
|
|
|
5095
5557
|
// src/lib/deploy.ts
|
|
5558
|
+
function goLiveCapabilities(declared, scanned) {
|
|
5559
|
+
const own = knownCapabilities(declared ?? []);
|
|
5560
|
+
if (scanned === void 0) return own.length > 0 ? own : void 0;
|
|
5561
|
+
return knownCapabilities([...own, ...scanned]);
|
|
5562
|
+
}
|
|
5563
|
+
function printCapabilityPreflight(capabilities, log) {
|
|
5564
|
+
if (capabilities === void 0) return;
|
|
5565
|
+
if (capabilities.length > 0) {
|
|
5566
|
+
log.info(`On its Genex page this build is given: ${capabilities.join(", ")}.`);
|
|
5567
|
+
} else {
|
|
5568
|
+
log.dim(
|
|
5569
|
+
` On its Genex page this build is given nothing that needs a permission (${KNOWN_CAPABILITIES.join(", ")}).`
|
|
5570
|
+
);
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5096
5573
|
function printMobilePreflight(files, log) {
|
|
5574
|
+
let scan = null;
|
|
5097
5575
|
try {
|
|
5098
|
-
|
|
5576
|
+
scan = scanBundle(files.map((f) => ({ relPath: f.relPath, bytes: f.bytes })));
|
|
5099
5577
|
const externalDims = /* @__PURE__ */ new Map();
|
|
5100
5578
|
for (const url of scan.genexAssetUrls) {
|
|
5101
5579
|
const role = url.split("/").pop() ?? "";
|
|
@@ -5112,7 +5590,7 @@ function printMobilePreflight(files, log) {
|
|
|
5112
5590
|
const line = `Mobile preflight: ~${est.estVramMb} MB est. GPU memory on phones (budget: <${MOBILE_BUDGET_MB.broad} MB broad, <${MOBILE_BUDGET_MB.modern} MB modern) \u2014 ${tier}.`;
|
|
5113
5591
|
if (tier === "mobile-broad") {
|
|
5114
5592
|
log.dim(` ${line}`);
|
|
5115
|
-
return;
|
|
5593
|
+
return scan;
|
|
5116
5594
|
}
|
|
5117
5595
|
log.warn(line);
|
|
5118
5596
|
for (const url of est.unresolvedExternals) {
|
|
@@ -5140,6 +5618,7 @@ function printMobilePreflight(files, log) {
|
|
|
5140
5618
|
}
|
|
5141
5619
|
} catch {
|
|
5142
5620
|
}
|
|
5621
|
+
return scan;
|
|
5143
5622
|
}
|
|
5144
5623
|
async function printUiAuditPreflight(log) {
|
|
5145
5624
|
try {
|
|
@@ -5246,7 +5725,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
5246
5725
|
if (lfsPointers.length > 8) log.dim(` \u2026and ${lfsPointers.length - 8} more`);
|
|
5247
5726
|
return false;
|
|
5248
5727
|
}
|
|
5249
|
-
printMobilePreflight(files, log);
|
|
5728
|
+
const scan = printMobilePreflight(files, log);
|
|
5729
|
+
const capabilities = scan?.capabilitiesIncomplete ? goLiveCapabilities([...opts.capabilities ?? [], ...scan.capabilities], void 0) : goLiveCapabilities(opts.capabilities, scan?.capabilities);
|
|
5730
|
+
printCapabilityPreflight(capabilities, log);
|
|
5250
5731
|
await printUiAuditPreflight(log);
|
|
5251
5732
|
if (await workspaceMode(cwd) === "game") await printPipelineStatePreflight(log);
|
|
5252
5733
|
const commit = contentCommit(files);
|
|
@@ -5293,7 +5774,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
5293
5774
|
);
|
|
5294
5775
|
if (pushed !== true) return false;
|
|
5295
5776
|
log.step("Publishing\u2026");
|
|
5296
|
-
const published = await callPublish(ctx, commit, opts, log, sourceCommitSha);
|
|
5777
|
+
const published = await callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities);
|
|
5297
5778
|
if (!published) return false;
|
|
5298
5779
|
if (opts.channel === "staging") {
|
|
5299
5780
|
const tree = await sourceTreeHash(cwd);
|
|
@@ -5515,7 +5996,7 @@ async function uploadPartWithRetry(base, encoded, auth, uploadId, partNumber, by
|
|
|
5515
5996
|
await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 500 : 2e3));
|
|
5516
5997
|
}
|
|
5517
5998
|
}
|
|
5518
|
-
async function callPublish(ctx, commit, opts, log, sourceCommitSha) {
|
|
5999
|
+
async function callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities) {
|
|
5519
6000
|
let res;
|
|
5520
6001
|
try {
|
|
5521
6002
|
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/publish`, {
|
|
@@ -5531,6 +6012,11 @@ async function callPublish(ctx, commit, opts, log, sourceCommitSha) {
|
|
|
5531
6012
|
...opts.embedSdkVersion ? { embedSdkVersion: opts.embedSdkVersion } : {},
|
|
5532
6013
|
...opts.multiplayer !== void 0 ? { multiplayer: opts.multiplayer } : {},
|
|
5533
6014
|
...opts.mobileControls !== void 0 ? { mobileControls: opts.mobileControls } : {},
|
|
6015
|
+
// The build's declared capabilities, on both channels: a preview's list
|
|
6016
|
+
// is what the owner's own preview frame is given, and a promotion
|
|
6017
|
+
// carries it to production with the build. An API from before the
|
|
6018
|
+
// field strips it, so the two can deploy in either order.
|
|
6019
|
+
...capabilities !== void 0 ? { capabilities } : {},
|
|
5534
6020
|
// Recorded into the release history and acted on by nothing. Absent when
|
|
5535
6021
|
// the source push produced no commit (a user's own repo, or a failure
|
|
5536
6022
|
// that already stopped the deploy), which the server accepts as null.
|
|
@@ -6147,6 +6633,9 @@ async function runPreview(opts) {
|
|
|
6147
6633
|
// declared preset doesn't silently run the default (null clears a removed
|
|
6148
6634
|
// config), embedSdkVersion/multiplayer so the dashboard's Publish button can
|
|
6149
6635
|
// list the draft without wrongly treating it as a pre-embed-auth bundle.
|
|
6636
|
+
// Capabilities ride the preview for a different reason: the staging build's
|
|
6637
|
+
// own list is what the owner's preview frame is given, so a microphone or
|
|
6638
|
+
// camera can be tried before anything is published.
|
|
6150
6639
|
{
|
|
6151
6640
|
// Preview goes to the STAGING channel — the whole point of the release
|
|
6152
6641
|
// cycle. Before channels this same call replaced the build every player
|
|
@@ -6157,7 +6646,8 @@ async function runPreview(opts) {
|
|
|
6157
6646
|
matchmaking: detections.matchmaking,
|
|
6158
6647
|
embedSdkVersion: detections.embedSdkVersion,
|
|
6159
6648
|
multiplayer: detections.multiplayer,
|
|
6160
|
-
mobileControls: detections.mobileControls
|
|
6649
|
+
mobileControls: detections.mobileControls,
|
|
6650
|
+
capabilities: detections.capabilities
|
|
6161
6651
|
},
|
|
6162
6652
|
log
|
|
6163
6653
|
);
|
|
@@ -7968,7 +8458,10 @@ async function runPublish(opts) {
|
|
|
7968
8458
|
matchmaking: detections.matchmaking,
|
|
7969
8459
|
embedSdkVersion: detections.embedSdkVersion,
|
|
7970
8460
|
multiplayer: detections.multiplayer,
|
|
7971
|
-
mobileControls: detections.mobileControls
|
|
8461
|
+
mobileControls: detections.mobileControls,
|
|
8462
|
+
// Declared on the staging go-live; the promotion below copies that
|
|
8463
|
+
// build's own list to production with it.
|
|
8464
|
+
capabilities: detections.capabilities
|
|
7972
8465
|
},
|
|
7973
8466
|
log
|
|
7974
8467
|
);
|