@genex-ai/cli-demo 0.60.0 → 0.62.0-dev.143
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/README.md +5 -2
- package/dist/index.js +997 -798
- package/package.json +1 -1
- package/templates/controllers/character/meshy/meshy-loader.ts +15 -1
- package/templates/skills/genex-ai-character/SKILL.md +19 -4
- package/templates/skills/genex-ai-hud/SKILL.md +1 -1
- package/templates/skills/genex-ai-image/SKILL.md +1 -1
- package/templates/skills/genex-ai-menu/SKILL.md +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +1 -1
- package/templates/skills/genex-ai-sfx/SKILL.md +1 -1
- package/templates/skills/genex-ai-skybox/SKILL.md +1 -1
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-explore/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-character-controller/SKILL.md +11 -0
- package/templates/skills/genex-threejs-character-controller/references/animations.md +5 -0
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +4 -0
- package/templates/skills/genex-updates/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from "fs";
|
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
-
var RAW_CHANNEL = "
|
|
11
|
+
var RAW_CHANNEL = "dev";
|
|
12
12
|
var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
|
|
13
13
|
var STANDS = {
|
|
14
14
|
prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
|
|
@@ -1089,13 +1089,13 @@ function renderGenexConfig() {
|
|
|
1089
1089
|
return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
|
|
1090
1090
|
//
|
|
1091
1091
|
// One build runs on BOTH stands: the serving hostname picks the stack at runtime
|
|
1092
|
-
// (
|
|
1092
|
+
// (\`-dev.genex.technology\` -> dev), so promoting a game is a copy, never a rebuild.
|
|
1093
1093
|
// Vite env still overrides, but is loaded ONLY by \`npm run dev\`:
|
|
1094
1094
|
// .env -> VITE_GENEX_SLUG (this game's identity; committed)
|
|
1095
1095
|
// .env.development.local -> local-stack URL overrides (dev mode ONLY; gitignored)
|
|
1096
1096
|
const IS_DEV =
|
|
1097
1097
|
typeof location !== "undefined" &&
|
|
1098
|
-
location.hostname.endsWith(".
|
|
1098
|
+
location.hostname.endsWith("-dev.genex.technology");
|
|
1099
1099
|
export const GENEX = {
|
|
1100
1100
|
slug: import.meta.env.VITE_GENEX_SLUG as string,
|
|
1101
1101
|
apiUrl:
|
|
@@ -1711,6 +1711,9 @@ async function pushSource(cwd, ctx, log) {
|
|
|
1711
1711
|
if (!fresh) return false;
|
|
1712
1712
|
return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log);
|
|
1713
1713
|
}
|
|
1714
|
+
function urlHasEmbeddedCredentials(pushUrl) {
|
|
1715
|
+
return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(pushUrl);
|
|
1716
|
+
}
|
|
1714
1717
|
async function pushWorktree(cwd, pushUrl, managed, log) {
|
|
1715
1718
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1716
1719
|
const failed = () => {
|
|
@@ -1719,6 +1722,11 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
|
|
|
1719
1722
|
};
|
|
1720
1723
|
const gitDir = await fs9.mkdtemp(path10.join(os4.tmpdir(), "genex-source-"));
|
|
1721
1724
|
const base = { GIT_DIR: gitDir };
|
|
1725
|
+
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
1726
|
+
base.GIT_CONFIG_COUNT = "1";
|
|
1727
|
+
base.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
1728
|
+
base.GIT_CONFIG_VALUE_0 = "";
|
|
1729
|
+
}
|
|
1722
1730
|
const ident = {
|
|
1723
1731
|
GIT_AUTHOR_NAME: "genex",
|
|
1724
1732
|
GIT_AUTHOR_EMAIL: "agent@genex.local",
|
|
@@ -2747,656 +2755,62 @@ async function runWait(opts) {
|
|
|
2747
2755
|
import fs12 from "fs/promises";
|
|
2748
2756
|
import path13 from "path";
|
|
2749
2757
|
|
|
2750
|
-
// src/
|
|
2751
|
-
import
|
|
2752
|
-
import path12 from "path";
|
|
2753
|
-
var ANIMS_DEST = path12.join("public", "assets", "anims");
|
|
2754
|
-
var HIDDEN_TAG = "reference";
|
|
2755
|
-
async function runAnims(opts) {
|
|
2756
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
2757
|
-
const root = opts.cwd ?? process.cwd();
|
|
2758
|
-
const selectors = opts.selectors ?? [];
|
|
2759
|
-
log.plain(c.bold("genex controller anims"));
|
|
2760
|
-
log.plain("");
|
|
2761
|
-
const { manifest, source } = await loadManifest(opts.animsBase);
|
|
2762
|
-
if (source === "snapshot") {
|
|
2763
|
-
log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
|
|
2764
|
-
}
|
|
2765
|
-
if (opts.list) {
|
|
2766
|
-
printCatalog(log, manifest, selectors);
|
|
2767
|
-
return;
|
|
2768
|
-
}
|
|
2769
|
-
const controllerMarker = path12.join(root, "src", "controllers", "character");
|
|
2770
|
-
if (!await exists2(controllerMarker)) {
|
|
2771
|
-
log.error(
|
|
2772
|
-
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
2773
|
-
);
|
|
2774
|
-
log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
|
|
2775
|
-
process.exitCode = 1;
|
|
2776
|
-
return;
|
|
2777
|
-
}
|
|
2778
|
-
const destDir = path12.join(root, ANIMS_DEST);
|
|
2779
|
-
const gameManifestPath = path12.join(destDir, "manifest.json");
|
|
2780
|
-
if (opts.reset) {
|
|
2781
|
-
await fs11.rm(destDir, { recursive: true, force: true });
|
|
2782
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
|
|
2783
|
-
}
|
|
2784
|
-
if (selectors.length === 0) {
|
|
2785
|
-
const installed = await readGameManifest(gameManifestPath);
|
|
2786
|
-
if (installed === null || installed.clips.length === 0) {
|
|
2787
|
-
log.plain(" No animation packs installed yet.");
|
|
2788
|
-
} else {
|
|
2789
|
-
log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
|
|
2790
|
-
}
|
|
2791
|
-
log.plain("");
|
|
2792
|
-
log.plain(
|
|
2793
|
-
` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
|
|
2794
|
-
"genex controller anims --list"
|
|
2795
|
-
)}.`
|
|
2796
|
-
);
|
|
2797
|
-
return;
|
|
2798
|
-
}
|
|
2799
|
-
let resolved;
|
|
2800
|
-
try {
|
|
2801
|
-
resolved = resolveSelectors(manifest, selectors);
|
|
2802
|
-
} catch (err) {
|
|
2803
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
2804
|
-
process.exitCode = 1;
|
|
2805
|
-
return;
|
|
2806
|
-
}
|
|
2807
|
-
const coreNames = new Set(manifest.core);
|
|
2808
|
-
const byName = /* @__PURE__ */ new Map();
|
|
2809
|
-
let bundledSkips = 0;
|
|
2810
|
-
for (const entries of resolved.values()) {
|
|
2811
|
-
for (const entry of entries) {
|
|
2812
|
-
if (coreNames.has(entry.name)) {
|
|
2813
|
-
bundledSkips++;
|
|
2814
|
-
continue;
|
|
2815
|
-
}
|
|
2816
|
-
byName.set(entry.name, entry);
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2820
|
-
const cacheDir = path12.join(
|
|
2821
|
-
opts.cacheDir ?? getAnimsCacheDir(),
|
|
2822
|
-
`${manifest.library}-v${manifest.version}`
|
|
2823
|
-
);
|
|
2824
|
-
await fs11.mkdir(cacheDir, { recursive: true });
|
|
2825
|
-
await fs11.mkdir(destDir, { recursive: true });
|
|
2826
|
-
const base = getAnimsBase(opts.animsBase);
|
|
2827
|
-
let installedCount = 0;
|
|
2828
|
-
let presentCount = 0;
|
|
2829
|
-
let addedBytes = 0;
|
|
2830
|
-
const failures = [];
|
|
2831
|
-
for (const entry of wanted) {
|
|
2832
|
-
const dest = path12.join(destDir, entry.file);
|
|
2833
|
-
if (await hasSize(dest, entry.bytes)) {
|
|
2834
|
-
presentCount++;
|
|
2835
|
-
continue;
|
|
2836
|
-
}
|
|
2837
|
-
try {
|
|
2838
|
-
const cached = path12.join(cacheDir, entry.file);
|
|
2839
|
-
if (!await hasSize(cached, entry.bytes)) {
|
|
2840
|
-
const res = await fetch(base + entry.file);
|
|
2841
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2842
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
2843
|
-
await fs11.writeFile(cached, buf);
|
|
2844
|
-
}
|
|
2845
|
-
await fs11.copyFile(cached, dest);
|
|
2846
|
-
installedCount++;
|
|
2847
|
-
addedBytes += entry.bytes;
|
|
2848
|
-
log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
2849
|
-
} catch (err) {
|
|
2850
|
-
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
const previous = await readGameManifest(gameManifestPath);
|
|
2854
|
-
const union = new Set(previous?.clips ?? []);
|
|
2855
|
-
for (const entry of wanted) {
|
|
2856
|
-
if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
|
|
2857
|
-
}
|
|
2858
|
-
const gameManifest = {
|
|
2859
|
-
schema: 1,
|
|
2860
|
-
library: manifest.library,
|
|
2861
|
-
version: manifest.version,
|
|
2862
|
-
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
2863
|
-
};
|
|
2864
|
-
await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
2865
|
-
log.plain("");
|
|
2866
|
-
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
2867
|
-
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
2868
|
-
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
2869
|
-
log.success(
|
|
2870
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
2871
|
-
);
|
|
2872
|
-
for (const [selector, entries] of resolved) {
|
|
2873
|
-
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
2874
|
-
if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
|
|
2875
|
-
}
|
|
2876
|
-
log.info(
|
|
2877
|
-
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
2878
|
-
);
|
|
2879
|
-
if (failures.length > 0) {
|
|
2880
|
-
log.plain("");
|
|
2881
|
-
log.error(
|
|
2882
|
-
`${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
|
|
2883
|
-
);
|
|
2884
|
-
log.plain(
|
|
2885
|
-
" Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
|
|
2886
|
-
);
|
|
2887
|
-
process.exitCode = 1;
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
async function loadManifest(baseOverride) {
|
|
2891
|
-
const base = getAnimsBase(baseOverride);
|
|
2892
|
-
try {
|
|
2893
|
-
const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
|
|
2894
|
-
if (res.ok) {
|
|
2895
|
-
const manifest2 = await res.json();
|
|
2896
|
-
if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
|
|
2897
|
-
return { manifest: manifest2, source: "cdn" };
|
|
2898
|
-
}
|
|
2899
|
-
}
|
|
2900
|
-
} catch {
|
|
2901
|
-
}
|
|
2902
|
-
const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
2903
|
-
const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
|
|
2904
|
-
return { manifest, source: "snapshot" };
|
|
2905
|
-
}
|
|
2906
|
-
function resolveSelectors(manifest, selectors) {
|
|
2907
|
-
const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
2908
|
-
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
2909
|
-
const tags = /* @__PURE__ */ new Map();
|
|
2910
|
-
for (const entry of manifest.clips) {
|
|
2911
|
-
for (const tag of entry.tags) {
|
|
2912
|
-
const list = tags.get(tag) ?? [];
|
|
2913
|
-
list.push(entry);
|
|
2914
|
-
tags.set(tag, list);
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
const out = /* @__PURE__ */ new Map();
|
|
2918
|
-
for (const selector of selectors) {
|
|
2919
|
-
const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
2920
|
-
if (exact) {
|
|
2921
|
-
out.set(selector, [exact]);
|
|
2922
|
-
continue;
|
|
2923
|
-
}
|
|
2924
|
-
const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
|
|
2925
|
-
if (tagHit) {
|
|
2926
|
-
out.set(selector, tagHit);
|
|
2927
|
-
continue;
|
|
2928
|
-
}
|
|
2929
|
-
const candidates = [...tags.keys(), ...byName.keys()];
|
|
2930
|
-
const close = suggest(selector, candidates);
|
|
2931
|
-
throw new Error(
|
|
2932
|
-
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
2933
|
-
"genex controller anims --list"
|
|
2934
|
-
)} for the catalog.`
|
|
2935
|
-
);
|
|
2936
|
-
}
|
|
2937
|
-
return out;
|
|
2938
|
-
}
|
|
2939
|
-
function suggest(input, candidates) {
|
|
2940
|
-
const lower = input.toLowerCase();
|
|
2941
|
-
const scored = [];
|
|
2942
|
-
for (const candidate of candidates) {
|
|
2943
|
-
const candidateLower = candidate.toLowerCase();
|
|
2944
|
-
if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
|
|
2945
|
-
scored.push({ name: candidate, score: 0 });
|
|
2946
|
-
continue;
|
|
2947
|
-
}
|
|
2948
|
-
const distance = levenshtein(lower, candidateLower, 2);
|
|
2949
|
-
if (distance <= 2) scored.push({ name: candidate, score: distance });
|
|
2950
|
-
}
|
|
2951
|
-
scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
2952
|
-
return scored.slice(0, 4).map((s) => s.name);
|
|
2953
|
-
}
|
|
2954
|
-
function levenshtein(a, b, max) {
|
|
2955
|
-
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
2956
|
-
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2957
|
-
for (let i = 1; i <= a.length; i++) {
|
|
2958
|
-
const curr = [i];
|
|
2959
|
-
let rowMin = i;
|
|
2960
|
-
for (let j = 1; j <= b.length; j++) {
|
|
2961
|
-
curr[j] = Math.min(
|
|
2962
|
-
prev[j] + 1,
|
|
2963
|
-
curr[j - 1] + 1,
|
|
2964
|
-
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
2965
|
-
);
|
|
2966
|
-
if (curr[j] < rowMin) rowMin = curr[j];
|
|
2967
|
-
}
|
|
2968
|
-
if (rowMin > max) return max + 1;
|
|
2969
|
-
prev = curr;
|
|
2970
|
-
}
|
|
2971
|
-
return prev[b.length];
|
|
2972
|
-
}
|
|
2973
|
-
function printCatalog(log, manifest, selectors) {
|
|
2974
|
-
const coreNames = new Set(manifest.core);
|
|
2975
|
-
if (selectors.length > 0) {
|
|
2976
|
-
let resolved;
|
|
2977
|
-
try {
|
|
2978
|
-
resolved = resolveSelectors(manifest, selectors);
|
|
2979
|
-
} catch (err) {
|
|
2980
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
2981
|
-
process.exitCode = 1;
|
|
2982
|
-
return;
|
|
2983
|
-
}
|
|
2984
|
-
for (const [selector, entries] of resolved) {
|
|
2985
|
-
log.plain(c.bold(selector));
|
|
2986
|
-
for (const entry of entries) {
|
|
2987
|
-
const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
|
|
2988
|
-
log.plain(
|
|
2989
|
-
` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
|
|
2990
|
-
);
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
const tags = /* @__PURE__ */ new Map();
|
|
2996
|
-
for (const entry of manifest.clips) {
|
|
2997
|
-
for (const tag of entry.tags) {
|
|
2998
|
-
if (tag === HIDDEN_TAG) continue;
|
|
2999
|
-
const list = tags.get(tag) ?? [];
|
|
3000
|
-
list.push(entry);
|
|
3001
|
-
tags.set(tag, list);
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
log.plain(
|
|
3005
|
-
`${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
|
|
3006
|
-
);
|
|
3007
|
-
log.plain(
|
|
3008
|
-
` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
|
|
3009
|
-
"genex controller anims --list <tag>"
|
|
3010
|
-
)}`
|
|
3011
|
-
);
|
|
3012
|
-
log.plain("");
|
|
3013
|
-
for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3014
|
-
const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
3015
|
-
const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
|
|
3016
|
-
log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
async function readGameManifest(file) {
|
|
3020
|
-
try {
|
|
3021
|
-
return JSON.parse(await fs11.readFile(file, "utf8"));
|
|
3022
|
-
} catch {
|
|
3023
|
-
return null;
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
async function hasSize(file, bytes) {
|
|
3027
|
-
try {
|
|
3028
|
-
return (await fs11.stat(file)).size === bytes;
|
|
3029
|
-
} catch {
|
|
3030
|
-
return false;
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
async function exists2(p) {
|
|
3034
|
-
try {
|
|
3035
|
-
await fs11.access(p);
|
|
3036
|
-
return true;
|
|
3037
|
-
} catch {
|
|
3038
|
-
return false;
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
function formatMb(bytes) {
|
|
3042
|
-
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
3043
|
-
}
|
|
2758
|
+
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
2759
|
+
import { createHash } from "crypto";
|
|
3044
2760
|
|
|
3045
|
-
// src/
|
|
3046
|
-
var
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
"touch/touch-joystick.ts",
|
|
3060
|
-
"touch/drag-zone.ts",
|
|
3061
|
-
"touch/rotate-overlay.ts"
|
|
3062
|
-
];
|
|
3063
|
-
var INPUT_AND_CAMERA = [
|
|
3064
|
-
"character/follow-camera.ts",
|
|
3065
|
-
"character/keyboard-input.ts",
|
|
3066
|
-
"character/touch-joystick.ts",
|
|
3067
|
-
...TOUCH_KIT
|
|
3068
|
-
];
|
|
3069
|
-
var NOTICE = "NOTICE.md";
|
|
3070
|
-
var CONTROLLER_FILE_SETS = {
|
|
3071
|
-
character: {
|
|
3072
|
-
code: [
|
|
3073
|
-
...SHARED,
|
|
3074
|
-
"character/character-controller.ts",
|
|
3075
|
-
"character/character-animations.ts",
|
|
3076
|
-
"character/animation-packs.ts",
|
|
3077
|
-
"character/motion-actions.ts",
|
|
3078
|
-
"character/meshy/meshy-loader.ts",
|
|
3079
|
-
"character/presets.ts",
|
|
3080
|
-
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
3081
|
-
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
3082
|
-
"character/vrm/vrm-loader.ts",
|
|
3083
|
-
"character/vrm/vrm-retarget.ts",
|
|
3084
|
-
"character/vrm/capsule-fit.ts",
|
|
3085
|
-
"character/vrm/foot-ik.ts",
|
|
3086
|
-
...INPUT_AND_CAMERA,
|
|
3087
|
-
NOTICE
|
|
3088
|
-
],
|
|
3089
|
-
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
3090
|
-
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
3091
|
-
// a static manifest asset. animation-library.glb (the 12-clip core) still is;
|
|
3092
|
-
// extra clips arrive via `genex controller anims` into public/assets/anims/.
|
|
3093
|
-
assets: ["assets/animation-library.glb"],
|
|
3094
|
-
skill: "genex-threejs-character-controller",
|
|
3095
|
-
sketch: [
|
|
3096
|
-
`const physics = await PhysicsWorld.create();`,
|
|
3097
|
-
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
3098
|
-
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
3099
|
-
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
3100
|
-
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
3101
|
-
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
3102
|
-
]
|
|
2761
|
+
// ../../packages/meshy-animation-catalog/src/generated.ts
|
|
2762
|
+
var MESHY_ANIMATION_CATALOG = [
|
|
2763
|
+
{
|
|
2764
|
+
"actionId": -2,
|
|
2765
|
+
"key": "Walking_man",
|
|
2766
|
+
"name": "Walking",
|
|
2767
|
+
"category": "WalkAndRun",
|
|
2768
|
+
"subCategory": "Walking",
|
|
2769
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking.gif",
|
|
2770
|
+
"rigType": "biped",
|
|
2771
|
+
"tag": null,
|
|
2772
|
+
"isDefault": true,
|
|
2773
|
+
"isFree": true,
|
|
2774
|
+
"createdAt": 1750829487798
|
|
3103
2775
|
},
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
sketch: [
|
|
3117
|
-
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
3118
|
-
`const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
|
|
3119
|
-
`scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
|
|
3120
|
-
]
|
|
2776
|
+
{
|
|
2777
|
+
"actionId": -1,
|
|
2778
|
+
"key": "Running",
|
|
2779
|
+
"name": "Running",
|
|
2780
|
+
"category": "WalkAndRun",
|
|
2781
|
+
"subCategory": "Running",
|
|
2782
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Running.gif",
|
|
2783
|
+
"rigType": "biped",
|
|
2784
|
+
"tag": null,
|
|
2785
|
+
"isDefault": true,
|
|
2786
|
+
"isFree": true,
|
|
2787
|
+
"createdAt": 1750829487790
|
|
3121
2788
|
},
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
3135
|
-
`const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
|
|
3136
|
-
`physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
|
|
3137
|
-
]
|
|
2789
|
+
{
|
|
2790
|
+
"actionId": 0,
|
|
2791
|
+
"key": "Idle",
|
|
2792
|
+
"name": "Idle",
|
|
2793
|
+
"category": "DailyActions",
|
|
2794
|
+
"subCategory": "Idle",
|
|
2795
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Idle.gif",
|
|
2796
|
+
"rigType": "style_01",
|
|
2797
|
+
"tag": null,
|
|
2798
|
+
"isDefault": false,
|
|
2799
|
+
"isFree": true,
|
|
2800
|
+
"createdAt": 1750829487810
|
|
3138
2801
|
},
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
code: [
|
|
3152
|
-
...SHARED,
|
|
3153
|
-
"network/pose.ts",
|
|
3154
|
-
"network/networked-pushable.ts",
|
|
3155
|
-
"network/networked-vehicle.ts",
|
|
3156
|
-
"NETWORKING.md",
|
|
3157
|
-
NOTICE
|
|
3158
|
-
],
|
|
3159
|
-
assets: [],
|
|
3160
|
-
skill: "genex-threejs-multiplayer",
|
|
3161
|
-
sketch: [
|
|
3162
|
-
`const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
|
|
3163
|
-
`physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
|
|
3164
|
-
`contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
|
|
3165
|
-
]
|
|
3166
|
-
}
|
|
3167
|
-
};
|
|
3168
|
-
var CODE_DEST = path13.join("src", "controllers");
|
|
3169
|
-
var ASSETS_DEST = path13.join("public", "assets");
|
|
3170
|
-
async function runController(opts) {
|
|
3171
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
3172
|
-
if (opts.kind?.trim() === "anims") {
|
|
3173
|
-
await runAnims(opts);
|
|
3174
|
-
return;
|
|
3175
|
-
}
|
|
3176
|
-
const kind = opts.kind?.trim();
|
|
3177
|
-
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
3178
|
-
log.error(
|
|
3179
|
-
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
3180
|
-
"genex controller <character|car|drone|touch|networked-physics> [--force]"
|
|
3181
|
-
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
3182
|
-
);
|
|
3183
|
-
process.exitCode = 1;
|
|
3184
|
-
return;
|
|
3185
|
-
}
|
|
3186
|
-
const srcDir = path13.join(getTemplatesDir(), "controllers");
|
|
3187
|
-
const root = opts.cwd ?? process.cwd();
|
|
3188
|
-
const set = CONTROLLER_FILE_SETS[kind];
|
|
3189
|
-
log.plain(c.bold(`genex controller ${kind}`));
|
|
3190
|
-
log.plain("");
|
|
3191
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
|
|
3192
|
-
const plan = [
|
|
3193
|
-
...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
|
|
3194
|
-
...set.assets.map((rel) => ({
|
|
3195
|
-
from: rel,
|
|
3196
|
-
rel: path13.join(ASSETS_DEST, path13.basename(rel))
|
|
3197
|
-
}))
|
|
3198
|
-
];
|
|
3199
|
-
let copied = 0;
|
|
3200
|
-
let skipped = 0;
|
|
3201
|
-
try {
|
|
3202
|
-
for (const file of plan) {
|
|
3203
|
-
const dest = path13.join(root, file.rel);
|
|
3204
|
-
if (!opts.force && await exists3(dest)) {
|
|
3205
|
-
skipped++;
|
|
3206
|
-
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
3207
|
-
continue;
|
|
3208
|
-
}
|
|
3209
|
-
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
3210
|
-
await fs12.copyFile(path13.join(srcDir, file.from), dest);
|
|
3211
|
-
copied++;
|
|
3212
|
-
log.dim(` ${file.rel}`);
|
|
3213
|
-
}
|
|
3214
|
-
} catch (err) {
|
|
3215
|
-
log.error(`Copy failed: ${String(err)}`);
|
|
3216
|
-
process.exitCode = 1;
|
|
3217
|
-
return;
|
|
3218
|
-
}
|
|
3219
|
-
log.success(
|
|
3220
|
-
`Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
|
|
3221
|
-
);
|
|
3222
|
-
log.plain("");
|
|
3223
|
-
if (kind === "character") {
|
|
3224
|
-
if (opts.character) {
|
|
3225
|
-
try {
|
|
3226
|
-
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
3227
|
-
if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
|
|
3228
|
-
await installMeshyCharacterManifest({
|
|
3229
|
-
root,
|
|
3230
|
-
characterId: opts.character,
|
|
3231
|
-
apiUrl: getApiUrl(opts.apiUrl),
|
|
3232
|
-
token,
|
|
3233
|
-
log
|
|
3234
|
-
});
|
|
3235
|
-
} catch (error) {
|
|
3236
|
-
log.error(error instanceof Error ? error.message : String(error));
|
|
3237
|
-
process.exitCode = 1;
|
|
3238
|
-
return;
|
|
3239
|
-
}
|
|
3240
|
-
} else {
|
|
3241
|
-
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
3242
|
-
await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
|
|
3243
|
-
}
|
|
3244
|
-
log.plain("");
|
|
3245
|
-
}
|
|
3246
|
-
log.plain(c.bold("Next steps"));
|
|
3247
|
-
if (kind !== "touch") {
|
|
3248
|
-
log.plain(
|
|
3249
|
-
` 1. ${c.cyan(
|
|
3250
|
-
kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
|
|
3251
|
-
)} (three is already in the scaffold).`
|
|
3252
|
-
);
|
|
3253
|
-
}
|
|
3254
|
-
const stepOffset = kind === "touch" ? 0 : 1;
|
|
3255
|
-
log.plain(
|
|
3256
|
-
` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
|
|
3257
|
-
);
|
|
3258
|
-
log.plain(
|
|
3259
|
-
kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
|
|
3260
|
-
);
|
|
3261
|
-
const sketch = kind === "character" && opts.character ? [
|
|
3262
|
-
`const physics = await PhysicsWorld.create();`,
|
|
3263
|
-
`const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
|
|
3264
|
-
`const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
3265
|
-
`character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
|
|
3266
|
-
`physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
|
|
3267
|
-
] : set.sketch;
|
|
3268
|
-
for (const line of sketch) {
|
|
3269
|
-
log.dim(` ${line}`);
|
|
3270
|
-
}
|
|
3271
|
-
await firstPreviewNudge(log);
|
|
3272
|
-
}
|
|
3273
|
-
async function installMeshyCharacterManifest(args) {
|
|
3274
|
-
const response = await apiFetch(
|
|
3275
|
-
`${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
|
|
3276
|
-
{ headers: { Authorization: `Bearer ${args.token}` } }
|
|
3277
|
-
);
|
|
3278
|
-
if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
|
|
3279
|
-
if (!response.ok) throw new Error(`Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
|
|
3280
|
-
const body = await response.json();
|
|
3281
|
-
const manifest = body.manifest;
|
|
3282
|
-
if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId) {
|
|
3283
|
-
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
3284
|
-
}
|
|
3285
|
-
const destination = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
3286
|
-
await fs12.mkdir(path13.dirname(destination), { recursive: true });
|
|
3287
|
-
await fs12.writeFile(
|
|
3288
|
-
destination,
|
|
3289
|
-
`${JSON.stringify(manifest, null, 2)}
|
|
3290
|
-
`
|
|
3291
|
-
);
|
|
3292
|
-
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
3293
|
-
const pack = manifest.controllerPack;
|
|
3294
|
-
if (typeof pack?.key === "string" && typeof pack.version === "number") {
|
|
3295
|
-
args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
|
|
3296
|
-
} else {
|
|
3297
|
-
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the preview-reviewed neutral-v2 locomotion pack.");
|
|
3298
|
-
}
|
|
3299
|
-
const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
|
|
3300
|
-
args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
|
|
3301
|
-
const slots = Object.keys(manifest.locomotion?.bindings ?? manifest.locomotion?.slots ?? {}).sort();
|
|
3302
|
-
args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
|
|
3303
|
-
const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
|
|
3304
|
-
if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
|
|
3305
|
-
else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
|
|
3306
|
-
}
|
|
3307
|
-
async function installOwnerAvatar(args) {
|
|
3308
|
-
const { root, srcDir, apiUrl, token, log } = args;
|
|
3309
|
-
const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
|
|
3310
|
-
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
3311
|
-
if (token) {
|
|
3312
|
-
try {
|
|
3313
|
-
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
3314
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
3315
|
-
});
|
|
3316
|
-
if (res.ok) {
|
|
3317
|
-
const me = await res.json();
|
|
3318
|
-
if (me.vrmUrl) {
|
|
3319
|
-
const vrmRes = await fetch(me.vrmUrl);
|
|
3320
|
-
if (vrmRes.ok) {
|
|
3321
|
-
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
3322
|
-
await fs12.writeFile(dest, buf);
|
|
3323
|
-
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
3324
|
-
return;
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
log.dim(" couldn't fetch your avatar; using the bundled default.");
|
|
3329
|
-
} catch {
|
|
3330
|
-
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
3334
|
-
log.dim(
|
|
3335
|
-
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
3336
|
-
);
|
|
3337
|
-
}
|
|
3338
|
-
async function exists3(p) {
|
|
3339
|
-
try {
|
|
3340
|
-
await fs12.access(p);
|
|
3341
|
-
return true;
|
|
3342
|
-
} catch {
|
|
3343
|
-
return false;
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
// ../../packages/meshy-animation-catalog/src/generated.ts
|
|
3348
|
-
var MESHY_ANIMATION_CATALOG = [
|
|
3349
|
-
{
|
|
3350
|
-
"actionId": -2,
|
|
3351
|
-
"key": "Walking_man",
|
|
3352
|
-
"name": "Walking",
|
|
3353
|
-
"category": "WalkAndRun",
|
|
3354
|
-
"subCategory": "Walking",
|
|
3355
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking.gif",
|
|
3356
|
-
"rigType": "biped",
|
|
3357
|
-
"tag": null,
|
|
3358
|
-
"isDefault": true,
|
|
3359
|
-
"isFree": true,
|
|
3360
|
-
"createdAt": 1750829487798
|
|
3361
|
-
},
|
|
3362
|
-
{
|
|
3363
|
-
"actionId": -1,
|
|
3364
|
-
"key": "Running",
|
|
3365
|
-
"name": "Running",
|
|
3366
|
-
"category": "WalkAndRun",
|
|
3367
|
-
"subCategory": "Running",
|
|
3368
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Running.gif",
|
|
3369
|
-
"rigType": "biped",
|
|
3370
|
-
"tag": null,
|
|
3371
|
-
"isDefault": true,
|
|
3372
|
-
"isFree": true,
|
|
3373
|
-
"createdAt": 1750829487790
|
|
3374
|
-
},
|
|
3375
|
-
{
|
|
3376
|
-
"actionId": 0,
|
|
3377
|
-
"key": "Idle",
|
|
3378
|
-
"name": "Idle",
|
|
3379
|
-
"category": "DailyActions",
|
|
3380
|
-
"subCategory": "Idle",
|
|
3381
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Idle.gif",
|
|
3382
|
-
"rigType": "style_01",
|
|
3383
|
-
"tag": null,
|
|
3384
|
-
"isDefault": false,
|
|
3385
|
-
"isFree": true,
|
|
3386
|
-
"createdAt": 1750829487810
|
|
3387
|
-
},
|
|
3388
|
-
{
|
|
3389
|
-
"actionId": 1,
|
|
3390
|
-
"key": "Walking_Woman",
|
|
3391
|
-
"name": "Walking Woman",
|
|
3392
|
-
"category": "WalkAndRun",
|
|
3393
|
-
"subCategory": "Walking",
|
|
3394
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking_Woman_woman.gif",
|
|
3395
|
-
"rigType": "style_01",
|
|
3396
|
-
"tag": null,
|
|
3397
|
-
"isDefault": false,
|
|
3398
|
-
"isFree": true,
|
|
3399
|
-
"createdAt": 1750829487808
|
|
2802
|
+
{
|
|
2803
|
+
"actionId": 1,
|
|
2804
|
+
"key": "Walking_Woman",
|
|
2805
|
+
"name": "Walking Woman",
|
|
2806
|
+
"category": "WalkAndRun",
|
|
2807
|
+
"subCategory": "Walking",
|
|
2808
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking_Woman_woman.gif",
|
|
2809
|
+
"rigType": "style_01",
|
|
2810
|
+
"tag": null,
|
|
2811
|
+
"isDefault": false,
|
|
2812
|
+
"isFree": true,
|
|
2813
|
+
"createdAt": 1750829487808
|
|
3400
2814
|
},
|
|
3401
2815
|
{
|
|
3402
2816
|
"actionId": 2,
|
|
@@ -12217,6 +11631,12 @@ var CURATED = {
|
|
|
12217
11631
|
controllerSlots: ["crouch.forward"],
|
|
12218
11632
|
reviewStatus: "preview-reviewed"
|
|
12219
11633
|
},
|
|
11634
|
+
[657]: {
|
|
11635
|
+
loop: true,
|
|
11636
|
+
motionPolicy: "controller-loop",
|
|
11637
|
+
controllerSlots: ["run.forward"],
|
|
11638
|
+
reviewStatus: "preview-reviewed"
|
|
11639
|
+
},
|
|
12220
11640
|
[658]: {
|
|
12221
11641
|
loop: true,
|
|
12222
11642
|
motionPolicy: "controller-loop",
|
|
@@ -12226,169 +11646,946 @@ var CURATED = {
|
|
|
12226
11646
|
[659]: {
|
|
12227
11647
|
loop: true,
|
|
12228
11648
|
motionPolicy: "controller-loop",
|
|
12229
|
-
controllerSlots: [
|
|
12230
|
-
reviewStatus: "
|
|
11649
|
+
controllerSlots: [],
|
|
11650
|
+
reviewStatus: "rejected"
|
|
11651
|
+
}
|
|
11652
|
+
};
|
|
11653
|
+
var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
|
|
11654
|
+
var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
|
|
11655
|
+
"Climbing",
|
|
11656
|
+
"HangingfromLedge",
|
|
11657
|
+
"VaultingOverObstacle",
|
|
11658
|
+
"Interacting",
|
|
11659
|
+
"PickingUpItem",
|
|
11660
|
+
"Pushing",
|
|
11661
|
+
"Sleeping"
|
|
11662
|
+
]);
|
|
11663
|
+
var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
|
|
11664
|
+
var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
|
|
11665
|
+
function words(value) {
|
|
11666
|
+
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
11667
|
+
}
|
|
11668
|
+
function inferRequirements(key, subCategory) {
|
|
11669
|
+
const lower = `${key} ${subCategory}`.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
11670
|
+
const props = [
|
|
11671
|
+
["gun", /\b(?:gun|rifle|pistol)\b/],
|
|
11672
|
+
["sword", /\b(?:sword|blade)\b/],
|
|
11673
|
+
["bow", /\b(?:bow|arrow)\b/],
|
|
11674
|
+
["shield", /\bshield\b/],
|
|
11675
|
+
["chair", /\b(?:chair|sit|sitting)\b/],
|
|
11676
|
+
["ladder", /\bladder\b/],
|
|
11677
|
+
["rope", /\brope\b/]
|
|
11678
|
+
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
11679
|
+
const environment = [
|
|
11680
|
+
["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
|
|
11681
|
+
["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
|
|
11682
|
+
["water", /\b(?:swim|swimming)\b/],
|
|
11683
|
+
["seat", /\b(?:sit|sitting|chair)\b/]
|
|
11684
|
+
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
11685
|
+
return { props, environment, partner: /\b(?:partner|carry person|hug|handshake)\b/.test(lower) };
|
|
11686
|
+
}
|
|
11687
|
+
function inferSlots(key) {
|
|
11688
|
+
const lower = key.toLowerCase();
|
|
11689
|
+
const band = lower.includes("crouch") ? "crouch" : lower.includes("run") || lower.includes("sprint") ? "run" : lower.includes("walk") ? "walk" : null;
|
|
11690
|
+
if (!band) return [];
|
|
11691
|
+
const direction = lower.includes("backleft") ? "back-left" : lower.includes("backright") ? "back-right" : lower.includes("backward") || lower.includes("_back") ? "backward" : lower.includes("left") ? "left" : lower.includes("right") ? "right" : "forward";
|
|
11692
|
+
return [`${band}.${direction}`];
|
|
11693
|
+
}
|
|
11694
|
+
function curateMeshyAnimation(raw) {
|
|
11695
|
+
const inPlace = raw.tag === "InPlace";
|
|
11696
|
+
const requirements = inferRequirements(raw.key, raw.subCategory);
|
|
11697
|
+
const choreography = CHOREOGRAPHY_SUBCATEGORIES.has(raw.subCategory) || requirements.partner;
|
|
11698
|
+
const loop = inPlace && LOOP_SUBCATEGORIES.has(raw.subCategory) && !NON_LOOP_ACTION.test(`${raw.key} ${raw.name}`);
|
|
11699
|
+
const motionPolicy = loop ? "controller-loop" : choreography ? "choreography" : PLANAR_ACTION.test(`${raw.key} ${raw.name}`) ? "planar-root-action" : "anchored-action";
|
|
11700
|
+
const base = {
|
|
11701
|
+
actionId: raw.actionId,
|
|
11702
|
+
key: raw.key,
|
|
11703
|
+
name: raw.name,
|
|
11704
|
+
category: raw.category,
|
|
11705
|
+
subCategory: raw.subCategory,
|
|
11706
|
+
previewUrl: raw.previewUrl,
|
|
11707
|
+
rigType: raw.rigType,
|
|
11708
|
+
inPlace,
|
|
11709
|
+
isDefault: raw.isDefault,
|
|
11710
|
+
isFree: raw.isFree,
|
|
11711
|
+
createdAt: raw.createdAt,
|
|
11712
|
+
aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
|
|
11713
|
+
gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
|
|
11714
|
+
loop,
|
|
11715
|
+
motionPolicy,
|
|
11716
|
+
rootMotionValidated: false,
|
|
11717
|
+
// A name that contains "walk" or "run" is not enough to make a safe
|
|
11718
|
+
// controller loop. Only provider-declared InPlace loops (plus explicit
|
|
11719
|
+
// measured overrides above) may populate automatic locomotion slots.
|
|
11720
|
+
controllerSlots: loop ? inferSlots(raw.key) : [],
|
|
11721
|
+
requirements,
|
|
11722
|
+
reviewStatus: "metadata-reviewed"
|
|
11723
|
+
};
|
|
11724
|
+
return { ...base, ...CURATED[raw.actionId], requirements };
|
|
11725
|
+
}
|
|
11726
|
+
|
|
11727
|
+
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
11728
|
+
function normalizedPackDefinition(definition) {
|
|
11729
|
+
const key = definition.key.trim();
|
|
11730
|
+
if (!key) throw new Error("Meshy controller-pack key cannot be empty");
|
|
11731
|
+
if (!Number.isInteger(definition.version) || definition.version < 1) {
|
|
11732
|
+
throw new Error("Meshy controller-pack version must be a positive integer");
|
|
11733
|
+
}
|
|
11734
|
+
const actionIds = [...definition.actionIds];
|
|
11735
|
+
if (actionIds.some((actionId) => !Number.isInteger(actionId))) {
|
|
11736
|
+
throw new Error("Meshy controller-pack action IDs must be integers");
|
|
11737
|
+
}
|
|
11738
|
+
if (new Set(actionIds).size !== actionIds.length) {
|
|
11739
|
+
throw new Error("Meshy controller-pack action IDs must be unique");
|
|
11740
|
+
}
|
|
11741
|
+
const allowed = new Set(actionIds);
|
|
11742
|
+
const bindings = {};
|
|
11743
|
+
for (const [slot, binding] of Object.entries(definition.bindings).sort(([left], [right]) => left.localeCompare(right))) {
|
|
11744
|
+
if (!slot.trim()) throw new Error("Meshy controller-pack slots cannot be empty");
|
|
11745
|
+
if (!allowed.has(binding.actionId)) {
|
|
11746
|
+
throw new Error(`Meshy controller-pack slot ${slot} refers to action ${binding.actionId} outside its action set`);
|
|
11747
|
+
}
|
|
11748
|
+
if (binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
|
|
11749
|
+
throw new Error(`Meshy controller-pack slot ${slot} has an invalid playback mode`);
|
|
11750
|
+
}
|
|
11751
|
+
if (binding.phase !== void 0 && (!Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
|
|
11752
|
+
throw new Error(`Meshy controller-pack slot ${slot} has an invalid normalized phase`);
|
|
11753
|
+
}
|
|
11754
|
+
bindings[slot] = {
|
|
11755
|
+
actionId: binding.actionId,
|
|
11756
|
+
mode: binding.mode,
|
|
11757
|
+
...binding.phase === void 0 ? {} : { phase: binding.phase }
|
|
11758
|
+
};
|
|
11759
|
+
}
|
|
11760
|
+
return { key, version: definition.version, actionIds, bindings };
|
|
11761
|
+
}
|
|
11762
|
+
function packFingerprint(definition) {
|
|
11763
|
+
return createHash("sha256").update(JSON.stringify(definition)).digest("hex");
|
|
11764
|
+
}
|
|
11765
|
+
function createMeshyControllerPackSnapshot(definition) {
|
|
11766
|
+
const normalized = normalizedPackDefinition(definition);
|
|
11767
|
+
const actionIds = Object.freeze([...normalized.actionIds]);
|
|
11768
|
+
const bindings = Object.freeze(Object.fromEntries(
|
|
11769
|
+
Object.entries(normalized.bindings).map(([slot, binding]) => [slot, Object.freeze({ ...binding })])
|
|
11770
|
+
));
|
|
11771
|
+
return Object.freeze({
|
|
11772
|
+
...normalized,
|
|
11773
|
+
actionIds,
|
|
11774
|
+
bindings,
|
|
11775
|
+
fingerprint: packFingerprint(normalized)
|
|
11776
|
+
});
|
|
11777
|
+
}
|
|
11778
|
+
var NEUTRAL_CONTROLLER_BINDINGS = {
|
|
11779
|
+
"idle.default": { actionId: 243, mode: "loop" },
|
|
11780
|
+
"walk.forward": { actionId: 613, mode: "loop" },
|
|
11781
|
+
"run.forward": { actionId: 657, mode: "loop" },
|
|
11782
|
+
"crouch.forward": { actionId: 616, mode: "loop" },
|
|
11783
|
+
// The reviewed opening frame is a stable lowered stance; the loader samples
|
|
11784
|
+
// it into a separate static clip so crouch movement can keep the full loop.
|
|
11785
|
+
"crouch.idle": { actionId: 616, mode: "pose", phase: 0 },
|
|
11786
|
+
"jump.full": { actionId: 466, mode: "one-shot" }
|
|
11787
|
+
};
|
|
11788
|
+
var MESHY_CONTROLLER_PACK = createMeshyControllerPackSnapshot({
|
|
11789
|
+
key: "neutral-v3",
|
|
11790
|
+
version: 3,
|
|
11791
|
+
actionIds: [243, 613, 657, 616, 466],
|
|
11792
|
+
bindings: NEUTRAL_CONTROLLER_BINDINGS
|
|
11793
|
+
});
|
|
11794
|
+
var MESHY_CONTROLLER_PACK_KEY = MESHY_CONTROLLER_PACK.key;
|
|
11795
|
+
var MESHY_CONTROLLER_PACK_VERSION = MESHY_CONTROLLER_PACK.version;
|
|
11796
|
+
var MESHY_CONTROLLER_PACK_FINGERPRINT = MESHY_CONTROLLER_PACK.fingerprint;
|
|
11797
|
+
var MESHY_CONTROLLER_CORE_ACTION_IDS = MESHY_CONTROLLER_PACK.actionIds;
|
|
11798
|
+
var MESHY_CONTROLLER_BINDINGS = MESHY_CONTROLLER_PACK.bindings;
|
|
11799
|
+
var SYNONYMS = {
|
|
11800
|
+
attack: ["fight", "punch", "kick", "weapon", "combat"],
|
|
11801
|
+
combat: ["fight", "attack", "punch", "weapon"],
|
|
11802
|
+
crouch: ["sneak", "stealth"],
|
|
11803
|
+
die: ["death", "dying", "fall"],
|
|
11804
|
+
emote: ["gesture", "acting", "dance"],
|
|
11805
|
+
gun: ["rifle", "pistol", "shoot", "firearm"],
|
|
11806
|
+
idle: ["stand", "breathing"],
|
|
11807
|
+
jump: ["leap", "vault"],
|
|
11808
|
+
run: ["running", "jog", "sprint", "charge"],
|
|
11809
|
+
sit: ["sitting", "chair", "seat"],
|
|
11810
|
+
skate: ["skateboard", "skating", "board"],
|
|
11811
|
+
sword: ["blade", "weapon", "slash"],
|
|
11812
|
+
walk: ["walking", "stride", "stroll"],
|
|
11813
|
+
wave: ["hello", "greeting", "gesture"]
|
|
11814
|
+
};
|
|
11815
|
+
var STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "and", "for", "of", "the", "to", "with"]);
|
|
11816
|
+
function tokens(value) {
|
|
11817
|
+
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((token) => token && !STOP_WORDS.has(token));
|
|
11818
|
+
}
|
|
11819
|
+
function tokenGroup(token) {
|
|
11820
|
+
const group = /* @__PURE__ */ new Set([token, ...SYNONYMS[token] ?? []]);
|
|
11821
|
+
for (const [canonical, synonyms] of Object.entries(SYNONYMS)) {
|
|
11822
|
+
if (synonyms.includes(token)) {
|
|
11823
|
+
group.add(canonical);
|
|
11824
|
+
for (const synonym of synonyms) group.add(synonym);
|
|
11825
|
+
}
|
|
11826
|
+
}
|
|
11827
|
+
return group;
|
|
11828
|
+
}
|
|
11829
|
+
var MESHY_ANIMATIONS = MESHY_ANIMATION_CATALOG.map(curateMeshyAnimation);
|
|
11830
|
+
var MESHY_ANIMATIONS_BY_ID = new Map(MESHY_ANIMATIONS.map((entry) => [entry.actionId, entry]));
|
|
11831
|
+
var MESHY_ANIMATIONS_BY_KEY = new Map(MESHY_ANIMATIONS.map((entry) => [entry.key.toLowerCase(), entry]));
|
|
11832
|
+
function animationById(actionId) {
|
|
11833
|
+
return MESHY_ANIMATIONS_BY_ID.get(actionId);
|
|
11834
|
+
}
|
|
11835
|
+
function searchMeshyAnimations(query, options = {}) {
|
|
11836
|
+
const rawQuery = query.trim().toLowerCase();
|
|
11837
|
+
const queryTokens = tokens(query);
|
|
11838
|
+
const groups = queryTokens.map(tokenGroup);
|
|
11839
|
+
const expanded = new Set(groups.flatMap((group) => [...group]));
|
|
11840
|
+
const results = [];
|
|
11841
|
+
for (const entry of MESHY_ANIMATIONS) {
|
|
11842
|
+
if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
|
|
11843
|
+
if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
|
|
11844
|
+
const matched = [];
|
|
11845
|
+
let score = 0;
|
|
11846
|
+
if (String(entry.actionId) === rawQuery) {
|
|
11847
|
+
score += 1e5;
|
|
11848
|
+
matched.push("action id");
|
|
11849
|
+
}
|
|
11850
|
+
if (entry.key.toLowerCase() === rawQuery) {
|
|
11851
|
+
score += 8e4;
|
|
11852
|
+
matched.push("stable key");
|
|
11853
|
+
}
|
|
11854
|
+
if (entry.name.toLowerCase() === rawQuery) {
|
|
11855
|
+
score += 6e4;
|
|
11856
|
+
matched.push("exact name");
|
|
11857
|
+
}
|
|
11858
|
+
const haystack = new Set(tokens([
|
|
11859
|
+
entry.key,
|
|
11860
|
+
entry.name,
|
|
11861
|
+
entry.category,
|
|
11862
|
+
entry.subCategory,
|
|
11863
|
+
...entry.aliases,
|
|
11864
|
+
...entry.gameplayTags
|
|
11865
|
+
].join(" ")));
|
|
11866
|
+
let coveredGroups = 0;
|
|
11867
|
+
for (let index = 0; index < groups.length; index++) {
|
|
11868
|
+
const hits = [...groups[index]].filter((token) => haystack.has(token));
|
|
11869
|
+
if (hits.length === 0) continue;
|
|
11870
|
+
coveredGroups++;
|
|
11871
|
+
const original = queryTokens[index];
|
|
11872
|
+
const best = hits.includes(original) ? original : hits[0];
|
|
11873
|
+
score += best === original ? 400 : 140;
|
|
11874
|
+
matched.push(best);
|
|
11875
|
+
}
|
|
11876
|
+
if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score += 35;
|
|
11877
|
+
const exact = score >= 6e4;
|
|
11878
|
+
if (score > 0 && (exact || coveredGroups === groups.length)) {
|
|
11879
|
+
results.push({ entry, score, matched: [...new Set(matched)] });
|
|
11880
|
+
}
|
|
11881
|
+
}
|
|
11882
|
+
return results.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name) || a.entry.actionId - b.entry.actionId).slice(0, options.limit ?? 20);
|
|
11883
|
+
}
|
|
11884
|
+
|
|
11885
|
+
// src/lib/anims.ts
|
|
11886
|
+
import fs11 from "fs/promises";
|
|
11887
|
+
import path12 from "path";
|
|
11888
|
+
var ANIMS_DEST = path12.join("public", "assets", "anims");
|
|
11889
|
+
var HIDDEN_TAG = "reference";
|
|
11890
|
+
async function runAnims(opts) {
|
|
11891
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
11892
|
+
const root = opts.cwd ?? process.cwd();
|
|
11893
|
+
const selectors = opts.selectors ?? [];
|
|
11894
|
+
log.plain(c.bold("genex controller anims"));
|
|
11895
|
+
log.plain("");
|
|
11896
|
+
const { manifest, source } = await loadManifest(opts.animsBase);
|
|
11897
|
+
if (source === "snapshot") {
|
|
11898
|
+
log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
|
|
11899
|
+
}
|
|
11900
|
+
if (opts.list) {
|
|
11901
|
+
printCatalog(log, manifest, selectors);
|
|
11902
|
+
return;
|
|
11903
|
+
}
|
|
11904
|
+
const controllerMarker = path12.join(root, "src", "controllers", "character");
|
|
11905
|
+
if (!await exists2(controllerMarker)) {
|
|
11906
|
+
log.error(
|
|
11907
|
+
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
11908
|
+
);
|
|
11909
|
+
log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
|
|
11910
|
+
process.exitCode = 1;
|
|
11911
|
+
return;
|
|
11912
|
+
}
|
|
11913
|
+
const destDir = path12.join(root, ANIMS_DEST);
|
|
11914
|
+
const gameManifestPath = path12.join(destDir, "manifest.json");
|
|
11915
|
+
if (opts.reset) {
|
|
11916
|
+
await fs11.rm(destDir, { recursive: true, force: true });
|
|
11917
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
|
|
11918
|
+
}
|
|
11919
|
+
if (selectors.length === 0) {
|
|
11920
|
+
const installed = await readGameManifest(gameManifestPath);
|
|
11921
|
+
if (installed === null || installed.clips.length === 0) {
|
|
11922
|
+
log.plain(" No animation packs installed yet.");
|
|
11923
|
+
} else {
|
|
11924
|
+
log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
|
|
11925
|
+
}
|
|
11926
|
+
log.plain("");
|
|
11927
|
+
log.plain(
|
|
11928
|
+
` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
|
|
11929
|
+
"genex controller anims --list"
|
|
11930
|
+
)}.`
|
|
11931
|
+
);
|
|
11932
|
+
return;
|
|
11933
|
+
}
|
|
11934
|
+
let resolved;
|
|
11935
|
+
try {
|
|
11936
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
11937
|
+
} catch (err) {
|
|
11938
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
11939
|
+
process.exitCode = 1;
|
|
11940
|
+
return;
|
|
11941
|
+
}
|
|
11942
|
+
const coreNames = new Set(manifest.core);
|
|
11943
|
+
const byName = /* @__PURE__ */ new Map();
|
|
11944
|
+
let bundledSkips = 0;
|
|
11945
|
+
for (const entries of resolved.values()) {
|
|
11946
|
+
for (const entry of entries) {
|
|
11947
|
+
if (coreNames.has(entry.name)) {
|
|
11948
|
+
bundledSkips++;
|
|
11949
|
+
continue;
|
|
11950
|
+
}
|
|
11951
|
+
byName.set(entry.name, entry);
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11954
|
+
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
11955
|
+
const cacheDir = path12.join(
|
|
11956
|
+
opts.cacheDir ?? getAnimsCacheDir(),
|
|
11957
|
+
`${manifest.library}-v${manifest.version}`
|
|
11958
|
+
);
|
|
11959
|
+
await fs11.mkdir(cacheDir, { recursive: true });
|
|
11960
|
+
await fs11.mkdir(destDir, { recursive: true });
|
|
11961
|
+
const base = getAnimsBase(opts.animsBase);
|
|
11962
|
+
let installedCount = 0;
|
|
11963
|
+
let presentCount = 0;
|
|
11964
|
+
let addedBytes = 0;
|
|
11965
|
+
const failures = [];
|
|
11966
|
+
for (const entry of wanted) {
|
|
11967
|
+
const dest = path12.join(destDir, entry.file);
|
|
11968
|
+
if (await hasSize(dest, entry.bytes)) {
|
|
11969
|
+
presentCount++;
|
|
11970
|
+
continue;
|
|
11971
|
+
}
|
|
11972
|
+
try {
|
|
11973
|
+
const cached = path12.join(cacheDir, entry.file);
|
|
11974
|
+
if (!await hasSize(cached, entry.bytes)) {
|
|
11975
|
+
const res = await fetch(base + entry.file);
|
|
11976
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
11977
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
11978
|
+
await fs11.writeFile(cached, buf);
|
|
11979
|
+
}
|
|
11980
|
+
await fs11.copyFile(cached, dest);
|
|
11981
|
+
installedCount++;
|
|
11982
|
+
addedBytes += entry.bytes;
|
|
11983
|
+
log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
11984
|
+
} catch (err) {
|
|
11985
|
+
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
const previous = await readGameManifest(gameManifestPath);
|
|
11989
|
+
const union = new Set(previous?.clips ?? []);
|
|
11990
|
+
for (const entry of wanted) {
|
|
11991
|
+
if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
|
|
11992
|
+
}
|
|
11993
|
+
const gameManifest = {
|
|
11994
|
+
schema: 1,
|
|
11995
|
+
library: manifest.library,
|
|
11996
|
+
version: manifest.version,
|
|
11997
|
+
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
11998
|
+
};
|
|
11999
|
+
await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
12000
|
+
log.plain("");
|
|
12001
|
+
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
12002
|
+
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
12003
|
+
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
12004
|
+
log.success(
|
|
12005
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
12006
|
+
);
|
|
12007
|
+
for (const [selector, entries] of resolved) {
|
|
12008
|
+
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
12009
|
+
if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
|
|
12010
|
+
}
|
|
12011
|
+
log.info(
|
|
12012
|
+
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
12013
|
+
);
|
|
12014
|
+
if (failures.length > 0) {
|
|
12015
|
+
log.plain("");
|
|
12016
|
+
log.error(
|
|
12017
|
+
`${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
|
|
12018
|
+
);
|
|
12019
|
+
log.plain(
|
|
12020
|
+
" Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
|
|
12021
|
+
);
|
|
12022
|
+
process.exitCode = 1;
|
|
12023
|
+
}
|
|
12024
|
+
}
|
|
12025
|
+
async function loadManifest(baseOverride) {
|
|
12026
|
+
const base = getAnimsBase(baseOverride);
|
|
12027
|
+
try {
|
|
12028
|
+
const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
|
|
12029
|
+
if (res.ok) {
|
|
12030
|
+
const manifest2 = await res.json();
|
|
12031
|
+
if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
|
|
12032
|
+
return { manifest: manifest2, source: "cdn" };
|
|
12033
|
+
}
|
|
12034
|
+
}
|
|
12035
|
+
} catch {
|
|
12036
|
+
}
|
|
12037
|
+
const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
12038
|
+
const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
|
|
12039
|
+
return { manifest, source: "snapshot" };
|
|
12040
|
+
}
|
|
12041
|
+
function resolveSelectors(manifest, selectors) {
|
|
12042
|
+
const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
12043
|
+
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
12044
|
+
const tags = /* @__PURE__ */ new Map();
|
|
12045
|
+
for (const entry of manifest.clips) {
|
|
12046
|
+
for (const tag of entry.tags) {
|
|
12047
|
+
const list = tags.get(tag) ?? [];
|
|
12048
|
+
list.push(entry);
|
|
12049
|
+
tags.set(tag, list);
|
|
12050
|
+
}
|
|
12051
|
+
}
|
|
12052
|
+
const out = /* @__PURE__ */ new Map();
|
|
12053
|
+
for (const selector of selectors) {
|
|
12054
|
+
const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
12055
|
+
if (exact) {
|
|
12056
|
+
out.set(selector, [exact]);
|
|
12057
|
+
continue;
|
|
12058
|
+
}
|
|
12059
|
+
const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
|
|
12060
|
+
if (tagHit) {
|
|
12061
|
+
out.set(selector, tagHit);
|
|
12062
|
+
continue;
|
|
12063
|
+
}
|
|
12064
|
+
const candidates = [...tags.keys(), ...byName.keys()];
|
|
12065
|
+
const close = suggest(selector, candidates);
|
|
12066
|
+
throw new Error(
|
|
12067
|
+
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
12068
|
+
"genex controller anims --list"
|
|
12069
|
+
)} for the catalog.`
|
|
12070
|
+
);
|
|
12071
|
+
}
|
|
12072
|
+
return out;
|
|
12073
|
+
}
|
|
12074
|
+
function suggest(input, candidates) {
|
|
12075
|
+
const lower = input.toLowerCase();
|
|
12076
|
+
const scored = [];
|
|
12077
|
+
for (const candidate of candidates) {
|
|
12078
|
+
const candidateLower = candidate.toLowerCase();
|
|
12079
|
+
if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
|
|
12080
|
+
scored.push({ name: candidate, score: 0 });
|
|
12081
|
+
continue;
|
|
12082
|
+
}
|
|
12083
|
+
const distance = levenshtein(lower, candidateLower, 2);
|
|
12084
|
+
if (distance <= 2) scored.push({ name: candidate, score: distance });
|
|
12085
|
+
}
|
|
12086
|
+
scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
12087
|
+
return scored.slice(0, 4).map((s) => s.name);
|
|
12088
|
+
}
|
|
12089
|
+
function levenshtein(a, b, max) {
|
|
12090
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
12091
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
12092
|
+
for (let i = 1; i <= a.length; i++) {
|
|
12093
|
+
const curr = [i];
|
|
12094
|
+
let rowMin = i;
|
|
12095
|
+
for (let j = 1; j <= b.length; j++) {
|
|
12096
|
+
curr[j] = Math.min(
|
|
12097
|
+
prev[j] + 1,
|
|
12098
|
+
curr[j - 1] + 1,
|
|
12099
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
12100
|
+
);
|
|
12101
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
12102
|
+
}
|
|
12103
|
+
if (rowMin > max) return max + 1;
|
|
12104
|
+
prev = curr;
|
|
12105
|
+
}
|
|
12106
|
+
return prev[b.length];
|
|
12107
|
+
}
|
|
12108
|
+
function printCatalog(log, manifest, selectors) {
|
|
12109
|
+
const coreNames = new Set(manifest.core);
|
|
12110
|
+
if (selectors.length > 0) {
|
|
12111
|
+
let resolved;
|
|
12112
|
+
try {
|
|
12113
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
12114
|
+
} catch (err) {
|
|
12115
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
12116
|
+
process.exitCode = 1;
|
|
12117
|
+
return;
|
|
12118
|
+
}
|
|
12119
|
+
for (const [selector, entries] of resolved) {
|
|
12120
|
+
log.plain(c.bold(selector));
|
|
12121
|
+
for (const entry of entries) {
|
|
12122
|
+
const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
|
|
12123
|
+
log.plain(
|
|
12124
|
+
` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
|
|
12125
|
+
);
|
|
12126
|
+
}
|
|
12127
|
+
}
|
|
12128
|
+
return;
|
|
12129
|
+
}
|
|
12130
|
+
const tags = /* @__PURE__ */ new Map();
|
|
12131
|
+
for (const entry of manifest.clips) {
|
|
12132
|
+
for (const tag of entry.tags) {
|
|
12133
|
+
if (tag === HIDDEN_TAG) continue;
|
|
12134
|
+
const list = tags.get(tag) ?? [];
|
|
12135
|
+
list.push(entry);
|
|
12136
|
+
tags.set(tag, list);
|
|
12137
|
+
}
|
|
12138
|
+
}
|
|
12139
|
+
log.plain(
|
|
12140
|
+
`${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
|
|
12141
|
+
);
|
|
12142
|
+
log.plain(
|
|
12143
|
+
` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
|
|
12144
|
+
"genex controller anims --list <tag>"
|
|
12145
|
+
)}`
|
|
12146
|
+
);
|
|
12147
|
+
log.plain("");
|
|
12148
|
+
for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
12149
|
+
const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
12150
|
+
const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
|
|
12151
|
+
log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
|
|
12231
12152
|
}
|
|
12232
|
-
};
|
|
12233
|
-
var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
|
|
12234
|
-
var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
|
|
12235
|
-
"Climbing",
|
|
12236
|
-
"HangingfromLedge",
|
|
12237
|
-
"VaultingOverObstacle",
|
|
12238
|
-
"Interacting",
|
|
12239
|
-
"PickingUpItem",
|
|
12240
|
-
"Pushing",
|
|
12241
|
-
"Sleeping"
|
|
12242
|
-
]);
|
|
12243
|
-
var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
|
|
12244
|
-
var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
|
|
12245
|
-
function words(value) {
|
|
12246
|
-
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
12247
12153
|
}
|
|
12248
|
-
function
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
["shield", /\bshield\b/],
|
|
12255
|
-
["chair", /\b(?:chair|sit|sitting)\b/],
|
|
12256
|
-
["ladder", /\bladder\b/],
|
|
12257
|
-
["rope", /\brope\b/]
|
|
12258
|
-
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
12259
|
-
const environment = [
|
|
12260
|
-
["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
|
|
12261
|
-
["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
|
|
12262
|
-
["water", /\b(?:swim|swimming)\b/],
|
|
12263
|
-
["seat", /\b(?:sit|sitting|chair)\b/]
|
|
12264
|
-
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
12265
|
-
return { props, environment, partner: /\b(?:partner|carry person|hug|handshake)\b/.test(lower) };
|
|
12154
|
+
async function readGameManifest(file) {
|
|
12155
|
+
try {
|
|
12156
|
+
return JSON.parse(await fs11.readFile(file, "utf8"));
|
|
12157
|
+
} catch {
|
|
12158
|
+
return null;
|
|
12159
|
+
}
|
|
12266
12160
|
}
|
|
12267
|
-
function
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12161
|
+
async function hasSize(file, bytes) {
|
|
12162
|
+
try {
|
|
12163
|
+
return (await fs11.stat(file)).size === bytes;
|
|
12164
|
+
} catch {
|
|
12165
|
+
return false;
|
|
12166
|
+
}
|
|
12273
12167
|
}
|
|
12274
|
-
function
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
category: raw.category,
|
|
12285
|
-
subCategory: raw.subCategory,
|
|
12286
|
-
previewUrl: raw.previewUrl,
|
|
12287
|
-
rigType: raw.rigType,
|
|
12288
|
-
inPlace,
|
|
12289
|
-
isDefault: raw.isDefault,
|
|
12290
|
-
isFree: raw.isFree,
|
|
12291
|
-
createdAt: raw.createdAt,
|
|
12292
|
-
aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
|
|
12293
|
-
gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
|
|
12294
|
-
loop,
|
|
12295
|
-
motionPolicy,
|
|
12296
|
-
rootMotionValidated: false,
|
|
12297
|
-
// A name that contains "walk" or "run" is not enough to make a safe
|
|
12298
|
-
// controller loop. Only provider-declared InPlace loops (plus explicit
|
|
12299
|
-
// measured overrides above) may populate automatic locomotion slots.
|
|
12300
|
-
controllerSlots: loop ? inferSlots(raw.key) : [],
|
|
12301
|
-
requirements,
|
|
12302
|
-
reviewStatus: "metadata-reviewed"
|
|
12303
|
-
};
|
|
12304
|
-
return { ...base, ...CURATED[raw.actionId], requirements };
|
|
12168
|
+
async function exists2(p) {
|
|
12169
|
+
try {
|
|
12170
|
+
await fs11.access(p);
|
|
12171
|
+
return true;
|
|
12172
|
+
} catch {
|
|
12173
|
+
return false;
|
|
12174
|
+
}
|
|
12175
|
+
}
|
|
12176
|
+
function formatMb(bytes) {
|
|
12177
|
+
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
12305
12178
|
}
|
|
12306
12179
|
|
|
12307
|
-
//
|
|
12308
|
-
var
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12180
|
+
// src/commands/controller.ts
|
|
12181
|
+
var CONTROLLER_KINDS = [
|
|
12182
|
+
"character",
|
|
12183
|
+
"car",
|
|
12184
|
+
"drone",
|
|
12185
|
+
"touch",
|
|
12186
|
+
"networked-physics"
|
|
12187
|
+
];
|
|
12188
|
+
var SHARED = [
|
|
12189
|
+
"shared/math.ts",
|
|
12190
|
+
"shared/physics-world.ts",
|
|
12191
|
+
"shared/colliders.ts"
|
|
12192
|
+
];
|
|
12193
|
+
var TOUCH_KIT = [
|
|
12194
|
+
"touch/touch-joystick.ts",
|
|
12195
|
+
"touch/drag-zone.ts",
|
|
12196
|
+
"touch/rotate-overlay.ts"
|
|
12197
|
+
];
|
|
12198
|
+
var INPUT_AND_CAMERA = [
|
|
12199
|
+
"character/follow-camera.ts",
|
|
12200
|
+
"character/keyboard-input.ts",
|
|
12201
|
+
"character/touch-joystick.ts",
|
|
12202
|
+
...TOUCH_KIT
|
|
12203
|
+
];
|
|
12204
|
+
var NOTICE = "NOTICE.md";
|
|
12205
|
+
var CONTROLLER_FILE_SETS = {
|
|
12206
|
+
character: {
|
|
12207
|
+
code: [
|
|
12208
|
+
...SHARED,
|
|
12209
|
+
"character/character-controller.ts",
|
|
12210
|
+
"character/character-animations.ts",
|
|
12211
|
+
"character/animation-packs.ts",
|
|
12212
|
+
"character/motion-actions.ts",
|
|
12213
|
+
"character/meshy/meshy-loader.ts",
|
|
12214
|
+
"character/presets.ts",
|
|
12215
|
+
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
12216
|
+
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
12217
|
+
"character/vrm/vrm-loader.ts",
|
|
12218
|
+
"character/vrm/vrm-retarget.ts",
|
|
12219
|
+
"character/vrm/capsule-fit.ts",
|
|
12220
|
+
"character/vrm/foot-ik.ts",
|
|
12221
|
+
...INPUT_AND_CAMERA,
|
|
12222
|
+
NOTICE
|
|
12223
|
+
],
|
|
12224
|
+
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
12225
|
+
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
12226
|
+
// a static manifest asset. animation-library.glb (the 12-clip core) still is;
|
|
12227
|
+
// extra clips arrive via `genex controller anims` into public/assets/anims/.
|
|
12228
|
+
assets: ["assets/animation-library.glb"],
|
|
12229
|
+
skill: "genex-threejs-character-controller",
|
|
12230
|
+
sketch: [
|
|
12231
|
+
`const physics = await PhysicsWorld.create();`,
|
|
12232
|
+
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
12233
|
+
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
12234
|
+
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
12235
|
+
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
12236
|
+
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
12237
|
+
]
|
|
12238
|
+
},
|
|
12239
|
+
car: {
|
|
12240
|
+
code: [
|
|
12241
|
+
...SHARED,
|
|
12242
|
+
"vehicle/vehicle-controller.ts",
|
|
12243
|
+
"vehicle/wheel.ts",
|
|
12244
|
+
"vehicle/presets.ts",
|
|
12245
|
+
"interact/enter-exit.ts",
|
|
12246
|
+
...INPUT_AND_CAMERA,
|
|
12247
|
+
NOTICE
|
|
12248
|
+
],
|
|
12249
|
+
assets: [],
|
|
12250
|
+
skill: "genex-threejs-vehicle-controllers",
|
|
12251
|
+
sketch: [
|
|
12252
|
+
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
12253
|
+
`const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
|
|
12254
|
+
`scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
|
|
12255
|
+
]
|
|
12256
|
+
},
|
|
12257
|
+
drone: {
|
|
12258
|
+
code: [
|
|
12259
|
+
...SHARED,
|
|
12260
|
+
"drone/drone-controller.ts",
|
|
12261
|
+
"drone/presets.ts",
|
|
12262
|
+
"interact/enter-exit.ts",
|
|
12263
|
+
...INPUT_AND_CAMERA,
|
|
12264
|
+
NOTICE
|
|
12265
|
+
],
|
|
12266
|
+
assets: [],
|
|
12267
|
+
skill: "genex-threejs-vehicle-controllers",
|
|
12268
|
+
sketch: [
|
|
12269
|
+
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
12270
|
+
`const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
|
|
12271
|
+
`physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
|
|
12272
|
+
]
|
|
12273
|
+
},
|
|
12274
|
+
touch: {
|
|
12275
|
+
code: [...TOUCH_KIT, NOTICE],
|
|
12276
|
+
assets: [],
|
|
12277
|
+
skill: "genex-threejs-touch-controls",
|
|
12278
|
+
sketch: [
|
|
12279
|
+
`const joy = new TouchJoystick({ floating: true }); // safe-area-aware defaults; static circle without the flag`,
|
|
12280
|
+
`const jump = new VirtualButton({ label: "Jump", onPress: () => player.jump() });`,
|
|
12281
|
+
`const look = new DragZone(); // right half; per frame: const { dx, dy } = look.consumeDelta()`,
|
|
12282
|
+
`[joy, jump, look].forEach((w) => w.setVisible(navigator.maxTouchPoints > 0));`
|
|
12283
|
+
]
|
|
12284
|
+
},
|
|
12285
|
+
"networked-physics": {
|
|
12286
|
+
code: [
|
|
12287
|
+
...SHARED,
|
|
12288
|
+
"network/pose.ts",
|
|
12289
|
+
"network/networked-pushable.ts",
|
|
12290
|
+
"network/networked-vehicle.ts",
|
|
12291
|
+
"NETWORKING.md",
|
|
12292
|
+
NOTICE
|
|
12293
|
+
],
|
|
12294
|
+
assets: [],
|
|
12295
|
+
skill: "genex-threejs-multiplayer",
|
|
12296
|
+
sketch: [
|
|
12297
|
+
`const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
|
|
12298
|
+
`physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
|
|
12299
|
+
`contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
|
|
12300
|
+
]
|
|
12301
|
+
}
|
|
12323
12302
|
};
|
|
12324
|
-
var
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
}
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12303
|
+
var CODE_DEST = path13.join("src", "controllers");
|
|
12304
|
+
var ASSETS_DEST = path13.join("public", "assets");
|
|
12305
|
+
async function runController(opts) {
|
|
12306
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
12307
|
+
if (opts.kind?.trim() === "anims") {
|
|
12308
|
+
await runAnims(opts);
|
|
12309
|
+
return;
|
|
12310
|
+
}
|
|
12311
|
+
const kind = opts.kind?.trim();
|
|
12312
|
+
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
12313
|
+
log.error(
|
|
12314
|
+
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
12315
|
+
"genex controller <character|car|drone|touch|networked-physics> [--force]"
|
|
12316
|
+
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
12317
|
+
);
|
|
12318
|
+
process.exitCode = 1;
|
|
12319
|
+
return;
|
|
12320
|
+
}
|
|
12321
|
+
const srcDir = path13.join(getTemplatesDir(), "controllers");
|
|
12322
|
+
const root = opts.cwd ?? process.cwd();
|
|
12323
|
+
const set = CONTROLLER_FILE_SETS[kind];
|
|
12324
|
+
log.plain(c.bold(`genex controller ${kind}`));
|
|
12325
|
+
log.plain("");
|
|
12326
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
|
|
12327
|
+
const plan = [
|
|
12328
|
+
...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
|
|
12329
|
+
...set.assets.map((rel) => ({
|
|
12330
|
+
from: rel,
|
|
12331
|
+
rel: path13.join(ASSETS_DEST, path13.basename(rel))
|
|
12332
|
+
}))
|
|
12333
|
+
];
|
|
12334
|
+
let copied = 0;
|
|
12335
|
+
let skipped = 0;
|
|
12336
|
+
try {
|
|
12337
|
+
for (const file of plan) {
|
|
12338
|
+
const dest = path13.join(root, file.rel);
|
|
12339
|
+
if (!opts.force && await exists3(dest)) {
|
|
12340
|
+
skipped++;
|
|
12341
|
+
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
12342
|
+
continue;
|
|
12343
|
+
}
|
|
12344
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
12345
|
+
await fs12.copyFile(path13.join(srcDir, file.from), dest);
|
|
12346
|
+
copied++;
|
|
12347
|
+
log.dim(` ${file.rel}`);
|
|
12334
12348
|
}
|
|
12349
|
+
} catch (err) {
|
|
12350
|
+
log.error(`Copy failed: ${String(err)}`);
|
|
12351
|
+
process.exitCode = 1;
|
|
12352
|
+
return;
|
|
12335
12353
|
}
|
|
12336
|
-
|
|
12337
|
-
}
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12354
|
+
log.success(
|
|
12355
|
+
`Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
|
|
12356
|
+
);
|
|
12357
|
+
log.plain("");
|
|
12358
|
+
if (kind === "character") {
|
|
12359
|
+
if (opts.character) {
|
|
12360
|
+
try {
|
|
12361
|
+
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
12362
|
+
if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
|
|
12363
|
+
await installMeshyCharacterManifest({
|
|
12364
|
+
root,
|
|
12365
|
+
characterId: opts.character,
|
|
12366
|
+
apiUrl: getApiUrl(opts.apiUrl),
|
|
12367
|
+
token,
|
|
12368
|
+
log
|
|
12369
|
+
});
|
|
12370
|
+
} catch (error) {
|
|
12371
|
+
log.error(error instanceof Error ? error.message : String(error));
|
|
12372
|
+
process.exitCode = 1;
|
|
12373
|
+
return;
|
|
12374
|
+
}
|
|
12375
|
+
} else {
|
|
12376
|
+
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
12377
|
+
await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
|
|
12358
12378
|
}
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12379
|
+
log.plain("");
|
|
12380
|
+
}
|
|
12381
|
+
log.plain(c.bold("Next steps"));
|
|
12382
|
+
if (kind !== "touch") {
|
|
12383
|
+
log.plain(
|
|
12384
|
+
` 1. ${c.cyan(
|
|
12385
|
+
kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
|
|
12386
|
+
)} (three is already in the scaffold).`
|
|
12387
|
+
);
|
|
12388
|
+
}
|
|
12389
|
+
const stepOffset = kind === "touch" ? 0 : 1;
|
|
12390
|
+
log.plain(
|
|
12391
|
+
` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
|
|
12392
|
+
);
|
|
12393
|
+
log.plain(
|
|
12394
|
+
kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
|
|
12395
|
+
);
|
|
12396
|
+
const sketch = kind === "character" && opts.character ? [
|
|
12397
|
+
`const physics = await PhysicsWorld.create();`,
|
|
12398
|
+
`const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
|
|
12399
|
+
`const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
12400
|
+
`character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
|
|
12401
|
+
`physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
|
|
12402
|
+
] : set.sketch;
|
|
12403
|
+
for (const line of sketch) {
|
|
12404
|
+
log.dim(` ${line}`);
|
|
12405
|
+
}
|
|
12406
|
+
await firstPreviewNudge(log);
|
|
12407
|
+
}
|
|
12408
|
+
async function installMeshyCharacterManifest(args) {
|
|
12409
|
+
const response = await apiFetch(
|
|
12410
|
+
`${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
|
|
12411
|
+
{ headers: { Authorization: `Bearer ${args.token}` } }
|
|
12412
|
+
);
|
|
12413
|
+
if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
|
|
12414
|
+
if (!response.ok) {
|
|
12415
|
+
let detail;
|
|
12416
|
+
try {
|
|
12417
|
+
const errorBody = await response.json();
|
|
12418
|
+
if (typeof errorBody.message === "string" && errorBody.message.length > 0) detail = errorBody.message;
|
|
12419
|
+
else if (typeof errorBody.error === "string" && errorBody.error.length > 0) detail = errorBody.error;
|
|
12420
|
+
} catch {
|
|
12362
12421
|
}
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12422
|
+
throw new Error(detail ?? `Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
|
|
12423
|
+
}
|
|
12424
|
+
const body = await response.json();
|
|
12425
|
+
const manifest = body.manifest;
|
|
12426
|
+
if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
|
|
12427
|
+
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
12428
|
+
}
|
|
12429
|
+
assertCompleteMeshyControllerPack(manifest);
|
|
12430
|
+
const destination = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
12431
|
+
await fs12.mkdir(path13.dirname(destination), { recursive: true });
|
|
12432
|
+
await fs12.writeFile(
|
|
12433
|
+
destination,
|
|
12434
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
12435
|
+
`
|
|
12436
|
+
);
|
|
12437
|
+
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
12438
|
+
const pack = manifest.controllerPack;
|
|
12439
|
+
if (typeof pack?.key === "string" && typeof pack.version === "number") {
|
|
12440
|
+
args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
|
|
12441
|
+
} else {
|
|
12442
|
+
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
|
|
12443
|
+
}
|
|
12444
|
+
const provenance = manifest.provenance;
|
|
12445
|
+
if (provenance?.provider === "meshy" && typeof provenance.aiModel === "string") {
|
|
12446
|
+
const apiVersion = typeof provenance.apiVersion === "string" && provenance.apiVersion.length > 0 ? `; API ${provenance.apiVersion}` : "";
|
|
12447
|
+
args.log.success(`Meshy model: ${provenance.aiModel}${apiVersion}`);
|
|
12448
|
+
if ((provenance.poseMode === "a-pose" || provenance.poseMode === "t-pose") && typeof provenance.shouldRemesh === "boolean" && typeof provenance.targetPolycount === "number") {
|
|
12449
|
+
args.log.dim(
|
|
12450
|
+
` generation: ${provenance.poseMode}, remesh ${provenance.shouldRemesh ? "on" : "off"}, target ${provenance.targetPolycount} polygons`
|
|
12451
|
+
);
|
|
12366
12452
|
}
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
].
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
coveredGroups++;
|
|
12380
|
-
const original = queryTokens[index];
|
|
12381
|
-
const best = hits.includes(original) ? original : hits[0];
|
|
12382
|
-
score += best === original ? 400 : 140;
|
|
12383
|
-
matched.push(best);
|
|
12453
|
+
} else {
|
|
12454
|
+
args.log.warn("Meshy model/version metadata is unavailable in this legacy manifest.");
|
|
12455
|
+
}
|
|
12456
|
+
const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
|
|
12457
|
+
args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
|
|
12458
|
+
const bindings = manifest.locomotion?.bindings;
|
|
12459
|
+
if (bindings) {
|
|
12460
|
+
for (const [slot, binding] of Object.entries(bindings).sort(([a], [b]) => a.localeCompare(b))) {
|
|
12461
|
+
if (typeof binding?.actionId === "number" && typeof binding.clip === "string" && typeof binding.mode === "string") {
|
|
12462
|
+
const phase = typeof binding.phase === "number" ? ` @ phase ${binding.phase}` : "";
|
|
12463
|
+
args.log.dim(` ${slot} -> action ${binding.actionId} (${binding.clip}, ${binding.mode}${phase})`);
|
|
12464
|
+
}
|
|
12384
12465
|
}
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12466
|
+
}
|
|
12467
|
+
const slots = Object.keys(bindings ?? manifest.locomotion?.slots ?? {}).sort();
|
|
12468
|
+
args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
|
|
12469
|
+
const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
|
|
12470
|
+
if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
|
|
12471
|
+
else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
|
|
12472
|
+
}
|
|
12473
|
+
var REQUIRED_MESHY_CONTROLLER_SLOTS = [
|
|
12474
|
+
"idle.default",
|
|
12475
|
+
"walk.forward",
|
|
12476
|
+
"run.forward",
|
|
12477
|
+
"crouch.forward",
|
|
12478
|
+
"crouch.idle",
|
|
12479
|
+
"jump.full"
|
|
12480
|
+
];
|
|
12481
|
+
function incompletePack(message) {
|
|
12482
|
+
throw new Error(`Incomplete Meshy controller pack: ${message}`);
|
|
12483
|
+
}
|
|
12484
|
+
function assertCompleteMeshyControllerPack(manifest) {
|
|
12485
|
+
const pack = manifest.controllerPack;
|
|
12486
|
+
if (pack === void 0) return;
|
|
12487
|
+
if (typeof pack.key !== "string" || pack.key.length === 0 || typeof pack.version !== "number" || !Number.isInteger(pack.version)) {
|
|
12488
|
+
incompletePack("invalid key or version.");
|
|
12489
|
+
}
|
|
12490
|
+
if (typeof pack.fingerprint !== "string" || pack.fingerprint.length === 0) {
|
|
12491
|
+
incompletePack(`${pack.key} v${pack.version} has no immutable fingerprint.`);
|
|
12492
|
+
}
|
|
12493
|
+
if (!Array.isArray(pack.actionIds) || pack.actionIds.length === 0 || pack.actionIds.some((actionId) => typeof actionId !== "number" || !Number.isInteger(actionId))) {
|
|
12494
|
+
incompletePack(`${pack.key} v${pack.version} has no valid action snapshot.`);
|
|
12495
|
+
}
|
|
12496
|
+
const actionIds = pack.actionIds;
|
|
12497
|
+
const packActionIds = new Set(actionIds);
|
|
12498
|
+
if (packActionIds.size !== actionIds.length) {
|
|
12499
|
+
incompletePack(`${pack.key} v${pack.version} repeats an action ID.`);
|
|
12500
|
+
}
|
|
12501
|
+
const bindings = manifest.locomotion?.bindings;
|
|
12502
|
+
if (!bindings) incompletePack(`${pack.key} v${pack.version} has no authoritative locomotion bindings.`);
|
|
12503
|
+
const missingSlots = REQUIRED_MESHY_CONTROLLER_SLOTS.filter((slot) => bindings[slot] === void 0);
|
|
12504
|
+
if (missingSlots.length > 0) {
|
|
12505
|
+
incompletePack(`${pack.key} v${pack.version} is missing ${missingSlots.join(", ")}.`);
|
|
12506
|
+
}
|
|
12507
|
+
const clips = manifest.clips ?? [];
|
|
12508
|
+
const boundActionIds = /* @__PURE__ */ new Set();
|
|
12509
|
+
const snapshotBindings = {};
|
|
12510
|
+
for (const [slot, binding] of Object.entries(bindings)) {
|
|
12511
|
+
if (typeof binding.actionId !== "number" || !Number.isInteger(binding.actionId) || typeof binding.clip !== "string" || binding.clip.length === 0 || binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
|
|
12512
|
+
incompletePack(`${pack.key} v${pack.version} has an invalid ${slot} binding.`);
|
|
12513
|
+
}
|
|
12514
|
+
if (binding.phase !== void 0 && (typeof binding.phase !== "number" || !Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
|
|
12515
|
+
incompletePack(`${pack.key} v${pack.version} has an invalid ${slot} phase.`);
|
|
12516
|
+
}
|
|
12517
|
+
const actionId = binding.actionId;
|
|
12518
|
+
boundActionIds.add(actionId);
|
|
12519
|
+
if (!packActionIds.has(actionId)) {
|
|
12520
|
+
incompletePack(`${slot} points at action ${actionId}, which is outside the stored pack snapshot.`);
|
|
12521
|
+
}
|
|
12522
|
+
const clip = clips.find((entry) => entry.actionId === actionId);
|
|
12523
|
+
if (!clip || clip.key !== binding.clip) {
|
|
12524
|
+
incompletePack(`${slot} expects action ${actionId} clip ${binding.clip}, but that exact clip is not installed.`);
|
|
12525
|
+
}
|
|
12526
|
+
snapshotBindings[slot] = {
|
|
12527
|
+
actionId,
|
|
12528
|
+
mode: binding.mode,
|
|
12529
|
+
...binding.phase === void 0 ? {} : { phase: binding.phase }
|
|
12530
|
+
};
|
|
12531
|
+
}
|
|
12532
|
+
const unboundActionIds = actionIds.filter((actionId) => !boundActionIds.has(actionId));
|
|
12533
|
+
if (unboundActionIds.length > 0) {
|
|
12534
|
+
incompletePack(`${pack.key} v${pack.version} action snapshot contains unbound actions ${unboundActionIds.join(", ")}.`);
|
|
12535
|
+
}
|
|
12536
|
+
let computedFingerprint;
|
|
12537
|
+
try {
|
|
12538
|
+
computedFingerprint = createMeshyControllerPackSnapshot({
|
|
12539
|
+
key: pack.key,
|
|
12540
|
+
version: pack.version,
|
|
12541
|
+
actionIds,
|
|
12542
|
+
bindings: snapshotBindings
|
|
12543
|
+
}).fingerprint;
|
|
12544
|
+
} catch (error) {
|
|
12545
|
+
incompletePack(error instanceof Error ? error.message : "the immutable snapshot is invalid.");
|
|
12546
|
+
}
|
|
12547
|
+
if (computedFingerprint !== pack.fingerprint) {
|
|
12548
|
+
incompletePack(`${pack.key} v${pack.version} fingerprint does not match its action bindings.`);
|
|
12549
|
+
}
|
|
12550
|
+
}
|
|
12551
|
+
async function installOwnerAvatar(args) {
|
|
12552
|
+
const { root, srcDir, apiUrl, token, log } = args;
|
|
12553
|
+
const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
|
|
12554
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
12555
|
+
if (token) {
|
|
12556
|
+
try {
|
|
12557
|
+
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
12558
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
12559
|
+
});
|
|
12560
|
+
if (res.ok) {
|
|
12561
|
+
const me = await res.json();
|
|
12562
|
+
if (me.vrmUrl) {
|
|
12563
|
+
const vrmRes = await fetch(me.vrmUrl);
|
|
12564
|
+
if (vrmRes.ok) {
|
|
12565
|
+
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
12566
|
+
await fs12.writeFile(dest, buf);
|
|
12567
|
+
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
12568
|
+
return;
|
|
12569
|
+
}
|
|
12570
|
+
}
|
|
12571
|
+
}
|
|
12572
|
+
log.dim(" couldn't fetch your avatar; using the bundled default.");
|
|
12573
|
+
} catch {
|
|
12574
|
+
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
12389
12575
|
}
|
|
12390
12576
|
}
|
|
12391
|
-
|
|
12577
|
+
await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
12578
|
+
log.dim(
|
|
12579
|
+
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
12580
|
+
);
|
|
12581
|
+
}
|
|
12582
|
+
async function exists3(p) {
|
|
12583
|
+
try {
|
|
12584
|
+
await fs12.access(p);
|
|
12585
|
+
return true;
|
|
12586
|
+
} catch {
|
|
12587
|
+
return false;
|
|
12588
|
+
}
|
|
12392
12589
|
}
|
|
12393
12590
|
|
|
12394
12591
|
// src/commands/character.ts
|
|
@@ -12473,8 +12670,10 @@ async function runCharacter(opts) {
|
|
|
12473
12670
|
log.plain(c.bold("Character quote"));
|
|
12474
12671
|
log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
|
|
12475
12672
|
if (price.controllerPackKey) {
|
|
12673
|
+
const version = price.controllerPackVersion === void 0 ? "" : ` v${price.controllerPackVersion}`;
|
|
12674
|
+
const fingerprint = price.controllerPackFingerprint ? ` \xB7 fingerprint ${price.controllerPackFingerprint}` : "";
|
|
12476
12675
|
log.plain(
|
|
12477
|
-
` controller pack ${c.cyan(price.controllerPackKey)} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
|
|
12676
|
+
` controller pack ${c.cyan(`${price.controllerPackKey}${version}`)}${fingerprint} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
|
|
12478
12677
|
);
|
|
12479
12678
|
}
|
|
12480
12679
|
log.dim(` ${price.actionsGenerated} Meshy animation task${price.actionsGenerated === 1 ? "" : "s"} in this request.`);
|