@hypersoniclabs/helix-mcp 0.2.5 → 0.2.12
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 +81 -11
- package/dist/continuumCanary.d.ts +17 -0
- package/dist/continuumCanary.js +17 -0
- package/dist/continuumCanary.js.map +1 -0
- package/dist/server.d.ts +14 -1
- package/dist/server.js +4058 -162
- package/dist/server.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/vehicleTools.d.ts +86 -0
- package/dist/vehicleTools.js +229 -0
- package/dist/vehicleTools.js.map +1 -0
- package/docs/avatar-face.md +115 -0
- package/docs/bridge.md +98 -0
- package/docs/bring-your-world.md +117 -0
- package/docs/catalog.md +69 -1
- package/docs/character-animation.md +442 -0
- package/docs/character-attachments.md +166 -0
- package/docs/character-world.md +785 -130
- package/docs/continuum.md +153 -0
- package/docs/items.md +73 -0
- package/docs/lighting-world.md +667 -0
- package/docs/locomotion-clip-spec.md +294 -0
- package/docs/manifest.md +31 -6
- package/docs/multiplayer-logic.md +460 -17
- package/docs/multiplayer-templates/chrono-orchard.md +36 -22
- package/docs/multiplayer-templates/collect-a-thon.md +28 -38
- package/docs/multiplayer-templates/collections.md +24 -24
- package/docs/multiplayer-templates/hangout.md +124 -111
- package/docs/multiplayer-templates/npc-wave.md +310 -0
- package/docs/multiplayer-templates/obby.md +13 -16
- package/docs/multiplayer-templates/persistent-progress.md +218 -0
- package/docs/multiplayer-templates/physics-bumper.md +27 -9
- package/docs/multiplayer-templates/physics-football.md +22 -8
- package/docs/multiplayer-templates/relic-bearers.md +12 -15
- package/docs/multiplayer-templates/server-motion.md +16 -19
- package/docs/multiplayer-templates/shooter-range.md +275 -0
- package/docs/multiplayer-templates/team-control.md +28 -15
- package/docs/multiplayer-templates/turn-arena.md +31 -22
- package/docs/multiplayer-templates/voice-radio.md +166 -0
- package/docs/multiplayer-templates/wave-survival.md +7 -8
- package/docs/multiplayer-templates/world-shop.md +240 -0
- package/docs/multiplayer-world.md +222 -129
- package/docs/npc-world.md +623 -0
- package/docs/publishing.md +108 -28
- package/docs/purchases.md +223 -0
- package/docs/scene-performance.md +64 -0
- package/docs/screenshots.md +140 -0
- package/docs/sdk.md +324 -5
- package/docs/shooter-worlds.md +537 -0
- package/docs/terrain.md +173 -0
- package/docs/upgrades.md +324 -0
- package/docs/vehicles.md +727 -0
- package/docs/world-inspect.md +156 -0
- package/docs/world-look.md +241 -0
- package/docs/world-recipe.md +65 -6
- package/package.json +15 -4
- package/skills/README.md +91 -0
- package/skills/helix-assets/SKILL.md +491 -0
- package/skills/helix-assets/references/asset-sources.md +143 -0
- package/skills/helix-assets/references/vault-api.md +105 -0
- package/skills/helix-avatar-qa/SKILL.md +85 -0
- package/skills/helix-avatars/SKILL.md +206 -0
- package/skills/helix-avatars/references/contract.md +166 -0
- package/skills/helix-avatars/references/dynamics.md +367 -0
- package/skills/helix-avatars/references/face.md +50 -0
- package/skills/helix-avatars/references/publish.md +76 -0
- package/skills/helix-avatars/references/qa.md +251 -0
- package/skills/helix-avatars/references/rigging.md +88 -0
- package/skills/helix-avatars/references/source-generated.md +190 -0
- package/skills/helix-avatars/references/source-model.md +90 -0
- package/skills/helix-avatars/references/source-rigid.md +90 -0
- package/skills/helix-avatars/references/source-vrm.md +61 -0
- package/skills/helix-gauntlet/SKILL.md +128 -0
- package/skills/helix-multiplayer/SKILL.md +150 -0
- package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
- package/skills/helix-vehicles/SKILL.md +218 -0
- package/skills/helix-vehicles/references/addons.md +212 -0
- package/skills/helix-vehicles/references/appearance.md +339 -0
- package/skills/helix-vehicles/references/audio-import.md +138 -0
- package/skills/helix-vehicles/references/audio.md +580 -0
- package/skills/helix-vehicles/references/cabin.md +225 -0
- package/skills/helix-vehicles/references/host-manifest.md +174 -0
- package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
- package/skills/helix-vehicles/references/publish.md +214 -0
- package/skills/helix-vehicles/references/qa.md +177 -0
- package/skills/helix-vehicles/references/reference-package.json +3481 -0
- package/skills/helix-vehicles/references/reference-package.md +69 -0
- package/skills/helix-vehicles/references/source-beamng.md +167 -0
- package/skills/helix-vehicles/references/source-concept.md +38 -0
- package/skills/helix-vehicles/references/source-model.md +100 -0
- package/skills/helix-vehicles/references/source-scratch.md +60 -0
- package/skills/helix-world-build/SKILL.md +376 -0
- package/skills/helix-world-build/references/config-gates.md +104 -0
- package/skills/helix-world-director/SKILL.md +210 -0
- package/skills/helix-world-qa/SKILL.md +371 -0
- package/skills/helix-world-qa/references/perf-budgets.md +240 -0
- package/skills/helix-world-qa/references/perf-handle.md +125 -0
- package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MINIMUM_VEHICLE_PARTS = exports.VEHICLE_AUDIO_RULE = exports.VEHICLE_OUTPUT_LIMIT = void 0;
|
|
4
|
+
exports.reportVehicleRun = reportVehicleRun;
|
|
5
|
+
exports.displayArg = displayArg;
|
|
6
|
+
exports.displayCommand = displayCommand;
|
|
7
|
+
exports.audioImportArgs = audioImportArgs;
|
|
8
|
+
exports.vehicleUnavailableText = vehicleUnavailableText;
|
|
9
|
+
exports.summariseReferencePackage = summariseReferencePackage;
|
|
10
|
+
exports.summariseVehicleHost = summariseVehicleHost;
|
|
11
|
+
exports.summariseCustomizerGuard = summariseCustomizerGuard;
|
|
12
|
+
// Pure helpers behind the vehicle tools in server.ts: how an executed `helix vehicle …` run is reported back, what
|
|
13
|
+
// the reference-package copy tool says about the file it wrote, and how a host's slot board + add-on rail are
|
|
14
|
+
// summarised. No I/O beyond the log file a long run spills into, so smoke tests can drive them directly.
|
|
15
|
+
//
|
|
16
|
+
// Why the vehicle tools EXECUTE rather than hand back a command (round-1 F40 forensics §14): a DELEGATED
|
|
17
|
+
// `validate_vehicle` sent the agent hunting for the CLI inside the MCP's npm cache, after which it used the raw CLI
|
|
18
|
+
// for everything — skipping the MCP wrappers and pinning a stale QA harness. Running the bundled CLI here keeps an
|
|
19
|
+
// agent on the MCP path, and the exit code is the gate result.
|
|
20
|
+
const node_fs_1 = require("node:fs");
|
|
21
|
+
const node_os_1 = require("node:os");
|
|
22
|
+
const node_path_1 = require("node:path");
|
|
23
|
+
/** Above this many characters a run's output is cut to its head and tail; the whole of it goes to a log file. */
|
|
24
|
+
exports.VEHICLE_OUTPUT_LIMIT = 30_000;
|
|
25
|
+
const HEAD_CHARS = 6_000;
|
|
26
|
+
function spill(label, body, logDir) {
|
|
27
|
+
try {
|
|
28
|
+
const dir = logDir ?? (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'helix-mcp-vehicle');
|
|
29
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
30
|
+
const file = (0, node_path_1.join)(dir, `${label.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-${Date.now()}.log`);
|
|
31
|
+
(0, node_fs_1.writeFileSync)(file, body);
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Turn an executed CLI run into the tool result. Exit 0 is a pass; any other exit is returned as `isError` with the
|
|
40
|
+
* full output (never a throw), because the output IS the list of things to fix.
|
|
41
|
+
*/
|
|
42
|
+
function reportVehicleRun(label, run, options = {}) {
|
|
43
|
+
const limit = options.limit ?? exports.VEHICLE_OUTPUT_LIMIT;
|
|
44
|
+
const minutes = (run.elapsedMs / 60_000).toFixed(1);
|
|
45
|
+
let status;
|
|
46
|
+
if (run.spawnError)
|
|
47
|
+
status = `${label}: COULD NOT RUN (${run.spawnError})`;
|
|
48
|
+
else if (run.timedOut)
|
|
49
|
+
status = `${label}: TIMED OUT after ${minutes} min — the CLI was stopped; nothing after that point happened. Re-run the same call (vehicle publish steps resume from their ledger).`;
|
|
50
|
+
else if (run.exitCode === 0)
|
|
51
|
+
status = `${label}: exit 0 (${options.exitMeaning?.[0] ?? 'pass'}) in ${minutes} min`;
|
|
52
|
+
else {
|
|
53
|
+
const meaning = run.exitCode === null ? undefined : options.exitMeaning?.[run.exitCode];
|
|
54
|
+
status = `${label}: FAILED — exit ${run.exitCode}${meaning ? ` (${meaning})` : ''} in ${minutes} min. The output below says what to fix.`;
|
|
55
|
+
}
|
|
56
|
+
const sections = [run.stdout.trim(), run.stderr.trim() ? `--- stderr ---\n${run.stderr.trim()}` : ''].filter(Boolean);
|
|
57
|
+
let body = sections.join('\n\n') || '(no output)';
|
|
58
|
+
if (body.length > limit) {
|
|
59
|
+
const file = spill(label, `$ ${run.command}\n${status}\n\n${body}\n`, options.logDir);
|
|
60
|
+
const tail = limit - HEAD_CHARS;
|
|
61
|
+
body = `${body.slice(0, HEAD_CHARS)}\n\n… ${body.length - limit} characters omitted${file ? ` — the full output is in ${file}` : ''} …\n\n${body.slice(-tail)}`;
|
|
62
|
+
}
|
|
63
|
+
const ok = !run.spawnError && !run.timedOut && run.exitCode === 0;
|
|
64
|
+
const text = [`$ ${run.command}`, status, '', body, ok && options.next ? `\nNext: ${options.next}` : ''].filter((s) => s !== '').join('\n');
|
|
65
|
+
return { text, isError: !ok };
|
|
66
|
+
}
|
|
67
|
+
/** Shell-quote one argument for display (the run itself never goes through a shell). */
|
|
68
|
+
function displayArg(value) {
|
|
69
|
+
return /^[A-Za-z0-9_./:=@%+-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
70
|
+
}
|
|
71
|
+
function displayCommand(args) {
|
|
72
|
+
return ['helix', ...args.map(displayArg)].join(' ');
|
|
73
|
+
}
|
|
74
|
+
// ---- vehicle audio: the hard rule, and the audio-import call ---------------------------------------------------
|
|
75
|
+
/** The standing rule every vehicle-audio tool description and route note repeats, so no agent reaches it second-hand. */
|
|
76
|
+
exports.VEHICLE_AUDIO_RULE = 'HARD RULE: NEVER generate or synthesise vehicle sound (no resynthesis, no generate_audio, not even for a slot nothing provides, and never rely on the platform\'s default sounds: the synthesised fallback engine, the generated default sound pack, runtime noise); ALWAYS source real recordings, from BeamNG mods as much as possible, and a car ships a sourced recording for EVERY one of the 31 runtime slots (the only empty slots are a component the real car physically lacks, declared consumed:false, absent:"not-fitted" plus a reason, and exhaustLadder absent:"combined-with-engine" beside a sourced engine ladder; doors are required). Audio step one, whatever the mesh source: find the most accurate simulator mod of that exact car (BeamNG first; Assetto Corsa/ACC, rFactor 2, Automobilista 2 where theirs is more accurate), choose it by MEASURING candidates against real recordings of the car (firing-fundamental pitch track vs rpm, spectral centroid, roughness, idle character, limiter cadence), and extract it with import_vehicle_audio.';
|
|
77
|
+
/**
|
|
78
|
+
* The argv for `helix vehicle audio-import <source> --out <dir>` (sim-audio-cli lane's interface). `beamngRoot`
|
|
79
|
+
* (the BeamNG.drive install that holds the stock FMOD banks and blends a mod refers to by name) is `--beamng-root`;
|
|
80
|
+
* the rest map one to one onto the flags documented in helix-creator-cli docs/vehicle-audio-import.md.
|
|
81
|
+
*/
|
|
82
|
+
function audioImportArgs(a, resolvePath) {
|
|
83
|
+
return [
|
|
84
|
+
'vehicle', 'audio-import', resolvePath(a.source),
|
|
85
|
+
'--out', resolvePath(a.out),
|
|
86
|
+
...(a.sim ? ['--sim', a.sim] : []),
|
|
87
|
+
...(a.vehicle ? ['--vehicle', a.vehicle] : []),
|
|
88
|
+
...(a.config ? ['--config', a.config.includes('/') ? resolvePath(a.config) : a.config] : []),
|
|
89
|
+
...(a.map ? ['--map', resolvePath(a.map)] : []),
|
|
90
|
+
...(a.beamngRoot ? ['--beamng-root', resolvePath(a.beamngRoot)] : []),
|
|
91
|
+
...(a.cylinders ? ['--cylinders', String(a.cylinders)] : []),
|
|
92
|
+
...(a.prefer ? ['--prefer', a.prefer] : []),
|
|
93
|
+
...(a.acNativeRungs ? ['--ac-native-rungs'] : []),
|
|
94
|
+
...(a.modUrl ? ['--mod-url', a.modUrl] : []),
|
|
95
|
+
...(a.basePackage ? ['--base-package', resolvePath(a.basePackage)] : []),
|
|
96
|
+
...(a.json ? ['--json'] : []),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
/** What the tools answer when the bundled CLI lacks a vehicle command, so a stale CLI never reads as a crash. */
|
|
100
|
+
function vehicleUnavailableText(kind, path, tool) {
|
|
101
|
+
const command = `helix ${path.join(' ')}`;
|
|
102
|
+
if (kind === 'audio-import') {
|
|
103
|
+
return [
|
|
104
|
+
`${tool}: NOT AVAILABLE in the CLI bundled with this MCP — \`${command}\` does not exist in it.`,
|
|
105
|
+
'Nothing was done. Run check_for_updates: a newer MCP bundles a CLI that has it.',
|
|
106
|
+
`${exports.VEHICLE_AUDIO_RULE}`,
|
|
107
|
+
'Until then the rule still holds. Do NOT synthesise, generate or resynthesise the sound to fill the gap, and do not omit audio.bank or a slot to fall back on a platform default; check_vehicle_audio and publish_vehicle fail an empty or default-falling-back slot (AUDIO_SLOT_UNSOURCED) and a car without sourced engine audio. Extract the mod by hand (BeamNG art/sound blends + .flac rungs, Assetto Corsa .bank via vgmstream-cli), record each slot\'s provenance { kind, sim, mod, modUrl, sourceFile, sourceSha256, extractor } yourself, and report the import as blocked on the CLI.',
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
return [
|
|
111
|
+
`${tool}: NOT AVAILABLE in the CLI bundled with this MCP — \`${command}\` does not exist in it.`,
|
|
112
|
+
'Minting a new vehicle or vehicle add-on item is unavailable during the Package-first publication cutover. Nothing was done.',
|
|
113
|
+
'What still works: every offline gate (validate_vehicle, simulate_vehicle, check_vehicle_audio, preview_vehicle, check_vehicle_addon), check_vehicle_host, visual QA, distributions on existing items, and a NEW VERSION of an existing car: seal it staged with publish_continuum_package, pass visual_qa_item on that version, then activate it by re-running `helix vehicle publish <dir> --item <itemId> --package-version <v> --reason "<why>"` once the bundled CLI has it (the resume moves the pin and properties.vehiclePackage together). NEVER `helix item set-version` for a vehicle: it moves the Continuum pin but not properties.vehiclePackage, so the car keeps serving its old package (until the CLI says otherwise) (read_skill({ name: "helix-vehicles", reference: "publish" }), "A new version of an existing car").',
|
|
114
|
+
'Finish every gate, keep the directory ready, and REPORT the mint as blocked. Do not improvise it with raw API calls, an older CLI, or publish_item. Run check_for_updates: a newer MCP may bundle a CLI that has the command again.',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
118
|
+
/** One line per top-level block: its size and what it holds, so the agent can open the file block by block. */
|
|
119
|
+
function summariseReferencePackage(pkg) {
|
|
120
|
+
if (!isRecord(pkg))
|
|
121
|
+
return ['(not a JSON object)'];
|
|
122
|
+
const lines = [];
|
|
123
|
+
for (const [key, value] of Object.entries(pkg)) {
|
|
124
|
+
const chars = JSON.stringify(value)?.length ?? 0;
|
|
125
|
+
let what = '';
|
|
126
|
+
if (Array.isArray(value))
|
|
127
|
+
what = `${value.length} entries`;
|
|
128
|
+
else if (isRecord(value)) {
|
|
129
|
+
const keys = Object.keys(value);
|
|
130
|
+
what = `${keys.length} keys: ${keys.slice(0, 12).join(', ')}${keys.length > 12 ? ', …' : ''}`;
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
what = JSON.stringify(value);
|
|
134
|
+
if (key === 'audio' && isRecord(value)) {
|
|
135
|
+
const slots = Array.isArray(value.slots) ? value.slots.length : 0;
|
|
136
|
+
const assets = isRecord(value.bank) && Array.isArray(value.bank.assets) ? value.bank.assets.length : 0;
|
|
137
|
+
what += ` (${slots} slots, ${assets} bank samples)`;
|
|
138
|
+
}
|
|
139
|
+
lines.push(`- ${key} (${chars.toLocaleString('en-GB')} chars): ${what}`);
|
|
140
|
+
}
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
// ---- vehicle host board + add-on rail --------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* The parts set a car must ship before it is done (helix-vehicles step "Add-ons"). Slot types are the backend's
|
|
146
|
+
* `helix/vehicle-base@2` pack ids. `spoiler` is required only when the real car has a wing or aftermarket ones exist,
|
|
147
|
+
* so it is reported, never counted as missing.
|
|
148
|
+
*/
|
|
149
|
+
exports.MINIMUM_VEHICLE_PARTS = [
|
|
150
|
+
{ slotType: 'vehicle.wheel@1', part: 'wheel set (rims + tyres)', required: true },
|
|
151
|
+
{ slotType: 'vehicle.exhaust@1', part: 'exhaust', required: true },
|
|
152
|
+
{ slotType: 'vehicle.ecu@1', part: 'ECU', required: true },
|
|
153
|
+
{ slotType: 'vehicle.transmission@1', part: 'gearbox', required: true },
|
|
154
|
+
{ slotType: 'vehicle.suspension@1', part: 'suspension (coilovers)', required: true },
|
|
155
|
+
{ slotType: 'vehicle.brakes@1', part: 'brakes', required: true },
|
|
156
|
+
{ slotType: 'vehicle.arb@1', part: 'anti-roll bars', required: true },
|
|
157
|
+
{ slotType: 'vehicle.engine@1', part: 'engine / powertrain (where the real car has a swappable engine or a well-known upgrade)', required: false },
|
|
158
|
+
{ slotType: 'vehicle.spoiler@1', part: 'spoiler / wing (where the real car has one or a well-known aftermarket one exists)', required: false },
|
|
159
|
+
];
|
|
160
|
+
function summariseVehicleHost(itemId, board, rail) {
|
|
161
|
+
const lines = [];
|
|
162
|
+
const b = isRecord(board) ? board : {};
|
|
163
|
+
const surface = isRecord(b.surface) ? b.surface : {};
|
|
164
|
+
const count = (k) => (Array.isArray(surface[k]) ? surface[k].length : 0);
|
|
165
|
+
const sockets = count('sockets');
|
|
166
|
+
const nodes = count('nodes');
|
|
167
|
+
const paths = count('paths');
|
|
168
|
+
const hostKind = typeof b.hostKind === 'string' ? b.hostKind : null;
|
|
169
|
+
const grants = isRecord(b.hostGrantExtensions) ? b.hostGrantExtensions : {};
|
|
170
|
+
const problems = [];
|
|
171
|
+
lines.push(`Vehicle host ${itemId}`);
|
|
172
|
+
lines.push(`- slot board: hostKind ${hostKind ?? 'none'}, use ${String(b.use ?? '—')}, ${Number(b.slotCount ?? 0)} slots`);
|
|
173
|
+
lines.push(`- measured surface: ${sockets} sockets, ${nodes} nodes, ${paths} tunable paths, ${count('materialSlots')} material slots`);
|
|
174
|
+
lines.push(`- hide grants: ${Object.keys(grants).length ? JSON.stringify(grants) : 'none (only the standard *_stock_* globs)'}`);
|
|
175
|
+
const repair = 'The board is derived ONCE, at mint, from the GLB\'s asset.extras.HELIX_vehicle, the vehicle package and its hostGrantExtensions; a version move does not rebuild it. Today the repair is a fresh car minted with the manifest (read_skill helix-vehicles reference "publish", section 2): delist this one and report it.';
|
|
176
|
+
if (hostKind !== 'vehicle')
|
|
177
|
+
problems.push(`the item is not a vehicle host: its GLB carried no asset.extras.HELIX_vehicle at upload (read_skill helix-vehicles reference "host-manifest"). ${repair}`);
|
|
178
|
+
if (hostKind === 'vehicle' && sockets === 0)
|
|
179
|
+
problems.push(`the board has no sockets, so wheel, wing and exhaust add-ons fit nothing: asset.extras.HELIX_vehicle.sockets was empty. ${repair}`);
|
|
180
|
+
if (hostKind === 'vehicle' && nodes === 0)
|
|
181
|
+
problems.push('the board has no nodes, so no add-on can hide a stock part.');
|
|
182
|
+
if (hostKind === 'vehicle' && paths === 0)
|
|
183
|
+
problems.push(`the board has no tunable paths, so no performance part (ECU, gearbox, brakes, suspension, anti-roll bars) can fit. ${repair}`);
|
|
184
|
+
const r = isRecord(rail) ? rail : {};
|
|
185
|
+
const items = Array.isArray(r.items) ? r.items.filter(isRecord) : [];
|
|
186
|
+
const total = typeof r.total === 'number' ? r.total : items.length;
|
|
187
|
+
lines.push(`- add-ons that fit it (the customizer's list): ${total}${r.truncated ? ' (truncated)' : ''}`);
|
|
188
|
+
const bySlot = new Map();
|
|
189
|
+
for (const item of items) {
|
|
190
|
+
const slotType = typeof item.slotType === 'string' ? item.slotType : 'unknown';
|
|
191
|
+
bySlot.set(slotType, [...(bySlot.get(slotType) ?? []), item]);
|
|
192
|
+
}
|
|
193
|
+
for (const [slotType, rows] of [...bySlot.entries()].sort()) {
|
|
194
|
+
lines.push(` - ${slotType}: ${rows.map((i) => `${String(i.title)}${i.distributionId ? ` (${String(i.priceLix ?? '?')} LIX)` : ' (not for sale)'}`).join('; ')}`);
|
|
195
|
+
}
|
|
196
|
+
lines.push('', 'Minimum parts set:');
|
|
197
|
+
const missing = [];
|
|
198
|
+
for (const want of exports.MINIMUM_VEHICLE_PARTS) {
|
|
199
|
+
const rows = bySlot.get(want.slotType) ?? [];
|
|
200
|
+
const sold = rows.filter((i) => i.distributionId).length;
|
|
201
|
+
const mark = rows.length ? (sold ? '✔' : '◐') : want.required ? '✖' : '·';
|
|
202
|
+
lines.push(` ${mark} ${want.part} — ${want.slotType}: ${rows.length} fitting, ${sold} for sale`);
|
|
203
|
+
if (want.required && sold === 0)
|
|
204
|
+
missing.push(want.part);
|
|
205
|
+
}
|
|
206
|
+
if (missing.length)
|
|
207
|
+
problems.push(`the minimum parts set is not complete and for sale: ${missing.join(', ')}.`);
|
|
208
|
+
if (problems.length)
|
|
209
|
+
lines.push('', 'Not done:', ...problems.map((p) => `- ${p}`));
|
|
210
|
+
else
|
|
211
|
+
lines.push('', 'The host board is complete and every required part fits and is for sale. Now open the customizer and look at each part fitted.');
|
|
212
|
+
return { text: lines.join('\n'), ok: problems.length === 0 };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The customizer-guard half of `check_vehicle_host`: the CLI's `checkHostAddOns` verdicts (every add-on on the car's rail
|
|
216
|
+
* judged by the web customizer's own guard) as a section of the report. Any refused add-on, or a rail truncated before
|
|
217
|
+
* every add-on could be checked, fails the host check; the message names each add-on and quotes the customizer's text.
|
|
218
|
+
*/
|
|
219
|
+
function summariseCustomizerGuard(formatted, verdicts, truncated) {
|
|
220
|
+
const refused = verdicts.filter((v) => !v.ok);
|
|
221
|
+
const lines = ['Web customizer guard (the same check the customizer runs when a buyer stages each part):', ...formatted.split('\n').map((l) => ` ${l}`)];
|
|
222
|
+
if (refused.length) {
|
|
223
|
+
lines.push('', 'Not done:', ...refused.map((v) => `- add-on ${v.itemId} "${v.title ?? '?'}" would be refused by the web customizer ("${v.error}"): it cannot be equipped or saved there, so it is not a shipped part. Republish it as a new Package Version the customizer accepts (read_skill helix-vehicles reference "addons").`));
|
|
224
|
+
}
|
|
225
|
+
if (truncated)
|
|
226
|
+
lines.push('', 'Not done:', '- the add-on rail was truncated, so not every add-on could be checked against the customizer guard.');
|
|
227
|
+
return { text: lines.join('\n'), ok: refused.length === 0 && !truncated };
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=vehicleTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vehicleTools.js","sourceRoot":"","sources":["../src/vehicleTools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Avatar faces — lip-sync, blink and expressions
|
|
2
|
+
|
|
3
|
+
A HELIX character speaks with its mouth only if the GLB it was published from carries a
|
|
4
|
+
`helix/avatar-face@1` manifest. Nothing at runtime, in the world, or in the shell can add one later.
|
|
5
|
+
An avatar that arrives without it is **permanently mute**: it will stand in a voice chat with a closed
|
|
6
|
+
mouth and never blink, and no error is raised anywhere.
|
|
7
|
+
|
|
8
|
+
That is the whole trap this document exists for. Everything else here is detail.
|
|
9
|
+
|
|
10
|
+
## What actually drives the mouth
|
|
11
|
+
|
|
12
|
+
Face V1 is a **receiver-local presentation layer**. No facial weight, microphone sample or derived
|
|
13
|
+
biometric is ever sent through HELIX multiplayer state:
|
|
14
|
+
|
|
15
|
+
- Each client analyses the remote voice audio **it already plays** and solves visemes locally.
|
|
16
|
+
- The local player's own avatar is driven from their **own microphone analyser** — nobody is
|
|
17
|
+
subscribed to their own track, and that mouth is visible in third person, in mirrors and through
|
|
18
|
+
the in-world camera.
|
|
19
|
+
- Both lanes are gated by publish-mute × the sensitivity gate, so a muted player does not mouth
|
|
20
|
+
silently at everyone.
|
|
21
|
+
|
|
22
|
+
**No world authoring is required.** `mp.attachVoice(Helix.voice, ...)` wires replica faces, the local
|
|
23
|
+
face, the screen-space face LOD and pad push-to-talk in one call. Do not build a face system, do not
|
|
24
|
+
network viseme weights, and do not drive morph targets from world code.
|
|
25
|
+
|
|
26
|
+
## The capability ladder
|
|
27
|
+
|
|
28
|
+
`helix character import` prints a `face:` line and the avatar preflight reports the same capability.
|
|
29
|
+
It degrades in this order, and each rung is honest about what the asset can do:
|
|
30
|
+
|
|
31
|
+
| capability | what the asset carries | what a speaker looks like |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `viseme15` | the 15 Oculus visemes | full lip-sync |
|
|
34
|
+
| `vowel5` | the five VRM/VRoid vowels | lip-sync folded onto five shapes (the runtime does the fold — a 15-viseme solver drives a 5-vowel asset) |
|
|
35
|
+
| `jaw` | a `jawOpen` morph or jaw bone | the jaw opens with speech amplitude |
|
|
36
|
+
| `none` | nothing | **closed mouth, forever** |
|
|
37
|
+
|
|
38
|
+
Blink channels (`blink.left` / `blink.right`) and gaze channels are separate from the ladder. With
|
|
39
|
+
them the runtime blinks and idles the eyes on its own; without them the eyes are dead. V1 never
|
|
40
|
+
fabricates blink motion for an asset that has no explicit targets.
|
|
41
|
+
|
|
42
|
+
Screen-space LOD: full facial work above 96 projected avatar pixels, jaw-only from 32, nothing below.
|
|
43
|
+
|
|
44
|
+
## Getting a face onto an imported character
|
|
45
|
+
|
|
46
|
+
### VRM (`.vrm`) — automatic, use the Bridge
|
|
47
|
+
|
|
48
|
+
`bridge_import` routes a `.vrm` through the `helix.character.vrm.import` capability, which carries the
|
|
49
|
+
five vowels, both blinks and the six expression presets across on its own. It runs in-process on
|
|
50
|
+
read/write permissions alone and needs no reviewed plan key. **Never hand-roll a VRM conversion.**
|
|
51
|
+
|
|
52
|
+
### A GLB/FBX biped — `faceMap`, or it is probably mute
|
|
53
|
+
|
|
54
|
+
`import_character` (and `helix character import`) detects only **exact, established** morph names —
|
|
55
|
+
`viseme_*`, VRChat's `vrc.v_*`, plain `aa/ih/ou/ee/oh`, `jawOpen`/`mouthOpen`, `blinkLeft`/`Blink_L`
|
|
56
|
+
and their obvious spellings. It deliberately does not guess from arbitrary names, so a rig whose
|
|
57
|
+
shapes are called anything else — including a **VRoid model exported as GLB rather than VRM**, whose
|
|
58
|
+
shapes are `Fcl_MTH_A`, `Fcl_EYE_Close_L`, … — imports with `face: none` unless you say otherwise.
|
|
59
|
+
|
|
60
|
+
So: after any character import, **read the `face:` line**. If it says `none` and the source model has
|
|
61
|
+
blend shapes, inspect their names and pass `faceMap` — a JSON file mapping HELIX semantics onto the
|
|
62
|
+
morphs the asset actually has:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"contract": "helix/avatar-face@1",
|
|
67
|
+
"channels": {
|
|
68
|
+
"vowel.aa": { "kind": "morph", "name": "Fcl_MTH_A", "scale": 1 },
|
|
69
|
+
"vowel.ih": { "kind": "morph", "name": "Fcl_MTH_I", "scale": 1 },
|
|
70
|
+
"vowel.ou": { "kind": "morph", "name": "Fcl_MTH_U", "scale": 1 },
|
|
71
|
+
"vowel.ee": { "kind": "morph", "name": "Fcl_MTH_E", "scale": 1 },
|
|
72
|
+
"vowel.oh": { "kind": "morph", "name": "Fcl_MTH_O", "scale": 1 },
|
|
73
|
+
"blink.left": { "kind": "morph", "name": "Fcl_EYE_Close_L", "scale": 1 },
|
|
74
|
+
"blink.right":{ "kind": "morph", "name": "Fcl_EYE_Close_R", "scale": 1 }
|
|
75
|
+
},
|
|
76
|
+
"lod": { "fullMinPixels": 96, "jawMinPixels": 32 }
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A `jawOpen` channel may instead be a **bone**: `{ "kind": "bone", "name": "FACIAL_C_Jaw", "axis": "y",
|
|
81
|
+
"maxDegrees": 18 }`. The importer verifies every declared morph or bone exists after conversion and
|
|
82
|
+
fails loudly if one does not, so a face map cannot silently name nothing.
|
|
83
|
+
|
|
84
|
+
A stylised avatar with no mouth shapes at all is legitimately `none` — say so plainly rather than
|
|
85
|
+
inventing channels.
|
|
86
|
+
|
|
87
|
+
### MetaHuman / Unreal
|
|
88
|
+
|
|
89
|
+
Supply the source through Bridge with `type: "character"` and the face as the named `face` input, and
|
|
90
|
+
the head/eye material path is preserved. A MetaHuman legacy head can carry 800+ blend shapes; the
|
|
91
|
+
importer drops every delta the manifest cannot drive, so declaring the channels is also what keeps the
|
|
92
|
+
web asset small.
|
|
93
|
+
|
|
94
|
+
## Add-ons do not carry faces
|
|
95
|
+
|
|
96
|
+
`helix.character.addon.import` packages a modular wearable (`character.<slot>@1`) against a validated
|
|
97
|
+
base avatar. A hat, a jacket or a pair of ears is not a face and never declares face channels — the
|
|
98
|
+
face belongs to the base body the add-on is worn on. If the base is `none`, installing add-ons does
|
|
99
|
+
not change that.
|
|
100
|
+
|
|
101
|
+
## Honest limits, as of this writing
|
|
102
|
+
|
|
103
|
+
- **Face V1 is a `0.3`-line runtime feature.** A world published on the older `^0.2` engine line
|
|
104
|
+
resolves to `0.2.x` and has no face runtime at all. Re-publishing on the current line is the only
|
|
105
|
+
way such a world gains lip-sync.
|
|
106
|
+
- **The stock Base Female body ships with no face channels.** Base Male carries the full 21-channel
|
|
107
|
+
`viseme15` set; the female face pack currently has no morph targets and no facial rig, so she is
|
|
108
|
+
`none` — mute and unblinking. Do not tell a human that "the default avatars lip-sync"; only the male
|
|
109
|
+
one does.
|
|
110
|
+
- The portable browser solver reaches 8 of the 15 viseme shapes from its acoustic features
|
|
111
|
+
(`sil, pp, ff, th, kk, ss, rr, aa`). The remaining shapes exist in the asset and are reachable by a
|
|
112
|
+
better solver, not by this one.
|
|
113
|
+
- Expressions (`happy`/`angry`/`sad`/`relaxed`/`surprised`/`neutral`) ride the same manifest as a
|
|
114
|
+
separate `expressions` block and a separate driver. They are carried by the VRM path and are not
|
|
115
|
+
driven by speech.
|
package/docs/bridge.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# HELIX Bridge — agent workflow
|
|
2
|
+
|
|
3
|
+
Bridge is the one path for turning an existing external asset, character, animation, scene, mod export, or engine project into HELIX content. Do not invent a one-off conversion shell script before asking Bridge what is installed.
|
|
4
|
+
|
|
5
|
+
## Default loop
|
|
6
|
+
|
|
7
|
+
1. `bridge_detect` fingerprints the source.
|
|
8
|
+
2. `bridge_inspect` returns the structural evidence and viable goals.
|
|
9
|
+
3. `bridge_plan` resolves the capability graph without writing or executing anything.
|
|
10
|
+
4. If the plan has blockers, use `bridge_plugins_list` and `bridge_plugin_inspect`; install a plugin only when the requested source path is available and its declared permissions match the task.
|
|
11
|
+
5. For any command-backed capability, pass the reviewed plan's `deterministicKey` back as `expectedPlanKey`, approve every exact permission in `requiredPermissions`, and call `bridge_import`. An in-process built-in-only route with no command needs no additional acknowledgement.
|
|
12
|
+
6. Read every warning and output digest, then call `bridge_validate` on the artifact or provenance record.
|
|
13
|
+
7. Publish through the existing HELIX package, item, scene, or world path. Bridge conversion is not publication authority.
|
|
14
|
+
|
|
15
|
+
For the common case, `bridge_import` performs the first three steps internally. Use the explicit detect/inspect/plan sequence when the source is unfamiliar, an engine project, or a community plugin would run.
|
|
16
|
+
|
|
17
|
+
## What Bridge can actually convert
|
|
18
|
+
|
|
19
|
+
`bridge_plugins_list` and `bridge_doctor` are the authority — read them before planning, because a
|
|
20
|
+
plugin can be installed or absent on this machine. The HELIX Verified set the CLI ships with is:
|
|
21
|
+
|
|
22
|
+
| plugin | capability | accepts → produces |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `helix.formats` | `helix.asset.copy` | glb / gltf / fbx / obj / usd / usdz / vrm / image / audio → a checksummed package input (preserved, not converted) |
|
|
25
|
+
| `helix.characters` | `helix.character.import` | glb / fbx biped → `helix-humanoid@1` character (retarget, conform, LODs, KTX2) |
|
|
26
|
+
| `helix.characters` | `helix.character.vrm.import` | **`.vrm` → `helix-humanoid@1`**, carrying SpringBone physics, the five vowel visemes, both blinks and the expression presets |
|
|
27
|
+
| `helix.characters` | `helix.animation.retarget` | glb / fbx clip → `helix-humanoid@1` animation |
|
|
28
|
+
| `helix.characters` | `helix.character.addon.import` | glb / fbx part + a published host → a modular add-on and its runtime `character.<slot>@1` contract |
|
|
29
|
+
| `helix.metahuman` | `metahuman.face.materials` | a MetaHuman face → a character-face package with the eyes preserved |
|
|
30
|
+
| `helix.scenes` | `helix.scene.package` | glb / fbx / obj → a deterministic HELIX Scene package |
|
|
31
|
+
| `helix.engines` | `unity.project.extract`, `godot.project.extract` | a Unity or Godot **project** → extracted assets |
|
|
32
|
+
| `helix.bridge.unreal` | `unreal.project.extract` | an Unreal **project** → FBX assets (launches Unreal Editor) |
|
|
33
|
+
|
|
34
|
+
Shipped alongside, but **not** HELIX Verified: `hypersonic.beamng-vehicle`
|
|
35
|
+
(`beamng.vehicle.import`, trust `local-untrusted`) converts a BeamNG vehicle directory to a glb. It
|
|
36
|
+
spawns Blender, so it requires `spawn-process`, `blender` and `unrestricted-host-execution` plus a
|
|
37
|
+
reviewed plan key and a named `palette` input — approve it deliberately, never as a formality.
|
|
38
|
+
|
|
39
|
+
**The VRM route is in-process** — a VRM is a glTF and the whole conversion is TypeScript over
|
|
40
|
+
`gltf-transform` — so `helix bridge import avatar.vrm` runs on read-source/write-output alone and
|
|
41
|
+
needs no reviewed plan key. A **VRChat Unity avatar is the opposite case** and belongs to a
|
|
42
|
+
Unity-permissioned extraction, not this capability; some creators ship a `.vrm` alongside, and when
|
|
43
|
+
they do, the VRM capability handles it and the Unity adapter is not needed at all.
|
|
44
|
+
|
|
45
|
+
**There is no bridge for most game formats.** Cities: Skylines, Source, Bethesda archives, and the
|
|
46
|
+
rest have no capability today. When nothing accepts the source, say so plainly — do not hand-roll a
|
|
47
|
+
converter, and do not route the source through an unrelated capability because it is the only one
|
|
48
|
+
that would accept the file extension.
|
|
49
|
+
|
|
50
|
+
## Faces
|
|
51
|
+
|
|
52
|
+
A character imported through any of these routes is **mute unless a `helix/avatar-face@1` manifest
|
|
53
|
+
rides its GLB**, and nothing downstream can add one later. The VRM capability carries a face across on
|
|
54
|
+
its own. `helix.character.import` detects only established morph names, so a rig named anything else —
|
|
55
|
+
including a VRoid model exported as GLB rather than VRM — needs an explicit face map. An add-on never
|
|
56
|
+
carries a face; the face belongs to the base body. Read `read_doc({ name: "avatar-face" })` before
|
|
57
|
+
importing any character a human will speak through.
|
|
58
|
+
|
|
59
|
+
## Safety and permissions
|
|
60
|
+
|
|
61
|
+
Plugin installation and plugin execution are separate decisions. Installed plugins are discoverable, but every command-backed capability—including a packaged HELIX Verified adapter—runs only when `allowPermissions` contains every permission in the selected plan.
|
|
62
|
+
|
|
63
|
+
Never add permission names merely to make a plan green. Report the plugin id, capability, requested permissions, and why they are needed. A plan may be inspected safely without permission grants.
|
|
64
|
+
|
|
65
|
+
Command-backed execution is bound to the reviewed plan. `bridge_import` rejects a missing or changed `expectedPlanKey` before reserving output, snapshotting content, or starting a process. If the source, plugin digest, route, target, tool identity, or inputs change, call `bridge_plan` again and review the new key.
|
|
66
|
+
|
|
67
|
+
Pass the same `headTexture`, `eyeTexture`, `canonical`, `boneMap`, and named `inputs` to both `bridge_plan` and `bridge_import`. Bridge fingerprints them before planning, so changing any auxiliary file or a safe FBX/glTF sidecar changes the reviewed key.
|
|
68
|
+
|
|
69
|
+
Bridge executes plugins without a shell, bounds runtime and stdout, requires artifacts to remain inside the output directory, and re-hashes outputs itself. Command-backed capabilities must request `unrestricted-host-execution` in addition to process and filesystem permissions. That name is deliberate: until an isolated runner ships, approval grants the adapter the authority of a local process rather than pretending the narrower declarations are an OS sandbox. Prefer HELIX Verified capabilities when they satisfy the plan, while still approving the exact permissions they request.
|
|
70
|
+
|
|
71
|
+
## MetaHuman and Unreal character material path
|
|
72
|
+
|
|
73
|
+
The official MetaHuman face capability carries the eye-material safeguard migrated from `helix-web-engine-new` PR #99:
|
|
74
|
+
|
|
75
|
+
- provide the baked head texture with `headTexture`;
|
|
76
|
+
- omit `eyeTexture` to use the CLI-packaged canonical bake, or supply an explicit override;
|
|
77
|
+
- Bridge refuses textureless `MI_EyeL_Baked` / `MI_EyeR_Baked` materials;
|
|
78
|
+
- validate a combined base avatar with `bridge_validate({ baseAvatar: true })` before publication.
|
|
79
|
+
|
|
80
|
+
## Official Unreal and modular-character path
|
|
81
|
+
|
|
82
|
+
The bundled CLI packages `helix.bridge.unreal`, a HELIX Verified, command-backed Unreal skeletal-mesh adapter. Supply an Unreal asset or asset-set profile as the named `unrealAsset` input. Use `type: "extract"` to retain every FBX from a batch, then run each retained asset through its character or add-on import. Because the adapter launches Unreal Editor, its plan requires a reviewed plan key plus the exact filesystem, process, host-execution, and `unreal-editor` permissions reported by `bridge_plan`.
|
|
83
|
+
|
|
84
|
+
Use `type: "addon"` with named `host` and `addon` inputs to package a modular wearable against a validated base avatar. The add-on profile carries the runtime `character.<slot>@1` contract used by the customizer. Split body and face exports can use `type: "character"` with the face supplied as the named `face` input; Bridge preserves the shared rig, LODs, morphs, and material roles in the composed package.
|
|
85
|
+
|
|
86
|
+
## Tool boundary
|
|
87
|
+
|
|
88
|
+
Bridge implementation lives in `{{CLI_PKG}}`. These MCP tools either execute that bundled CLI or return its output. Do not port converter logic into the MCP server.
|
|
89
|
+
|
|
90
|
+
The canonical schemas are:
|
|
91
|
+
|
|
92
|
+
- `helix.bridge-detection/1`
|
|
93
|
+
- `helix.bridge-plan/1`
|
|
94
|
+
- `helix.bridge-plugin/1`
|
|
95
|
+
- `helix.bridge-plugin-request/1`
|
|
96
|
+
- `helix.bridge-provenance/1`
|
|
97
|
+
|
|
98
|
+
Use `bridge_doctor` to see in-process plugins, the packaged official Unreal adapter, and optional engine adapters. Engine projects can always be detected and planned; execution requires an eligible extraction capability and every permission reported by that plan.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Bring your world to HELIX — adopt the platform in an existing three.js project
|
|
2
|
+
|
|
3
|
+
Follow this when a game **already exists outside HELIX** — a plain three.js/vite project, a game-jam build, a
|
|
4
|
+
port from another platform — and you want it published as a HELIX Instant world. You do NOT rewrite it into a
|
|
5
|
+
scaffold shape: adoption is **staged**, each stage ships value on its own, and your scene/loop/code stay yours
|
|
6
|
+
throughout. (Converting a world that is already HELIX-shaped from single-player to multiplayer? Skip to the
|
|
7
|
+
hub's conversion section: `get_started({ kind: "multiplayer" })`.)
|
|
8
|
+
|
|
9
|
+
**The three stages:**
|
|
10
|
+
|
|
11
|
+
1. **Become a world** — your game runs on the platform as-is (own character/controller, single-player).
|
|
12
|
+
2. **Adopt the character** — swap your controller for the `humanoid-character` system (avatars, abilities).
|
|
13
|
+
3. **Go multiplayer** — one facade call + the manifest opt-in, then the authority audit.
|
|
14
|
+
|
|
15
|
+
Stop at any stage; each is a publishable world.
|
|
16
|
+
|
|
17
|
+
## Stage 1 — Become a world
|
|
18
|
+
|
|
19
|
+
A HELIX world is a **static bundle** (an `index.html` entry + assets) plus a **manifest** (`public/helix.json`).
|
|
20
|
+
If your project builds to a static `dist/` with vite (or any bundler), it qualifies already.
|
|
21
|
+
|
|
22
|
+
1. **Start with `scaffold_world`** to get the CLI command and recipe for your EXISTING project directory. The CLI does not overwrite by
|
|
23
|
+
default, so it only adds what you're missing:
|
|
24
|
+
- `public/helix.json` — the manifest (title/slug/entry/permissions). Start `kind: "character"` even if you
|
|
25
|
+
keep your own controller; trim what doesn't apply.
|
|
26
|
+
- `index.html` — if you have one, keep yours and copy over the TWO platform pieces from the scaffolded
|
|
27
|
+
reference: the `helix:three` import-map markers (only if you adopt the character in stage 2) and the
|
|
28
|
+
`#loading` overlay + `src/loading.ts` (optional but recommended — count-based progress off
|
|
29
|
+
`THREE.DefaultLoadingManager`, which your existing loaders already feed).
|
|
30
|
+
2. **`Helix.init()` first.** Add `"@hypersoniclabs/helix-sdk": "{{SDK_DEP_SPEC}}"` to dependencies and call
|
|
31
|
+
`await Helix.init()` before your game boots. Not embedded → `{ embedded: false }` and your game runs exactly
|
|
32
|
+
as before (the standalone path must keep working — the golden rule).
|
|
33
|
+
3. **Check the platform rules that bite existing games:**
|
|
34
|
+
- **HUD placement**: the player shell overlays chrome across the top-center (~top 56px: Exit / Save /
|
|
35
|
+
helixOS). Move any top-center UI to a corner or the bottom.
|
|
36
|
+
- **Build shape**: `base: './'` in vite config (relative asset URLs); no server-side anything — the bundle
|
|
37
|
+
is static.
|
|
38
|
+
- **NO ROOT-ABSOLUTE URLs into your own bundle.** This is the one that bites ported games hardest: a
|
|
39
|
+
published world is served from `…/instant-worlds/<id>/<build>/`, so every `'/assets/x.glb'`,
|
|
40
|
+
`fetch('/data.json')` or `new Audio('/theme.mp3')` your game already has resolves against the SITE
|
|
41
|
+
root, 404s, and usually throws. Rewrite each one against the document —
|
|
42
|
+
`new URL('assets/x.glb', document.baseURI).href` — before you publish.
|
|
43
|
+
- **Big assets**: they bundle as-is at this stage; that's fine. (Character assets in stage 2 stream from
|
|
44
|
+
the platform CDN instead.)
|
|
45
|
+
4. **Build + publish**: `npm run build` → `validate_world` on `dist/` (fix every problem) →
|
|
46
|
+
**`helix verify-subpath dist`** (REQUIRED — serves the bundle from a nested path and catches exactly the
|
|
47
|
+
root-absolute 404s above; local preview serves from the origin root and cannot see them) → `whoami` →
|
|
48
|
+
`publish_world`. Your game is now a HELIX world.
|
|
49
|
+
|
|
50
|
+
## Stage 2 — Adopt the character (required for multiplayer presence)
|
|
51
|
+
|
|
52
|
+
Swap your hand-rolled controller for the platform character: physics-driven chassis, locomotion + animation,
|
|
53
|
+
first/third-person cameras, ability host, and every player's **universal avatar**.
|
|
54
|
+
|
|
55
|
+
1. Pin it in the manifest: `"systems": { "humanoid-character": "^0.3" }`, then **`install_world_packages`**
|
|
56
|
+
(resolves the pin into `public/helix_modules/`, rewrites the import map, generates `src/helix.runtime.ts`).
|
|
57
|
+
Add the vite alias + tsconfig path from the character recipe §3–4.
|
|
58
|
+
2. **Externalize Three, Rapier, and platform systems** (`build.rollupOptions.external: (id) =>
|
|
59
|
+
id === 'three' || id === '@dimforge/rapier3d-compat' || id.startsWith('@helix/')`, plus the import-map
|
|
60
|
+
markers): the platform hosts the single shared Three; two copies break `instanceof` and
|
|
61
|
+
silently corrupt rendering. The character system imports `@dimforge/rapier3d-compat` by bare specifier, and
|
|
62
|
+
`helix install` maps it to the platform-hosted physics engine instead of baking ~2 MB into your bundle.
|
|
63
|
+
Your code keeps importing both normally — only the resolution changes. `three` moves to devDependencies
|
|
64
|
+
and onto the platform's line (`^0.185.1`, what `^0.3` of the system hosts — an older devDep bundles
|
|
65
|
+
mismatched jsm addons against it); `@dimforge/rapier3d-compat` goes there too (types + `helix dev`), never
|
|
66
|
+
to dependencies. Compatible
|
|
67
|
+
`@helix/*` systems update safely on fresh launch; validation and publish refuse a system-bearing build
|
|
68
|
+
that leaves them bundled.
|
|
69
|
+
3. Replace your controller with the facade in single-player mode — your scene, camera, and loop stay:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { CharacterMultiplayer, RapierBody } from '@helix/humanoid-character';
|
|
73
|
+
import { SYSTEM_ASSET_BASE, TRANSCODER_PATH } from './helix.runtime';
|
|
74
|
+
|
|
75
|
+
const body = await RapierBody.create({ position: SPAWN });
|
|
76
|
+
// Rebuild your walkable surfaces as static colliders on this body (box/capsule approximations are fine):
|
|
77
|
+
body.addStaticCuboid({ x: 24, y: 0.5, z: 24 }, { x: 0, y: -0.5, z: 0 });
|
|
78
|
+
|
|
79
|
+
const mp = await CharacterMultiplayer.create({
|
|
80
|
+
helix: Helix, renderer, scene, camera, body,
|
|
81
|
+
assetBase: SYSTEM_ASSET_BASE, transcoderPath: TRANSCODER_PATH, spawn: SPAWN,
|
|
82
|
+
join: false, // stage 2: character only — flip to multiplayer in stage 3
|
|
83
|
+
});
|
|
84
|
+
// your loop: mp.update(dt) each frame, before your game logic.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Map your old controller's feel onto the character **config** (camera mode/pitch, movement axis, speeds —
|
|
88
|
+
character recipe §8a; top-down and side-on are config, not code). Your old NON-movement key handling
|
|
89
|
+
(attack, dash, interact, menus — any raw `addEventListener('keydown', …)`) moves onto the input router:
|
|
90
|
+
create ONE `InputService`, pass it as `input` to the create call, and re-home each verb with
|
|
91
|
+
`input.registerAction` / the standard ids (character recipe §8c) — that's what makes them gamepad-ready,
|
|
92
|
+
rebindable, and menu-context aware. Delete your controller code last, once the feel matches.
|
|
93
|
+
4. Rebuild, `validate_world`, republish. Players now walk your world as their avatar.
|
|
94
|
+
|
|
95
|
+
## Stage 3 — Go multiplayer
|
|
96
|
+
|
|
97
|
+
1. Manifest: `helixVersion: "0.3"`, `maxPlayers > 1`, add the `"multiplayer"` permission and
|
|
98
|
+
`"multiplayer": { "authoritative": true }`.
|
|
99
|
+
2. Delete `join: false` — the facade now joins when embedded, and brings replicas, nameplates, and the
|
|
100
|
+
reconciler with it. That's presence done.
|
|
101
|
+
3. **The authority audit** — the step that makes or breaks a conversion. Your existing game code is full of
|
|
102
|
+
self-authority multiplayer must not keep (client-side teleports, local score state, local round resets).
|
|
103
|
+
Work through the hub's conversion sequence steps 3–7 (`get_started({ kind: "multiplayer" })`): every
|
|
104
|
+
position write becomes a server `respawn`/`teleport` rule; shared values move into declared `state` read via
|
|
105
|
+
the typed accessors; **verify with two browser windows**.
|
|
106
|
+
4. Game logic (zones, timers, entities, scores) is declared DATA in the manifest — `read_doc({ name:
|
|
107
|
+
"multiplayer-logic" })` + the closest template from the hub's index.
|
|
108
|
+
|
|
109
|
+
## The rules that matter (adoption)
|
|
110
|
+
|
|
111
|
+
- **Each stage must leave the standalone path working** — `Helix.init()` returning `embedded: false` is your
|
|
112
|
+
game exactly as it was.
|
|
113
|
+
- **Never two copies of three** after stage 2 — externalize + import map, or rendering corrupts silently.
|
|
114
|
+
`@dimforge/rapier3d-compat` is external and import-mapped for the same reason: one hosted instance, never bundled.
|
|
115
|
+
- **Your geometry/visuals/loop are never the platform's business.** The platform owns identity (login, avatars),
|
|
116
|
+
the character system, and the multiplayer room; everything else stays your code.
|
|
117
|
+
- **Publish early** — stage 1 is a real release; feedback on the platform beats a big-bang conversion.
|