@genex-ai/cli-demo 0.62.0-dev.143 → 0.63.0-dev.146
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 +221 -81
- package/package.json +1 -1
- package/templates/skills/genex-ai-menu/SKILL.md +13 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +24 -10
- package/templates/skills/genex-threejs-character-controller/references/animations.md +15 -5
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +4 -0
- package/templates/skills/genex-threejs-game-content/SKILL.md +189 -0
- package/templates/skills/genex-threejs-game-content/references/content-tables.md +269 -0
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -3
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +6 -2
- package/templates/skills/genex-threejs-open-world/SKILL.md +149 -0
- package/templates/skills/genex-threejs-open-world/references/terrain-streaming.md +215 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +20 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +38 -4
package/dist/index.js
CHANGED
|
@@ -1929,11 +1929,69 @@ async function runMakeRemixable(opts) {
|
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
1931
|
// src/lib/detect-features.ts
|
|
1932
|
+
import fs11 from "fs/promises";
|
|
1933
|
+
import path12 from "path";
|
|
1934
|
+
|
|
1935
|
+
// src/lib/generation-ledger.ts
|
|
1932
1936
|
import fs10 from "fs/promises";
|
|
1933
1937
|
import path11 from "path";
|
|
1938
|
+
var ledgerPath = (cwd) => path11.join(cwd, ".genex", "generations.ndjson");
|
|
1939
|
+
async function append(cwd, event) {
|
|
1940
|
+
try {
|
|
1941
|
+
await fs10.access(path11.join(cwd, ".genex"));
|
|
1942
|
+
await fs10.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
1943
|
+
`, "utf8");
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function readLedger(cwd = process.cwd()) {
|
|
1948
|
+
let raw;
|
|
1949
|
+
try {
|
|
1950
|
+
raw = await fs10.readFile(ledgerPath(cwd), "utf8");
|
|
1951
|
+
} catch {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1955
|
+
for (const line of raw.split("\n")) {
|
|
1956
|
+
if (!line.trim()) continue;
|
|
1957
|
+
try {
|
|
1958
|
+
const e = JSON.parse(line);
|
|
1959
|
+
if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
|
|
1960
|
+
if (!entries.has(e.id)) {
|
|
1961
|
+
entries.set(e.id, {
|
|
1962
|
+
id: e.id,
|
|
1963
|
+
kind: e.kind,
|
|
1964
|
+
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
1965
|
+
queuedAt: typeof e.at === "string" ? e.at : "",
|
|
1966
|
+
status: "queued"
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
} else if (e.t === "done" && typeof e.id === "string") {
|
|
1970
|
+
const entry = entries.get(e.id);
|
|
1971
|
+
if (entry && (e.status === "completed" || e.status === "failed")) {
|
|
1972
|
+
entry.status = e.status;
|
|
1973
|
+
if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return [...entries.values()];
|
|
1980
|
+
}
|
|
1981
|
+
async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
|
|
1982
|
+
await append(cwd, { t: "q", id, kind, prompt: prompt.slice(0, 120), at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1983
|
+
}
|
|
1984
|
+
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
1985
|
+
await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
|
|
1986
|
+
}
|
|
1987
|
+
async function countFailed(kind, cwd = process.cwd()) {
|
|
1988
|
+
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/lib/detect-features.ts
|
|
1934
1992
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
1935
1993
|
try {
|
|
1936
|
-
const raw = await
|
|
1994
|
+
const raw = await fs11.readFile(path12.join(cwd, "package.json"), "utf8");
|
|
1937
1995
|
const pkg = JSON.parse(raw);
|
|
1938
1996
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
1939
1997
|
return typeof version === "string" && version ? version : null;
|
|
@@ -1943,7 +2001,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
1943
2001
|
}
|
|
1944
2002
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
1945
2003
|
try {
|
|
1946
|
-
const raw = await
|
|
2004
|
+
const raw = await fs11.readFile(path12.join(cwd, "package.json"), "utf8");
|
|
1947
2005
|
const pkg = JSON.parse(raw);
|
|
1948
2006
|
return Boolean(
|
|
1949
2007
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -1955,7 +2013,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
1955
2013
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
1956
2014
|
let pkg;
|
|
1957
2015
|
try {
|
|
1958
|
-
pkg = JSON.parse(await
|
|
2016
|
+
pkg = JSON.parse(await fs11.readFile(path12.join(cwd, "package.json"), "utf8"));
|
|
1959
2017
|
} catch (err) {
|
|
1960
2018
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
1961
2019
|
return null;
|
|
@@ -1972,10 +2030,10 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
1972
2030
|
}
|
|
1973
2031
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
1974
2032
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
1975
|
-
const srcDir =
|
|
2033
|
+
const srcDir = path12.join(cwd, "src");
|
|
1976
2034
|
let entries;
|
|
1977
2035
|
try {
|
|
1978
|
-
entries = await
|
|
2036
|
+
entries = await fs11.readdir(srcDir, { recursive: true });
|
|
1979
2037
|
} catch {
|
|
1980
2038
|
return false;
|
|
1981
2039
|
}
|
|
@@ -1983,7 +2041,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1983
2041
|
if (rel.includes("node_modules")) continue;
|
|
1984
2042
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
1985
2043
|
try {
|
|
1986
|
-
const content = await
|
|
2044
|
+
const content = await fs11.readFile(path12.join(srcDir, rel), "utf8");
|
|
1987
2045
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
1988
2046
|
} catch {
|
|
1989
2047
|
}
|
|
@@ -1993,14 +2051,14 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1993
2051
|
var UI_PHASE_MARKERS = /data-phase|setPhase/;
|
|
1994
2052
|
async function detectUiPhases(cwd = process.cwd()) {
|
|
1995
2053
|
try {
|
|
1996
|
-
const html = await
|
|
2054
|
+
const html = await fs11.readFile(path12.join(cwd, "index.html"), "utf8").catch(() => "");
|
|
1997
2055
|
if (UI_PHASE_MARKERS.test(html)) return true;
|
|
1998
|
-
const srcDir =
|
|
1999
|
-
const entries = await
|
|
2056
|
+
const srcDir = path12.join(cwd, "src");
|
|
2057
|
+
const entries = await fs11.readdir(srcDir, { recursive: true });
|
|
2000
2058
|
for (const rel of entries) {
|
|
2001
2059
|
if (rel.includes("node_modules")) continue;
|
|
2002
2060
|
if (!/\.(ts|js|mts|mjs|tsx|jsx|html|css)$/.test(rel)) continue;
|
|
2003
|
-
const content = await
|
|
2061
|
+
const content = await fs11.readFile(path12.join(srcDir, rel), "utf8").catch(() => "");
|
|
2004
2062
|
if (UI_PHASE_MARKERS.test(content)) return true;
|
|
2005
2063
|
}
|
|
2006
2064
|
return false;
|
|
@@ -2045,17 +2103,17 @@ var lineOf = (content, index) => content.slice(0, index).split("\n").length;
|
|
|
2045
2103
|
var DEPTH_RATIO_LIMIT = 1e6;
|
|
2046
2104
|
async function detectSurfaceScan(cwd = process.cwd()) {
|
|
2047
2105
|
const found = { guessedRepeat: [], squarePoints: [], depthRange: [] };
|
|
2048
|
-
const srcDir =
|
|
2106
|
+
const srcDir = path12.join(cwd, "src");
|
|
2049
2107
|
let entries;
|
|
2050
2108
|
try {
|
|
2051
|
-
entries = await
|
|
2109
|
+
entries = await fs11.readdir(srcDir, { recursive: true });
|
|
2052
2110
|
} catch {
|
|
2053
2111
|
return found;
|
|
2054
2112
|
}
|
|
2055
2113
|
for (const rel of entries) {
|
|
2056
2114
|
if (rel.includes("node_modules")) continue;
|
|
2057
2115
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
2058
|
-
const raw = await
|
|
2116
|
+
const raw = await fs11.readFile(path12.join(srcDir, rel), "utf8").catch(() => "");
|
|
2059
2117
|
if (!raw) continue;
|
|
2060
2118
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
2061
2119
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
@@ -2093,6 +2151,54 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
2093
2151
|
}
|
|
2094
2152
|
return found;
|
|
2095
2153
|
}
|
|
2154
|
+
var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "video"]);
|
|
2155
|
+
var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
2156
|
+
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
2157
|
+
const ledger = await readLedger(cwd);
|
|
2158
|
+
const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
|
|
2159
|
+
if (audited.length === 0) return { unwired: [], unpicked: [] };
|
|
2160
|
+
let haystack = "";
|
|
2161
|
+
const read = async (file) => {
|
|
2162
|
+
try {
|
|
2163
|
+
haystack += await fs11.readFile(file, "utf8");
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
try {
|
|
2168
|
+
for (const entry of await fs11.readdir(cwd, { withFileTypes: true })) {
|
|
2169
|
+
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
2170
|
+
await read(path12.join(cwd, entry.name));
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} catch {
|
|
2174
|
+
}
|
|
2175
|
+
for (const sub of ["src", "public"]) {
|
|
2176
|
+
try {
|
|
2177
|
+
const entries = await fs11.readdir(path12.join(cwd, sub), { recursive: true });
|
|
2178
|
+
for (const rel of entries) {
|
|
2179
|
+
if (rel.includes("node_modules")) continue;
|
|
2180
|
+
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
2181
|
+
await read(path12.join(cwd, sub, rel));
|
|
2182
|
+
}
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
const ref = ({ id, kind, prompt }) => ({
|
|
2187
|
+
id,
|
|
2188
|
+
kind,
|
|
2189
|
+
prompt
|
|
2190
|
+
});
|
|
2191
|
+
const now = Date.now();
|
|
2192
|
+
return {
|
|
2193
|
+
unwired: audited.filter((e) => e.status === "completed" && !haystack.includes(e.id)).map(ref),
|
|
2194
|
+
// An id already referenced in the game was obviously picked up — a stale
|
|
2195
|
+
// "queued" record (e.g. `genex wait` ran from another directory) must not
|
|
2196
|
+
// nag about an asset that's wired in and working.
|
|
2197
|
+
unpicked: audited.filter(
|
|
2198
|
+
(e) => e.status === "queued" && !haystack.includes(e.id) && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS
|
|
2199
|
+
).map(ref)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2096
2202
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
2097
2203
|
return {
|
|
2098
2204
|
embedSdkVersion: await detectEmbedSdkVersion(cwd),
|
|
@@ -2100,7 +2206,8 @@ async function detectFeatures(log, cwd = process.cwd()) {
|
|
|
2100
2206
|
matchmaking: await detectMatchmaking(log, cwd),
|
|
2101
2207
|
gameStateUsed: await detectGameStateUsage(cwd),
|
|
2102
2208
|
uiPhases: await detectUiPhases(cwd),
|
|
2103
|
-
surfaces: await detectSurfaceScan(cwd)
|
|
2209
|
+
surfaces: await detectSurfaceScan(cwd),
|
|
2210
|
+
generations: await detectGenerationAudit(cwd)
|
|
2104
2211
|
};
|
|
2105
2212
|
}
|
|
2106
2213
|
function advisoryNudges(log, d) {
|
|
@@ -2125,6 +2232,20 @@ function advisoryNudges(log, d) {
|
|
|
2125
2232
|
);
|
|
2126
2233
|
}
|
|
2127
2234
|
surfaceNudges(log, d.surfaces);
|
|
2235
|
+
generationNudges(log, d.generations);
|
|
2236
|
+
}
|
|
2237
|
+
function generationNudges(log, g) {
|
|
2238
|
+
const describe = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
|
|
2239
|
+
if (g.unpicked.length) {
|
|
2240
|
+
log.warn(
|
|
2241
|
+
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
if (g.unwired.length) {
|
|
2245
|
+
log.warn(
|
|
2246
|
+
`${g.unwired.length} finished generation(s) are wired nowhere in the game: ${describe(g.unwired)}. Each was paid for \u2014 wire it in (its URL embeds the id) or tell the user in one plain line why it stays unused.`
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2128
2249
|
}
|
|
2129
2250
|
function surfaceNudges(log, s) {
|
|
2130
2251
|
if (s.guessedRepeat.length) {
|
|
@@ -2518,6 +2639,7 @@ async function runGenerate(kind, opts) {
|
|
|
2518
2639
|
process.exitCode = 1;
|
|
2519
2640
|
return;
|
|
2520
2641
|
}
|
|
2642
|
+
await recordQueued(id, kind, prompt);
|
|
2521
2643
|
if (opts.noWait) {
|
|
2522
2644
|
log.success(`Queued (${id}).`);
|
|
2523
2645
|
log.plain(` Pick it up any time with: ${c.cyan(`npx genex wait ${id}`)}`);
|
|
@@ -2544,6 +2666,13 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false) {
|
|
|
2544
2666
|
async function reportTerminal(kind, view, log, open = false) {
|
|
2545
2667
|
if (view.status !== "completed") {
|
|
2546
2668
|
log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
2669
|
+
if (view.status === "failed") {
|
|
2670
|
+
await recordTerminal(view.id, "failed");
|
|
2671
|
+
if (kind === "video") {
|
|
2672
|
+
const failures = await countFailed("video");
|
|
2673
|
+
(failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2547
2676
|
process.exitCode = 1;
|
|
2548
2677
|
return;
|
|
2549
2678
|
}
|
|
@@ -2553,6 +2682,7 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2553
2682
|
process.exitCode = 1;
|
|
2554
2683
|
return;
|
|
2555
2684
|
}
|
|
2685
|
+
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
2556
2686
|
log.plain("");
|
|
2557
2687
|
log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
|
|
2558
2688
|
for (const f of files) {
|
|
@@ -2572,6 +2702,12 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2572
2702
|
printHint(kind, view, files, log);
|
|
2573
2703
|
await firstPreviewNudge(log);
|
|
2574
2704
|
}
|
|
2705
|
+
function videoFailureAdvice(failures) {
|
|
2706
|
+
if (failures >= 2) {
|
|
2707
|
+
return ` ${c.bold(`\u270B ${failures} failed videos in this project \u2014 don't queue another attempt of the same clip.`)} One retry per clip is the budget. Use its key-art still as the backdrop instead (the genex-ai-menu skill's built-in fallback; a slow CSS pan gives it life), tell the user in one plain line, and spend the minutes in the game.`;
|
|
2708
|
+
}
|
|
2709
|
+
return " Video renders fail server-side sometimes. One retry is fair \u2014 shorter clip (4\u20136 s), simpler motion. After a second failure, switch to the key-art still (genex-ai-menu's fallback) instead of a third attempt.";
|
|
2710
|
+
}
|
|
2575
2711
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2576
2712
|
var KIND_WAIT_TIMEOUT_MS = {
|
|
2577
2713
|
video: 15 * 60 * 1e3,
|
|
@@ -2752,8 +2888,8 @@ async function runWait(opts) {
|
|
|
2752
2888
|
}
|
|
2753
2889
|
|
|
2754
2890
|
// src/commands/controller.ts
|
|
2755
|
-
import
|
|
2756
|
-
import
|
|
2891
|
+
import fs13 from "fs/promises";
|
|
2892
|
+
import path14 from "path";
|
|
2757
2893
|
|
|
2758
2894
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
2759
2895
|
import { createHash } from "crypto";
|
|
@@ -11883,9 +12019,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
11883
12019
|
}
|
|
11884
12020
|
|
|
11885
12021
|
// src/lib/anims.ts
|
|
11886
|
-
import
|
|
11887
|
-
import
|
|
11888
|
-
var ANIMS_DEST =
|
|
12022
|
+
import fs12 from "fs/promises";
|
|
12023
|
+
import path13 from "path";
|
|
12024
|
+
var ANIMS_DEST = path13.join("public", "assets", "anims");
|
|
11889
12025
|
var HIDDEN_TAG = "reference";
|
|
11890
12026
|
async function runAnims(opts) {
|
|
11891
12027
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -11901,7 +12037,7 @@ async function runAnims(opts) {
|
|
|
11901
12037
|
printCatalog(log, manifest, selectors);
|
|
11902
12038
|
return;
|
|
11903
12039
|
}
|
|
11904
|
-
const controllerMarker =
|
|
12040
|
+
const controllerMarker = path13.join(root, "src", "controllers", "character");
|
|
11905
12041
|
if (!await exists2(controllerMarker)) {
|
|
11906
12042
|
log.error(
|
|
11907
12043
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -11910,11 +12046,11 @@ async function runAnims(opts) {
|
|
|
11910
12046
|
process.exitCode = 1;
|
|
11911
12047
|
return;
|
|
11912
12048
|
}
|
|
11913
|
-
const destDir =
|
|
11914
|
-
const gameManifestPath =
|
|
12049
|
+
const destDir = path13.join(root, ANIMS_DEST);
|
|
12050
|
+
const gameManifestPath = path13.join(destDir, "manifest.json");
|
|
11915
12051
|
if (opts.reset) {
|
|
11916
|
-
await
|
|
11917
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
12052
|
+
await fs12.rm(destDir, { recursive: true, force: true });
|
|
12053
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path13.sep)} (--reset)`);
|
|
11918
12054
|
}
|
|
11919
12055
|
if (selectors.length === 0) {
|
|
11920
12056
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -11952,35 +12088,35 @@ async function runAnims(opts) {
|
|
|
11952
12088
|
}
|
|
11953
12089
|
}
|
|
11954
12090
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
11955
|
-
const cacheDir =
|
|
12091
|
+
const cacheDir = path13.join(
|
|
11956
12092
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
11957
12093
|
`${manifest.library}-v${manifest.version}`
|
|
11958
12094
|
);
|
|
11959
|
-
await
|
|
11960
|
-
await
|
|
12095
|
+
await fs12.mkdir(cacheDir, { recursive: true });
|
|
12096
|
+
await fs12.mkdir(destDir, { recursive: true });
|
|
11961
12097
|
const base = getAnimsBase(opts.animsBase);
|
|
11962
12098
|
let installedCount = 0;
|
|
11963
12099
|
let presentCount = 0;
|
|
11964
12100
|
let addedBytes = 0;
|
|
11965
12101
|
const failures = [];
|
|
11966
12102
|
for (const entry of wanted) {
|
|
11967
|
-
const dest =
|
|
12103
|
+
const dest = path13.join(destDir, entry.file);
|
|
11968
12104
|
if (await hasSize(dest, entry.bytes)) {
|
|
11969
12105
|
presentCount++;
|
|
11970
12106
|
continue;
|
|
11971
12107
|
}
|
|
11972
12108
|
try {
|
|
11973
|
-
const cached =
|
|
12109
|
+
const cached = path13.join(cacheDir, entry.file);
|
|
11974
12110
|
if (!await hasSize(cached, entry.bytes)) {
|
|
11975
12111
|
const res = await fetch(base + entry.file);
|
|
11976
12112
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
11977
12113
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
11978
|
-
await
|
|
12114
|
+
await fs12.writeFile(cached, buf);
|
|
11979
12115
|
}
|
|
11980
|
-
await
|
|
12116
|
+
await fs12.copyFile(cached, dest);
|
|
11981
12117
|
installedCount++;
|
|
11982
12118
|
addedBytes += entry.bytes;
|
|
11983
|
-
log.dim(` ${
|
|
12119
|
+
log.dim(` ${path13.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
11984
12120
|
} catch (err) {
|
|
11985
12121
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
11986
12122
|
}
|
|
@@ -11996,13 +12132,13 @@ async function runAnims(opts) {
|
|
|
11996
12132
|
version: manifest.version,
|
|
11997
12133
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
11998
12134
|
};
|
|
11999
|
-
await
|
|
12135
|
+
await fs12.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
12000
12136
|
log.plain("");
|
|
12001
12137
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
12002
12138
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
12003
12139
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
12004
12140
|
log.success(
|
|
12005
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
12141
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path13.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
12006
12142
|
);
|
|
12007
12143
|
for (const [selector, entries] of resolved) {
|
|
12008
12144
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -12034,8 +12170,8 @@ async function loadManifest(baseOverride) {
|
|
|
12034
12170
|
}
|
|
12035
12171
|
} catch {
|
|
12036
12172
|
}
|
|
12037
|
-
const snapshotPath =
|
|
12038
|
-
const manifest = JSON.parse(await
|
|
12173
|
+
const snapshotPath = path13.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
12174
|
+
const manifest = JSON.parse(await fs12.readFile(snapshotPath, "utf8"));
|
|
12039
12175
|
return { manifest, source: "snapshot" };
|
|
12040
12176
|
}
|
|
12041
12177
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -12153,21 +12289,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
12153
12289
|
}
|
|
12154
12290
|
async function readGameManifest(file) {
|
|
12155
12291
|
try {
|
|
12156
|
-
return JSON.parse(await
|
|
12292
|
+
return JSON.parse(await fs12.readFile(file, "utf8"));
|
|
12157
12293
|
} catch {
|
|
12158
12294
|
return null;
|
|
12159
12295
|
}
|
|
12160
12296
|
}
|
|
12161
12297
|
async function hasSize(file, bytes) {
|
|
12162
12298
|
try {
|
|
12163
|
-
return (await
|
|
12299
|
+
return (await fs12.stat(file)).size === bytes;
|
|
12164
12300
|
} catch {
|
|
12165
12301
|
return false;
|
|
12166
12302
|
}
|
|
12167
12303
|
}
|
|
12168
12304
|
async function exists2(p) {
|
|
12169
12305
|
try {
|
|
12170
|
-
await
|
|
12306
|
+
await fs12.access(p);
|
|
12171
12307
|
return true;
|
|
12172
12308
|
} catch {
|
|
12173
12309
|
return false;
|
|
@@ -12221,15 +12357,19 @@ var CONTROLLER_FILE_SETS = {
|
|
|
12221
12357
|
...INPUT_AND_CAMERA,
|
|
12222
12358
|
NOTICE
|
|
12223
12359
|
],
|
|
12224
|
-
// The
|
|
12360
|
+
// The FALLBACK VRM is written to public/assets/avatar.vrm at install time by
|
|
12225
12361
|
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
12226
|
-
// a static manifest asset
|
|
12227
|
-
//
|
|
12362
|
+
// a static manifest asset; at runtime the game loads the playing user's own
|
|
12363
|
+
// avatar (user.avatarUrl from the embed identity, AG-804) and this file
|
|
12364
|
+
// covers local dev + load failures. animation-library.glb (the 12-clip core)
|
|
12365
|
+
// still is a manifest asset; extra clips arrive via `genex controller anims`
|
|
12366
|
+
// into public/assets/anims/.
|
|
12228
12367
|
assets: ["assets/animation-library.glb"],
|
|
12229
12368
|
skill: "genex-threejs-character-controller",
|
|
12230
12369
|
sketch: [
|
|
12231
12370
|
`const physics = await PhysicsWorld.create();`,
|
|
12232
|
-
`const {
|
|
12371
|
+
`const { user } = await waitForPlayer(); // @genex-ai/embed-sdk \u2014 the player's own avatar`,
|
|
12372
|
+
`const { scene, vrm } = await loadVrm(user.avatarUrl ?? "./assets/avatar.vrm").catch(() => loadVrm("./assets/avatar.vrm"));`,
|
|
12233
12373
|
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
12234
12374
|
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
12235
12375
|
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
@@ -12300,8 +12440,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
12300
12440
|
]
|
|
12301
12441
|
}
|
|
12302
12442
|
};
|
|
12303
|
-
var CODE_DEST =
|
|
12304
|
-
var ASSETS_DEST =
|
|
12443
|
+
var CODE_DEST = path14.join("src", "controllers");
|
|
12444
|
+
var ASSETS_DEST = path14.join("public", "assets");
|
|
12305
12445
|
async function runController(opts) {
|
|
12306
12446
|
const log = createLogger({ quiet: opts.quiet });
|
|
12307
12447
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -12318,31 +12458,31 @@ async function runController(opts) {
|
|
|
12318
12458
|
process.exitCode = 1;
|
|
12319
12459
|
return;
|
|
12320
12460
|
}
|
|
12321
|
-
const srcDir =
|
|
12461
|
+
const srcDir = path14.join(getTemplatesDir(), "controllers");
|
|
12322
12462
|
const root = opts.cwd ?? process.cwd();
|
|
12323
12463
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
12324
12464
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
12325
12465
|
log.plain("");
|
|
12326
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
12466
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path14.sep)}`);
|
|
12327
12467
|
const plan = [
|
|
12328
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
12468
|
+
...set.code.map((rel) => ({ from: rel, rel: path14.join(CODE_DEST, rel) })),
|
|
12329
12469
|
...set.assets.map((rel) => ({
|
|
12330
12470
|
from: rel,
|
|
12331
|
-
rel:
|
|
12471
|
+
rel: path14.join(ASSETS_DEST, path14.basename(rel))
|
|
12332
12472
|
}))
|
|
12333
12473
|
];
|
|
12334
12474
|
let copied = 0;
|
|
12335
12475
|
let skipped = 0;
|
|
12336
12476
|
try {
|
|
12337
12477
|
for (const file of plan) {
|
|
12338
|
-
const dest =
|
|
12478
|
+
const dest = path14.join(root, file.rel);
|
|
12339
12479
|
if (!opts.force && await exists3(dest)) {
|
|
12340
12480
|
skipped++;
|
|
12341
12481
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
12342
12482
|
continue;
|
|
12343
12483
|
}
|
|
12344
|
-
await
|
|
12345
|
-
await
|
|
12484
|
+
await fs13.mkdir(path14.dirname(dest), { recursive: true });
|
|
12485
|
+
await fs13.copyFile(path14.join(srcDir, file.from), dest);
|
|
12346
12486
|
copied++;
|
|
12347
12487
|
log.dim(` ${file.rel}`);
|
|
12348
12488
|
}
|
|
@@ -12427,9 +12567,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
12427
12567
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
12428
12568
|
}
|
|
12429
12569
|
assertCompleteMeshyControllerPack(manifest);
|
|
12430
|
-
const destination =
|
|
12431
|
-
await
|
|
12432
|
-
await
|
|
12570
|
+
const destination = path14.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
12571
|
+
await fs13.mkdir(path14.dirname(destination), { recursive: true });
|
|
12572
|
+
await fs13.writeFile(
|
|
12433
12573
|
destination,
|
|
12434
12574
|
`${JSON.stringify(manifest, null, 2)}
|
|
12435
12575
|
`
|
|
@@ -12550,8 +12690,8 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
12550
12690
|
}
|
|
12551
12691
|
async function installOwnerAvatar(args) {
|
|
12552
12692
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
12553
|
-
const dest =
|
|
12554
|
-
await
|
|
12693
|
+
const dest = path14.join(root, ASSETS_DEST, "avatar.vrm");
|
|
12694
|
+
await fs13.mkdir(path14.dirname(dest), { recursive: true });
|
|
12555
12695
|
if (token) {
|
|
12556
12696
|
try {
|
|
12557
12697
|
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -12563,7 +12703,7 @@ async function installOwnerAvatar(args) {
|
|
|
12563
12703
|
const vrmRes = await fetch(me.vrmUrl);
|
|
12564
12704
|
if (vrmRes.ok) {
|
|
12565
12705
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
12566
|
-
await
|
|
12706
|
+
await fs13.writeFile(dest, buf);
|
|
12567
12707
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
12568
12708
|
return;
|
|
12569
12709
|
}
|
|
@@ -12574,14 +12714,14 @@ async function installOwnerAvatar(args) {
|
|
|
12574
12714
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
12575
12715
|
}
|
|
12576
12716
|
}
|
|
12577
|
-
await
|
|
12717
|
+
await fs13.copyFile(path14.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
12578
12718
|
log.dim(
|
|
12579
12719
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
12580
12720
|
);
|
|
12581
12721
|
}
|
|
12582
12722
|
async function exists3(p) {
|
|
12583
12723
|
try {
|
|
12584
|
-
await
|
|
12724
|
+
await fs13.access(p);
|
|
12585
12725
|
return true;
|
|
12586
12726
|
} catch {
|
|
12587
12727
|
return false;
|
|
@@ -12866,12 +13006,12 @@ function rank(items, query) {
|
|
|
12866
13006
|
}
|
|
12867
13007
|
|
|
12868
13008
|
// src/commands/ui.ts
|
|
12869
|
-
import
|
|
12870
|
-
import
|
|
13009
|
+
import fs15 from "fs/promises";
|
|
13010
|
+
import path15 from "path";
|
|
12871
13011
|
import { PNG as PNG3 } from "pngjs";
|
|
12872
13012
|
|
|
12873
13013
|
// src/lib/png-tools.ts
|
|
12874
|
-
import
|
|
13014
|
+
import fs14 from "fs/promises";
|
|
12875
13015
|
import { PNG as PNG2 } from "pngjs";
|
|
12876
13016
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
12877
13017
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -12882,12 +13022,12 @@ async function loadPng(input) {
|
|
|
12882
13022
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
12883
13023
|
buf = Buffer.from(await res.arrayBuffer());
|
|
12884
13024
|
} else {
|
|
12885
|
-
buf = await
|
|
13025
|
+
buf = await fs14.readFile(input);
|
|
12886
13026
|
}
|
|
12887
13027
|
return PNG2.sync.read(buf);
|
|
12888
13028
|
}
|
|
12889
13029
|
async function writePng(file, png) {
|
|
12890
|
-
await
|
|
13030
|
+
await fs14.writeFile(file, PNG2.sync.write(png));
|
|
12891
13031
|
}
|
|
12892
13032
|
function cropPng(image, box) {
|
|
12893
13033
|
const out = new PNG2({ width: box.w, height: box.h });
|
|
@@ -13093,7 +13233,7 @@ async function uiExtract(opts, log) {
|
|
|
13093
13233
|
const dilatePx = opts.dilate ?? 0;
|
|
13094
13234
|
const sheet = await loadPng(input);
|
|
13095
13235
|
const { width: W, height: H, data } = sheet;
|
|
13096
|
-
await
|
|
13236
|
+
await fs15.mkdir(outDir, { recursive: true });
|
|
13097
13237
|
log.plain(c.bold("genex ui extract"));
|
|
13098
13238
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
13099
13239
|
let hasTransparency = false;
|
|
@@ -13252,7 +13392,7 @@ async function uiExtract(opts, log) {
|
|
|
13252
13392
|
}
|
|
13253
13393
|
}
|
|
13254
13394
|
}
|
|
13255
|
-
const outPath =
|
|
13395
|
+
const outPath = path15.join(outDir, `${name}.png`);
|
|
13256
13396
|
await writePng(outPath, out);
|
|
13257
13397
|
const sidecar = {
|
|
13258
13398
|
name,
|
|
@@ -13270,7 +13410,7 @@ async function uiExtract(opts, log) {
|
|
|
13270
13410
|
componentPixels: comp.pixels
|
|
13271
13411
|
};
|
|
13272
13412
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
13273
|
-
await
|
|
13413
|
+
await fs15.writeFile(
|
|
13274
13414
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
13275
13415
|
JSON.stringify(sidecarBody, null, 2)
|
|
13276
13416
|
);
|
|
@@ -13279,8 +13419,8 @@ async function uiExtract(opts, log) {
|
|
|
13279
13419
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
13280
13420
|
);
|
|
13281
13421
|
}
|
|
13282
|
-
const debugPath =
|
|
13283
|
-
await
|
|
13422
|
+
const debugPath = path15.join(outDir, "extract-debug.json");
|
|
13423
|
+
await fs15.writeFile(
|
|
13284
13424
|
debugPath,
|
|
13285
13425
|
JSON.stringify(
|
|
13286
13426
|
{
|
|
@@ -13514,7 +13654,7 @@ async function uiMasks(opts, log) {
|
|
|
13514
13654
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
13515
13655
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
13516
13656
|
const image = await loadPng(input);
|
|
13517
|
-
await
|
|
13657
|
+
await fs15.mkdir(outDir, { recursive: true });
|
|
13518
13658
|
log.plain(c.bold("genex ui masks"));
|
|
13519
13659
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
13520
13660
|
const results = [];
|
|
@@ -13557,11 +13697,11 @@ async function uiMasks(opts, log) {
|
|
|
13557
13697
|
});
|
|
13558
13698
|
}
|
|
13559
13699
|
const overlay = makeOverlay(clean2, converted.png);
|
|
13560
|
-
const framePath =
|
|
13561
|
-
const maskPath =
|
|
13562
|
-
const annotatedPath =
|
|
13563
|
-
const overlayPath =
|
|
13564
|
-
const metaPath =
|
|
13700
|
+
const framePath = path15.join(outDir, `${pair.name}-frame.png`);
|
|
13701
|
+
const maskPath = path15.join(outDir, `${pair.name}-mask.png`);
|
|
13702
|
+
const annotatedPath = path15.join(outDir, `${pair.name}-annotated-source.png`);
|
|
13703
|
+
const overlayPath = path15.join(outDir, `${pair.name}-overlay.png`);
|
|
13704
|
+
const metaPath = path15.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
13565
13705
|
await writePng(framePath, clean2);
|
|
13566
13706
|
await writePng(maskPath, converted.png);
|
|
13567
13707
|
await writePng(annotatedPath, annotated);
|
|
@@ -13600,7 +13740,7 @@ async function uiMasks(opts, log) {
|
|
|
13600
13740
|
},
|
|
13601
13741
|
overlay: overlayPath
|
|
13602
13742
|
};
|
|
13603
|
-
await
|
|
13743
|
+
await fs15.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
13604
13744
|
`);
|
|
13605
13745
|
results.push(meta);
|
|
13606
13746
|
const fb = converted.bbox;
|
|
@@ -13609,8 +13749,8 @@ async function uiMasks(opts, log) {
|
|
|
13609
13749
|
`${pair.name}: coverage ${meta.mask.coverage},${fbText} ${components.length} component(s), registration \u0394 ${meta.registration.bboxDelta}`
|
|
13610
13750
|
);
|
|
13611
13751
|
}
|
|
13612
|
-
const indexPath =
|
|
13613
|
-
await
|
|
13752
|
+
const indexPath = path15.join(outDir, "annotated-progress.json");
|
|
13753
|
+
await fs15.writeFile(indexPath, `${JSON.stringify({ input, pairs: results }, null, 2)}
|
|
13614
13754
|
`);
|
|
13615
13755
|
log.plain("");
|
|
13616
13756
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -13731,7 +13871,7 @@ async function uiTextColor(opts, log) {
|
|
|
13731
13871
|
};
|
|
13732
13872
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
13733
13873
|
`);
|
|
13734
|
-
if (opts.out) await
|
|
13874
|
+
if (opts.out) await fs15.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
13735
13875
|
`);
|
|
13736
13876
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
13737
13877
|
}
|
|
@@ -13754,7 +13894,7 @@ async function uiTrim(opts, log) {
|
|
|
13754
13894
|
await writePng(outPath, trimmed);
|
|
13755
13895
|
const sidecar = computeBBoxes(trimmed);
|
|
13756
13896
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
13757
|
-
await
|
|
13897
|
+
await fs15.writeFile(sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
13758
13898
|
log.success(
|
|
13759
13899
|
`Trimmed ${png.width}x${png.height} \u2192 ${trimmed.width}x${trimmed.height} (${outPath}).`
|
|
13760
13900
|
);
|