@genex-ai/cli-demo 1.20.0-dev.602 → 1.21.0-dev.604
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/{blender-mcp-FQJFZETR.js → blender-mcp-PWOUSXK4.js} +1 -1
- package/dist/{chunk-YSMZVFJM.js → chunk-HEDRWUVP.js} +7 -0
- package/dist/index.js +58 -14
- package/package.json +1 -1
- package/templates/controllers/quality/deadline.ts +117 -0
- package/templates/controllers/quality/pick-asset.ts +27 -15
- package/templates/skills/genex-game-director/SKILL.md +11 -5
- package/templates/skills/genex-lane-card/SKILL.md +60 -0
|
@@ -447,6 +447,13 @@ async function apiFetch(url, init = {}, opts = {}) {
|
|
|
447
447
|
if (body?.error === "generation_paused") {
|
|
448
448
|
process.stderr.write(
|
|
449
449
|
`${c.red("\u2717")} ${body.message ?? "Generation is temporarily paused platform-wide. Try again later."}
|
|
450
|
+
`
|
|
451
|
+
);
|
|
452
|
+
structuredPrinted.add(res);
|
|
453
|
+
}
|
|
454
|
+
if (body?.error === "provider_unavailable") {
|
|
455
|
+
process.stderr.write(
|
|
456
|
+
`${c.red("\u2717")} ${body.message ?? "This generation lane is unavailable \u2014 our provider account is out of credit. Build it in code and move on."}
|
|
450
457
|
`
|
|
451
458
|
);
|
|
452
459
|
structuredPrinted.add(res);
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
writeProject,
|
|
43
43
|
writeUserToken,
|
|
44
44
|
writeWorkspace
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-HEDRWUVP.js";
|
|
46
46
|
import {
|
|
47
47
|
CLI_CHANNEL,
|
|
48
48
|
DEFAULT_API_URL,
|
|
@@ -2397,6 +2397,16 @@ async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
|
2397
2397
|
async function countFailed(kind, cwd = process.cwd()) {
|
|
2398
2398
|
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
2399
2399
|
}
|
|
2400
|
+
async function trailingFailures(kind, cwd = process.cwd()) {
|
|
2401
|
+
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
2402
|
+
let run2 = 0;
|
|
2403
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
2404
|
+
const status = rows[i].status;
|
|
2405
|
+
if (status === "completed") break;
|
|
2406
|
+
if (status === "failed") run2 += 1;
|
|
2407
|
+
}
|
|
2408
|
+
return run2;
|
|
2409
|
+
}
|
|
2400
2410
|
async function countOutcomes(kind, cwd = process.cwd()) {
|
|
2401
2411
|
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
2402
2412
|
return {
|
|
@@ -2516,26 +2526,32 @@ function auditGenerationPlan(input) {
|
|
|
2516
2526
|
const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
|
|
2517
2527
|
if (lane) declaredLanes.add(lane);
|
|
2518
2528
|
}
|
|
2519
|
-
const lanesRun = new Set(
|
|
2529
|
+
const lanesRun = new Set(
|
|
2530
|
+
ledger.filter((e) => e.status === "completed").map((e) => LANE_OF_KIND[e.kind] ?? e.kind)
|
|
2531
|
+
);
|
|
2532
|
+
const lanesPending = new Set(
|
|
2533
|
+
ledger.filter((e) => e.status === "queued").map((e) => LANE_OF_KIND[e.kind] ?? e.kind).filter((lane) => !lanesRun.has(lane))
|
|
2534
|
+
);
|
|
2520
2535
|
const warnings = [];
|
|
2521
2536
|
const owed = /* @__PURE__ */ new Map();
|
|
2522
2537
|
for (const row of rows) {
|
|
2523
2538
|
if (row.procedural || row.lane === null) continue;
|
|
2524
2539
|
if (row.status !== "planned" && row.status !== "generating") continue;
|
|
2525
2540
|
if (lanesRun.has(row.lane)) continue;
|
|
2541
|
+
if (lanesPending.has(row.lane)) continue;
|
|
2526
2542
|
if (row.lane === "skybox") continue;
|
|
2527
2543
|
const held = owed.get(row.lane) ?? [];
|
|
2528
2544
|
held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
|
|
2529
2545
|
owed.set(row.lane, held);
|
|
2530
2546
|
}
|
|
2531
2547
|
for (const lane of declaredLanes) {
|
|
2532
|
-
if (lanesRun.has(lane) || owed.has(lane) || lane === "skybox") continue;
|
|
2548
|
+
if (lanesRun.has(lane) || lanesPending.has(lane) || owed.has(lane) || lane === "skybox") continue;
|
|
2533
2549
|
if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
|
|
2534
2550
|
}
|
|
2535
2551
|
for (const [lane, names] of owed) {
|
|
2536
2552
|
const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? `, +${names.length - 3} more` : "");
|
|
2537
2553
|
warnings.push(
|
|
2538
|
-
`DESIGN.md plans generated ${lane} work (${shown}) and this project has
|
|
2554
|
+
`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.`
|
|
2539
2555
|
);
|
|
2540
2556
|
}
|
|
2541
2557
|
const paidRows = rows.filter((r) => !r.procedural);
|
|
@@ -6056,7 +6072,18 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
6056
6072
|
}
|
|
6057
6073
|
return found;
|
|
6058
6074
|
}
|
|
6059
|
-
var WIRED_BY_URL = /* @__PURE__ */ new Set([
|
|
6075
|
+
var WIRED_BY_URL = /* @__PURE__ */ new Set([
|
|
6076
|
+
"model",
|
|
6077
|
+
"model_segment",
|
|
6078
|
+
"model_rig",
|
|
6079
|
+
"model_animation",
|
|
6080
|
+
"skybox",
|
|
6081
|
+
"sfx",
|
|
6082
|
+
"music",
|
|
6083
|
+
"voice",
|
|
6084
|
+
"texture",
|
|
6085
|
+
"video"
|
|
6086
|
+
]);
|
|
6060
6087
|
var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
6061
6088
|
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
6062
6089
|
const ledger = await readLedger(cwd);
|
|
@@ -6067,7 +6094,10 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
6067
6094
|
let haystack = "";
|
|
6068
6095
|
const read = async (file) => {
|
|
6069
6096
|
try {
|
|
6070
|
-
|
|
6097
|
+
const raw = await fs15.readFile(file, "utf8");
|
|
6098
|
+
const ext = path14.extname(file).toLowerCase();
|
|
6099
|
+
if (ext === ".txt") return;
|
|
6100
|
+
haystack += ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".css" ? blankComments(raw) : ext === ".html" ? raw.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " ")) : raw;
|
|
6071
6101
|
} catch {
|
|
6072
6102
|
}
|
|
6073
6103
|
};
|
|
@@ -7566,7 +7596,10 @@ async function reportTerminal(kind, view, log, open = false, json = false, local
|
|
|
7566
7596
|
const failures = await countFailed("video");
|
|
7567
7597
|
(failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
|
|
7568
7598
|
} else if (!failure?.billing) {
|
|
7569
|
-
const advice = laneFailureAdvice(kind,
|
|
7599
|
+
const advice = laneFailureAdvice(kind, {
|
|
7600
|
+
...await countOutcomes(kind),
|
|
7601
|
+
trailingFailed: await trailingFailures(kind)
|
|
7602
|
+
});
|
|
7570
7603
|
if (advice) log.plain(advice);
|
|
7571
7604
|
}
|
|
7572
7605
|
}
|
|
@@ -7660,8 +7693,11 @@ function reportLocalFiles(result, log, json) {
|
|
|
7660
7693
|
}
|
|
7661
7694
|
}
|
|
7662
7695
|
function laneFailureAdvice(kind, outcomes) {
|
|
7663
|
-
|
|
7664
|
-
|
|
7696
|
+
const run2 = outcomes.trailingFailed ?? (outcomes.completed > 0 ? 0 : outcomes.failed);
|
|
7697
|
+
if (run2 < 2) return null;
|
|
7698
|
+
const everSucceeded = outcomes.completed > 0;
|
|
7699
|
+
const opening = everSucceeded ? `\u270B the last ${run2} ${kind} generations in this project all failed.` : `\u270B ${run2} ${kind} generations attempted in this project, ${run2} failed, none succeeded.`;
|
|
7700
|
+
return ` ${c.bold(opening)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
|
|
7665
7701
|
}
|
|
7666
7702
|
function videoFailureAdvice(failures) {
|
|
7667
7703
|
if (failures >= 2) {
|
|
@@ -7796,7 +7832,14 @@ var HINT_URL_KINDS = /* @__PURE__ */ new Set([
|
|
|
7796
7832
|
]);
|
|
7797
7833
|
function assetHint(kind, view, url) {
|
|
7798
7834
|
const hint = {
|
|
7799
|
-
|
|
7835
|
+
// "straight from the URL" was the OPPOSITE of the kit's own rule, which
|
|
7836
|
+
// reads: "EVERY tier loads a game-ready rung — provider-raw originals are
|
|
7837
|
+
// archival/remix source, not game assets" (controllers/quality/pick-asset.ts).
|
|
7838
|
+
// A bare GLTFLoader on this URL fetches the original — a Tripo prop is
|
|
7839
|
+
// ~500k tris and 3x4096 textures, and a scene of them floored an M4 Max.
|
|
7840
|
+
// The url clause stays: the BARE url is the right thing to store, because
|
|
7841
|
+
// `pickModel` resolves the tier's rung at load time.
|
|
7842
|
+
model: `Standard GLB. Load it through the quality kit \u2014 createGltfLoader(renderer) then loadModelWithFallback(url, tier, (u) => gltf.loader.loadAsync(u), { ktx2: gltf.ktx2 }) \u2014 so the tier gets a game-ready rung; a bare GLTFLoader on this URL fetches the provider-raw original (~500k tris, 3x4096 textures). Install it with npx genex controller quality. See genex-ai-model.`,
|
|
7800
7843
|
// Unreachable while the lane is paused (see src/index.ts) — kept so a
|
|
7801
7844
|
// restore is one deletion. Games that already ship a panorama still load
|
|
7802
7845
|
// it this way.
|
|
@@ -7817,9 +7860,9 @@ function assetHint(kind, view, url) {
|
|
|
7817
7860
|
character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
|
|
7818
7861
|
character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
|
|
7819
7862
|
character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`,
|
|
7820
|
-
model_segment: `One GLB with NAMED parts \u2014 load
|
|
7821
|
-
model_rig: `Rigged GLB (Tripo skeleton) \u2014 load
|
|
7822
|
-
model_animation: `Animated GLB \u2014 load
|
|
7863
|
+
model_segment: `One GLB with NAMED parts \u2014 load it through the quality kit (loadModelWithFallback, as for a plain model), then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
|
|
7864
|
+
model_rig: `Rigged GLB (Tripo skeleton) \u2014 load it through the quality kit (loadModelWithFallback); give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
|
|
7865
|
+
model_animation: `Animated GLB \u2014 load it through the quality kit (loadModelWithFallback) and play its clips via THREE.AnimationMixer (gltf.animations).`
|
|
7823
7866
|
};
|
|
7824
7867
|
const base = hint[kind];
|
|
7825
7868
|
return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
|
|
@@ -17939,6 +17982,7 @@ var CONTROLLER_FILE_SETS = {
|
|
|
17939
17982
|
code: [
|
|
17940
17983
|
"quality/tier.ts",
|
|
17941
17984
|
"quality/governor.ts",
|
|
17985
|
+
"quality/deadline.ts",
|
|
17942
17986
|
"quality/pick-asset.ts",
|
|
17943
17987
|
"quality/gltf-loader.ts",
|
|
17944
17988
|
"quality/depth.ts",
|
|
@@ -20898,7 +20942,7 @@ async function runBlender(opts) {
|
|
|
20898
20942
|
return serveLocalBlender({ port, log });
|
|
20899
20943
|
}
|
|
20900
20944
|
if (sub === "mcp") {
|
|
20901
|
-
const { runBlenderMcp } = await import("./blender-mcp-
|
|
20945
|
+
const { runBlenderMcp } = await import("./blender-mcp-PWOUSXK4.js");
|
|
20902
20946
|
return runBlenderMcp();
|
|
20903
20947
|
}
|
|
20904
20948
|
if (sub === "seat") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0-dev.604",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Genex adaptive-quality: BOUNDED loading.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. Before 2026-09-07 there was no timeout anywhere in the
|
|
4
|
+
// vendored kits — `grep -rn "setTimeout|AbortController|AbortSignal|timeout"`
|
|
5
|
+
// across quality/ and character/ returned NOTHING. Every fallback in
|
|
6
|
+
// `pick-asset.ts` lives inside a `catch`, so it advances on a REJECTION and
|
|
7
|
+
// never on silence: a rung that hangs rather than 404s parks the boot forever,
|
|
8
|
+
// and the player watches a black canvas with no error in the console. The one
|
|
9
|
+
// local Opus run that shipped had to hand-write its own `withDeadline` in
|
|
10
|
+
// `src/main.ts` to get around exactly this, which is the clearest possible
|
|
11
|
+
// signal that the kit owed it.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THIS BOUNDS, HONESTLY. `withDeadline` bounds the WAIT, not the fetch.
|
|
14
|
+
// The abandoned request keeps running in the background until the browser gives
|
|
15
|
+
// up on it; what changes is that the fallback chain ADVANCES instead of
|
|
16
|
+
// stalling, which is the failure being fixed. Real cancellation needs the
|
|
17
|
+
// caller to own the request (`fetchArrayBufferWithDeadline` below is that door,
|
|
18
|
+
// for callers that can hand bytes to `loader.parse`).
|
|
19
|
+
//
|
|
20
|
+
// Zero dependencies and browser-only APIs, because this ships inside a player's
|
|
21
|
+
// game bundle.
|
|
22
|
+
|
|
23
|
+
/** Thrown when a load outlives its deadline. Distinct so a caller can tell a
|
|
24
|
+
* timeout from a genuine 404 — they mean different things about the asset. */
|
|
25
|
+
export class DeadlineError extends Error {
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly ms: number;
|
|
28
|
+
constructor(label: string, ms: number) {
|
|
29
|
+
super(`[genex-quality] ${label} exceeded ${ms}ms`);
|
|
30
|
+
this.name = "DeadlineError";
|
|
31
|
+
this.label = label;
|
|
32
|
+
this.ms = ms;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve `work`, or reject with `DeadlineError` after `ms`.
|
|
38
|
+
*
|
|
39
|
+
* The timer is always cleared, including on the success path: a pending timer
|
|
40
|
+
* keeps a closure over the promise alive, and twenty of them per boot is a leak
|
|
41
|
+
* a long session notices.
|
|
42
|
+
*/
|
|
43
|
+
export function withDeadline<T>(work: Promise<T>, ms: number, label: string): Promise<T> {
|
|
44
|
+
if (!(ms > 0) || !Number.isFinite(ms)) return work;
|
|
45
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
46
|
+
const bell = new Promise<never>((_resolve, reject) => {
|
|
47
|
+
timer = setTimeout(() => reject(new DeadlineError(label, ms)), ms);
|
|
48
|
+
});
|
|
49
|
+
return Promise.race([work, bell]).finally(() => {
|
|
50
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
51
|
+
}) as Promise<T>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A budget shared across a boot, so N slow assets cannot SERIALISE into a wait
|
|
56
|
+
* no per-attempt deadline would ever catch.
|
|
57
|
+
*
|
|
58
|
+
* A per-attempt deadline alone is not enough and the arithmetic is the reason:
|
|
59
|
+
* twenty props at a 15 s rung deadline is five minutes of black screen, with
|
|
60
|
+
* every individual attempt inside its limit. `remaining()` is what makes the
|
|
61
|
+
* ceiling the BOOT's rather than each asset's.
|
|
62
|
+
*
|
|
63
|
+
* Never returns a negative, and `expired()` is the honest question to ask
|
|
64
|
+
* before starting more optional work.
|
|
65
|
+
*/
|
|
66
|
+
export function createBootBudget(totalMs: number, now: () => number = () => Date.now()) {
|
|
67
|
+
const startedAt = now();
|
|
68
|
+
return {
|
|
69
|
+
remaining(): number {
|
|
70
|
+
return Math.max(0, totalMs - (now() - startedAt));
|
|
71
|
+
},
|
|
72
|
+
expired(): boolean {
|
|
73
|
+
return now() - startedAt >= totalMs;
|
|
74
|
+
},
|
|
75
|
+
/** The deadline to give the next attempt: its own cap, clipped to what is
|
|
76
|
+
* left of the boot. */
|
|
77
|
+
slice(attemptMs: number): number {
|
|
78
|
+
return Math.min(attemptMs, Math.max(0, totalMs - (now() - startedAt)));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* REAL cancellation, for a caller that can parse bytes itself:
|
|
85
|
+
*
|
|
86
|
+
* const buf = await fetchArrayBufferWithDeadline(url, 15000);
|
|
87
|
+
* const gltf = await loader.parseAsync(buf, "");
|
|
88
|
+
*
|
|
89
|
+
* Safe for OUR generated rungs specifically, because they embed their textures
|
|
90
|
+
* — a GLB with external resources would lose its base URL this way, which is
|
|
91
|
+
* why the loaders do NOT use this by default.
|
|
92
|
+
*/
|
|
93
|
+
export async function fetchArrayBufferWithDeadline(
|
|
94
|
+
url: string,
|
|
95
|
+
ms: number,
|
|
96
|
+
label = url,
|
|
97
|
+
): Promise<ArrayBuffer> {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
102
|
+
if (!res.ok) throw new Error(`[genex-quality] ${label} → HTTP ${res.status}`);
|
|
103
|
+
return await res.arrayBuffer();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// An abort is a deadline, and the caller must be able to tell the two apart.
|
|
106
|
+
if (err instanceof Error && err.name === "AbortError") throw new DeadlineError(label, ms);
|
|
107
|
+
throw err;
|
|
108
|
+
} finally {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Per-attempt defaults. A rung is small by construction; the original is the
|
|
114
|
+
* archival asset and is allowed to be slower, because reaching it at all means
|
|
115
|
+
* every rung already failed. */
|
|
116
|
+
export const RUNG_DEADLINE_MS = 15_000;
|
|
117
|
+
export const ORIGINAL_DEADLINE_MS = 30_000;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// whose backfill hasn't run). loadTextureWithFallback retries the bare URL on
|
|
10
10
|
// a rung failure, so the worst case is today's behavior — never a broken boot.
|
|
11
11
|
import type { QualityTier } from './tier.ts';
|
|
12
|
+
import { withDeadline, RUNG_DEADLINE_MS, ORIGINAL_DEADLINE_MS } from './deadline.ts';
|
|
12
13
|
|
|
13
14
|
// Host-agnostic on purpose: each stand serves generated assets from its own
|
|
14
15
|
// domain (prod assets.genex.technology, dev assets.auras.cc), and baking one
|
|
@@ -56,23 +57,28 @@ export async function loadTextureWithFallback<T>(
|
|
|
56
57
|
url: string,
|
|
57
58
|
tier: QualityTier,
|
|
58
59
|
load: (resolvedUrl: string) => Promise<T>,
|
|
59
|
-
opts?: { ktx2Load?: (resolvedUrl: string) => Promise<T
|
|
60
|
+
opts?: { ktx2Load?: (resolvedUrl: string) => Promise<T>; deadlineMs?: number },
|
|
60
61
|
): Promise<T> {
|
|
62
|
+
// EVERY RUNG IS BOUNDED. The fallbacks below live in `catch`, so they advance
|
|
63
|
+
// on a rejection and never on silence — a rung that HANGS rather than 404s
|
|
64
|
+
// parked the boot forever, with no error in the console. `deadlineMs: 0`
|
|
65
|
+
// opts out.
|
|
66
|
+
const rungMs = opts?.deadlineMs ?? RUNG_DEADLINE_MS;
|
|
61
67
|
const picked = pickAsset(url, tier);
|
|
62
68
|
if (picked !== url && opts?.ktx2Load) {
|
|
63
69
|
try {
|
|
64
|
-
return await opts.ktx2Load(`${picked}.ktx2`);
|
|
70
|
+
return await withDeadline(opts.ktx2Load(`${picked}.ktx2`), rungMs, `ktx2 texture ${picked}`);
|
|
65
71
|
} catch {
|
|
66
|
-
console.warn(`[genex-quality] ktx2 variant missing for ${picked} — using the browser-decodable rung`);
|
|
72
|
+
console.warn(`[genex-quality] ktx2 variant missing or too slow for ${picked} — using the browser-decodable rung`);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
|
-
if (picked === url) return load(url);
|
|
75
|
+
if (picked === url) return withDeadline(load(url), opts?.deadlineMs ?? ORIGINAL_DEADLINE_MS, `texture ${url}`);
|
|
70
76
|
try {
|
|
71
|
-
return await load(picked);
|
|
77
|
+
return await withDeadline(load(picked), rungMs, `texture rung ${picked}`);
|
|
72
78
|
} catch {
|
|
73
|
-
// Missing rung (old asset, un-backfilled env) — degrade to the original.
|
|
74
|
-
console.warn(`[genex-quality] rung missing for ${url} — loading the original`);
|
|
75
|
-
return load(url);
|
|
79
|
+
// Missing or hung rung (old asset, un-backfilled env) — degrade to the original.
|
|
80
|
+
console.warn(`[genex-quality] rung missing or too slow for ${url} — loading the original`);
|
|
81
|
+
return withDeadline(load(url), opts?.deadlineMs ?? ORIGINAL_DEADLINE_MS, `texture ${url}`);
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -130,22 +136,28 @@ export async function loadModelWithFallback<T>(
|
|
|
130
136
|
url: string,
|
|
131
137
|
tier: QualityTier,
|
|
132
138
|
load: (resolvedUrl: string) => Promise<T>,
|
|
133
|
-
opts?: { ktx2?: boolean },
|
|
139
|
+
opts?: { ktx2?: boolean; deadlineMs?: number },
|
|
134
140
|
): Promise<T> {
|
|
141
|
+
// See the note in `loadTextureWithFallback`: without a deadline a hung rung
|
|
142
|
+
// never reaches these `catch` blocks and the boot never finishes. The
|
|
143
|
+
// original gets a longer one — reaching it at all means every rung failed,
|
|
144
|
+
// and it is the archival asset.
|
|
145
|
+
const rungMs = opts?.deadlineMs ?? RUNG_DEADLINE_MS;
|
|
146
|
+
const originalMs = opts?.deadlineMs ?? ORIGINAL_DEADLINE_MS;
|
|
135
147
|
const withKtx2 = pickModel(url, tier, opts);
|
|
136
148
|
const universal = pickModel(url, tier, { ktx2: false });
|
|
137
149
|
if (withKtx2 !== universal) {
|
|
138
150
|
try {
|
|
139
|
-
return await load(withKtx2);
|
|
151
|
+
return await withDeadline(load(withKtx2), rungMs, `ktx2 model rung ${withKtx2}`);
|
|
140
152
|
} catch {
|
|
141
|
-
console.warn(`[genex-quality] ktx2 model rung missing for ${url} — trying the universal rung`);
|
|
153
|
+
console.warn(`[genex-quality] ktx2 model rung missing or too slow for ${url} — trying the universal rung`);
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
|
-
if (universal === url) return load(url);
|
|
156
|
+
if (universal === url) return withDeadline(load(url), originalMs, `model ${url}`);
|
|
145
157
|
try {
|
|
146
|
-
return await load(universal);
|
|
158
|
+
return await withDeadline(load(universal), rungMs, `model rung ${universal}`);
|
|
147
159
|
} catch {
|
|
148
|
-
console.warn(`[genex-quality] model rung missing for ${url} — loading the original`);
|
|
149
|
-
return load(url);
|
|
160
|
+
console.warn(`[genex-quality] model rung missing or too slow for ${url} — loading the original`);
|
|
161
|
+
return withDeadline(load(url), originalMs, `model ${url}`);
|
|
150
162
|
}
|
|
151
163
|
}
|
|
@@ -119,11 +119,12 @@ vendored code from memory of another engine.
|
|
|
119
119
|
|
|
120
120
|
| Work needed | Load |
|
|
121
121
|
| --- | --- |
|
|
122
|
+
| **your first `npx genex model` or `npx genex character` this session** — the command, what comes back, how to scale and ground it, and what to do when a lane is dead. One page, thirty seconds. The full lanes below are long; measured across 105 generation-lane invocations, 94% ran without the owning skill open, and placement is where that showed | `$genex-lane-card` |
|
|
122
123
|
| shot composition, chase/side/orbit rigs, camera handoffs, projection ownership, pointer look, mouse-aimed action, mouse-look, the screen-direction contract for hand-rolled steering/pan/look input signs, floating origins | `$genex-threejs-camera-direction` |
|
|
123
124
|
| on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms, the player's body loader, directional locomotion, transitions, action motion | `$genex-threejs-character-controller` |
|
|
124
125
|
| **attacking, casting, aiming or reloading WHILE moving** — any action the legs must keep running under; a weapon carry stance over stock locomotion; a wind-up the character holds while walking | `$genex-threejs-character-controller` (`references/animations.md`, upper-body layering) |
|
|
125
126
|
| dash, dodge, roll, blink, backstep, a lunging attack — any burst that moves the character itself | `$genex-threejs-character-controller` (`references/tuning-and-presets.md`, dash recipe) |
|
|
126
|
-
| the game's own generated character—the player's body wherever a human body appears—or Meshy animation coverage beyond the stock pack: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
|
|
127
|
+
| the game's own generated character—the player's body wherever a human body appears—or Meshy animation coverage beyond the stock pack: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` (first time this session: `$genex-lane-card`) |
|
|
127
128
|
| a character/enemy needs motion the catalog lacks—a signature move, boss telegraph, death, full 8-way set, or the player's footage; free plan before spend | `$genex-ai-character` motion section + `references/motion-generation.md` |
|
|
128
129
|
| remote player bodies in multiplayer—never hand-built primitives: the game's generated character when it has one, otherwise the player's `p.avatarUrl` VRM | `$genex-threejs-multiplayer` + `$genex-threejs-character-controller` |
|
|
129
130
|
| enemies, NPCs, or creatures: rigged bipeds via `npx genex creature`; non-biped body plans (quadruped, flier, serpent, aquatic, multi-leg) via `npx genex model rig` + `model animate`; static plus procedural motion only for shapes with no body plan; collider, facing, hit reaction, death | `$genex-threejs-creatures` |
|
|
@@ -140,7 +141,7 @@ vendored code from memory of another engine.
|
|
|
140
141
|
| eye adaptation, tone mapping, output color, LUT grading, and proven static grain | `$genex-threejs-exposure-color-grading` |
|
|
141
142
|
| fixed-view screenshots, input direction, facing, temporal and budget evidence | `$genex-threejs-visual-validation` |
|
|
142
143
|
| **a model the player already has** — a `.glb` they exported, bought or made elsewhere: it is IMPORTED, never rebuilt. `npx genex model import <file.glb>` (free) for props and non-biped bodies (then `model rig`/`model animate`), `npx genex character import <file.glb>` for a humanoid that should walk (Uthana auto-rig, then `character animate --locomotion`) | `$genex-ai-model` · `$genex-ai-character` |
|
|
143
|
-
| the default for a concrete object the player looks at up close: a generated GLB for a prop, vehicle, building, or object — from text or a reference image (`--image`); split into named parts (`model segment`); rig + animate any mesh (`model rig` / `model animate`). A space routed to code above does NOT settle these: a village's ground plan can be code while the forge you walk up to is generated | `$genex-ai-model` |
|
|
144
|
+
| the default for a concrete object the player looks at up close: a generated GLB for a prop, vehicle, building, or object — from text or a reference image (`--image`); split into named parts (`model segment`); rig + animate any mesh (`model rig` / `model animate`). A space routed to code above does NOT settle these: a village's ground plan can be code while the forge you walk up to is generated | `$genex-ai-model` (first time this session: `$genex-lane-card`) |
|
|
144
145
|
| generated surface or terrain texture with real-world UV scale | `$genex-ai-texture` |
|
|
145
146
|
| sky, skybox, horizon, time of day, weather mood, night or space backdrop | build it in code in the scene—there is no sky command and no owning skill, so pick the technique this game needs |
|
|
146
147
|
| poster, sign, sprite, decal, reference sheet, or other 2D art | `$genex-ai-image` |
|
|
@@ -196,9 +197,14 @@ its local path for code-built ones — the table is where the per-object decisio
|
|
|
196
197
|
lives, and a table with no generated row records
|
|
197
198
|
`Generation: none — <why code alone reaches the bar here>` beside it. Mixing
|
|
198
199
|
both in one scene is the normal way to build, never a fallback. In focused
|
|
199
|
-
work, stay inside the touched scope.
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
work, stay inside the touched scope. Generate in batches you can finish. The first
|
|
201
|
+
batch is the smallest set that makes the scene read — the player's body,
|
|
202
|
+
whatever the request names, and the first thing they walk up to — enqueued
|
|
203
|
+
together with `--no-wait` while you scaffold. Before a second batch starts,
|
|
204
|
+
every id in the first is collected with `npx genex wait --all`, loaded from a
|
|
205
|
+
file in `src/`, and seen in a capture. A batch begun while the previous one is
|
|
206
|
+
still uncollected is how a game ends up with an asset library and an empty
|
|
207
|
+
world. Preserve every id, URL and wiring state across a compaction.
|
|
202
208
|
|
|
203
209
|
## 5. Execute by working mode
|
|
204
210
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-lane-card
|
|
3
|
+
description: The asset belt on one page — the command, what comes back, how to place it so it is not a speck or a wall, and what to do when a lane is dead. Read this before your first `npx genex model` or `npx genex character`. The full lanes are `$genex-ai-model` and `$genex-ai-character`; this card is what you need to not get it wrong.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex · Lane card
|
|
7
|
+
|
|
8
|
+
## Object → mesh
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx genex model "weathered oak barrel, iron bands, damp staves" --no-wait
|
|
12
|
+
npx genex wait --all # prints every id this project enqueued, with its URL
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A specific prompt beats a noun: "barrel" gives you a barrel-shaped guess, the
|
|
16
|
+
line above gives you the one in your scene. The URL is permanent — paste it into
|
|
17
|
+
the loader, never download it into the repo.
|
|
18
|
+
|
|
19
|
+
## Player body → character
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx genex character "stylized desert courier, layered dust-worn cloth"
|
|
23
|
+
npx genex character preview <concept-id> --candidate 1 --user-approved
|
|
24
|
+
npx genex character finalize <preview-id> --user-approved --approve-remesh 10000
|
|
25
|
+
npx genex controller character --character <id> # the whole integration
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Look at the three concepts, pick the strongest yourself, say which and why, and
|
|
29
|
+
keep going. Never foreground-`wait` on a character stage — they take minutes.
|
|
30
|
+
|
|
31
|
+
## Place it — a GLB arrives at any scale, with any origin
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const o = gltf.scene;
|
|
35
|
+
const box = new THREE.Box3().setFromObject(o);
|
|
36
|
+
const size = new THREE.Vector3();
|
|
37
|
+
box.getSize(size);
|
|
38
|
+
o.scale.multiplyScalar(TARGET_HEIGHT_M / size.y); // the metres you want
|
|
39
|
+
box.setFromObject(o); // re-measure, then ground it
|
|
40
|
+
o.position.y -= box.min.y;
|
|
41
|
+
scene.add(o);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Skip this and the mesh is a speck or a wall — it is the single most common way a
|
|
45
|
+
generated asset ships broken. Rigged bodies mismeasure here; those go through
|
|
46
|
+
the controller's own boot path, not this snippet.
|
|
47
|
+
|
|
48
|
+
## The one rule
|
|
49
|
+
|
|
50
|
+
An asset is not done when the command exits. It is done when something in
|
|
51
|
+
`src/` loads it, you have run the game, and you have LOOKED at it. A generation
|
|
52
|
+
nobody wired is money spent on nothing — `npx genex wait --all` marks a row
|
|
53
|
+
`wired` only once its URL appears in your source.
|
|
54
|
+
|
|
55
|
+
## When a lane is dead
|
|
56
|
+
|
|
57
|
+
A lane whose provider wallet is empty fails every call and refunds every one.
|
|
58
|
+
`npx genex doctor` prints each lane live/mock/paused with its credit state. If a
|
|
59
|
+
lane is red: **build that thing in code and move on** — do not re-run it, and do
|
|
60
|
+
not stall the game waiting for it. Say in chat what you fell back to.
|