@hypersoniclabs/helix-mcp 0.2.4 → 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.
Files changed (98) hide show
  1. package/README.md +81 -11
  2. package/dist/continuumCanary.d.ts +17 -0
  3. package/dist/continuumCanary.js +17 -0
  4. package/dist/continuumCanary.js.map +1 -0
  5. package/dist/server.d.ts +14 -1
  6. package/dist/server.js +4058 -162
  7. package/dist/server.js.map +1 -1
  8. package/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/vehicleTools.d.ts +86 -0
  10. package/dist/vehicleTools.js +229 -0
  11. package/dist/vehicleTools.js.map +1 -0
  12. package/docs/avatar-face.md +115 -0
  13. package/docs/bridge.md +98 -0
  14. package/docs/bring-your-world.md +117 -0
  15. package/docs/catalog.md +69 -1
  16. package/docs/character-animation.md +442 -0
  17. package/docs/character-attachments.md +166 -0
  18. package/docs/character-world.md +785 -130
  19. package/docs/continuum.md +153 -0
  20. package/docs/items.md +73 -0
  21. package/docs/lighting-world.md +667 -0
  22. package/docs/locomotion-clip-spec.md +294 -0
  23. package/docs/manifest.md +31 -6
  24. package/docs/multiplayer-logic.md +460 -17
  25. package/docs/multiplayer-templates/chrono-orchard.md +36 -22
  26. package/docs/multiplayer-templates/collect-a-thon.md +28 -38
  27. package/docs/multiplayer-templates/collections.md +24 -24
  28. package/docs/multiplayer-templates/hangout.md +124 -111
  29. package/docs/multiplayer-templates/npc-wave.md +310 -0
  30. package/docs/multiplayer-templates/obby.md +13 -16
  31. package/docs/multiplayer-templates/persistent-progress.md +218 -0
  32. package/docs/multiplayer-templates/physics-bumper.md +27 -9
  33. package/docs/multiplayer-templates/physics-football.md +22 -8
  34. package/docs/multiplayer-templates/relic-bearers.md +12 -15
  35. package/docs/multiplayer-templates/server-motion.md +16 -19
  36. package/docs/multiplayer-templates/shooter-range.md +275 -0
  37. package/docs/multiplayer-templates/team-control.md +28 -15
  38. package/docs/multiplayer-templates/turn-arena.md +31 -22
  39. package/docs/multiplayer-templates/voice-radio.md +166 -0
  40. package/docs/multiplayer-templates/wave-survival.md +7 -8
  41. package/docs/multiplayer-templates/world-shop.md +240 -0
  42. package/docs/multiplayer-world.md +222 -129
  43. package/docs/npc-world.md +623 -0
  44. package/docs/publishing.md +108 -28
  45. package/docs/purchases.md +223 -0
  46. package/docs/scene-performance.md +64 -0
  47. package/docs/screenshots.md +140 -0
  48. package/docs/sdk.md +324 -5
  49. package/docs/shooter-worlds.md +537 -0
  50. package/docs/terrain.md +173 -0
  51. package/docs/upgrades.md +324 -0
  52. package/docs/vehicles.md +727 -0
  53. package/docs/world-inspect.md +156 -0
  54. package/docs/world-look.md +241 -0
  55. package/docs/world-recipe.md +65 -6
  56. package/package.json +15 -4
  57. package/skills/README.md +91 -0
  58. package/skills/helix-assets/SKILL.md +491 -0
  59. package/skills/helix-assets/references/asset-sources.md +143 -0
  60. package/skills/helix-assets/references/vault-api.md +105 -0
  61. package/skills/helix-avatar-qa/SKILL.md +85 -0
  62. package/skills/helix-avatars/SKILL.md +206 -0
  63. package/skills/helix-avatars/references/contract.md +166 -0
  64. package/skills/helix-avatars/references/dynamics.md +367 -0
  65. package/skills/helix-avatars/references/face.md +50 -0
  66. package/skills/helix-avatars/references/publish.md +76 -0
  67. package/skills/helix-avatars/references/qa.md +251 -0
  68. package/skills/helix-avatars/references/rigging.md +88 -0
  69. package/skills/helix-avatars/references/source-generated.md +190 -0
  70. package/skills/helix-avatars/references/source-model.md +90 -0
  71. package/skills/helix-avatars/references/source-rigid.md +90 -0
  72. package/skills/helix-avatars/references/source-vrm.md +61 -0
  73. package/skills/helix-gauntlet/SKILL.md +128 -0
  74. package/skills/helix-multiplayer/SKILL.md +150 -0
  75. package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
  76. package/skills/helix-vehicles/SKILL.md +218 -0
  77. package/skills/helix-vehicles/references/addons.md +212 -0
  78. package/skills/helix-vehicles/references/appearance.md +339 -0
  79. package/skills/helix-vehicles/references/audio-import.md +138 -0
  80. package/skills/helix-vehicles/references/audio.md +580 -0
  81. package/skills/helix-vehicles/references/cabin.md +225 -0
  82. package/skills/helix-vehicles/references/host-manifest.md +174 -0
  83. package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
  84. package/skills/helix-vehicles/references/publish.md +214 -0
  85. package/skills/helix-vehicles/references/qa.md +177 -0
  86. package/skills/helix-vehicles/references/reference-package.json +3481 -0
  87. package/skills/helix-vehicles/references/reference-package.md +69 -0
  88. package/skills/helix-vehicles/references/source-beamng.md +167 -0
  89. package/skills/helix-vehicles/references/source-concept.md +38 -0
  90. package/skills/helix-vehicles/references/source-model.md +100 -0
  91. package/skills/helix-vehicles/references/source-scratch.md +60 -0
  92. package/skills/helix-world-build/SKILL.md +376 -0
  93. package/skills/helix-world-build/references/config-gates.md +104 -0
  94. package/skills/helix-world-director/SKILL.md +210 -0
  95. package/skills/helix-world-qa/SKILL.md +371 -0
  96. package/skills/helix-world-qa/references/perf-budgets.md +240 -0
  97. package/skills/helix-world-qa/references/perf-handle.md +125 -0
  98. package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
@@ -0,0 +1,727 @@
1
+ # HELIX Vehicle Contract
2
+
3
+ The rules a drivable vehicle item and its add-ons must meet to work the first time on the live
4
+ platform. Every rule here was checked against the engine, backend and CLI code as of October 2026.
5
+ Each one names the failure it prevents. The workflow that applies these rules is the
6
+ `helix-vehicles` skill: run `get_started({ kind: "vehicle" })`.
7
+
8
+ **Vocabulary.**
9
+
10
+ - **Vehicle item**: a universal item of kind `vehicle`. It is one GLB plus a **vehicle package**,
11
+ which is `helix.vehicle-package/2` JSON.
12
+ - **Continuum Package**: the sealed, content-addressed closure that holds the item's model, its
13
+ package JSON and its audio. Every item that is drawn, listed or sold is *pinned* to an exact
14
+ Package Version.
15
+ - **Add-on**: a separate item of kind `add_on`. It fits a vehicle slot and is sealed into its own
16
+ `addon` Package.
17
+ - **landV2**: the v2 drivetrain solver, with a clutch, differentials, ABS and a brush tyre model. A
18
+ car runs on landV2 **only if its package authors `simulation.land`**.
19
+
20
+ ---
21
+
22
+ ## 1. Frame, units, scale
23
+
24
+ | Rule | Prevents |
25
+ | --- | --- |
26
+ | The model frame is glTF **+X = vehicle LEFT, +Y up, +Z FORWARD**, in metres. The nose faces +Z. | A mirrored or backwards car. Left and right seats, doors and lamps swap. |
27
+ | Bake real-world size into the vertices. The item is placed at scale 1, and placement scale must be exactly 1. | A refused spawn: "physics does not support placement scale". |
28
+ | Keep every node scale at 1 and put no scale on the node chain. Bake scale and rotation into the vertex data. Moving parts keep only a *translation* to their pivot (see section 3). | The publish verifier measuring bounds wrongly. It reported a scale-1.4 car as 2.16 × 1.68 × 4.06 m instead of 1.82 × 1.54 × 3.57 m. |
29
+ | Centre the car on X and Z and put its lowest point at Y = 0 *before* upload. Author every socket in that same frame. | The upload door (`centerAndGroundSnapGlb`, tolerance 2 mm) re-centring a `vehicle` GLB. That moves the geometry under sockets you quoted in the old frame, and the driver ends up sitting underground. |
30
+ | Use a real length. The runtime flags a length outside 0.5–2× its class range as "check export scale (cm vs m?)". The longest horizontal axis must be Z. | A car exported in centimetres, or rotated 90°. |
31
+ | The tyre bottoms sit at Y = 0 at rest, and the wheel radius is the measured rolling radius of what is actually drawn. A car with **staggered tyres** (wider or taller at the rear) draws each axle at its own size and sets `simulation.land.tyreByAxle[].radius` per axle (section 9). | Floating or sunk tyres. A nominal 0.314 m radius on a rim-only mesh floated a car by 80 mm. Equalising staggered tyres to one size makes the car wrong. |
32
+
33
+ ## 2. Mesh parts and node names
34
+
35
+ A vehicle is **few nodes, one material each**. The runtime moves, hides or lights each named node
36
+ independently. Everything that does none of those things merges into the body.
37
+
38
+ Each separately addressed node costs at least one draw call, because draws are counted per glTF
39
+ primitive per placing node (see section 8). **The node list is the draw budget.**
40
+
41
+ | Part | Node | Required | Rule, and the failure it prevents |
42
+ | --- | --- | --- | --- |
43
+ | Body | `bodyshell`, for everything static: shell, trim, underbody, engine bay, static interior, dash, seats, gauges and housings. | yes | Merge, don't pad. Every extra static node is a draw for nothing. |
44
+ | Wheels | Four nodes that resolve to `wheel_lf/rf/lr/rr`. Canonical names are `rim_lf`, `rim_rf`, `rim_lr`, `rim_rr`, each holding rim + tyre + disc in one mesh. The suffix is side then end: `lf` is left-front. | yes | **All four or none.** The runtime discards a partial set (1–3 wheels) and draws placeholder cylinders, while your wheels stay welded to the body. These names never count as wheels: `arch`, `well`, `liner`, `flare`, `guard`, `mudguard`, `mudflap`, `fender`, `cover`, `hubcap`, `spare`, `nut`, `bolt`. |
45
+ | Brake calipers | Merge into the body, or leave them out. | no | Calipers are not claimed by the wheel, so they stay body-fixed and do not steer. A caliper that lands at the origin, as happens with unapplied BeamNG flexbodies, must be dropped. |
46
+ | Steering wheel | `steering` | strongly | Rotates with steering input in first-person view. If it is missing, the runtime derives a socket and nothing turns. |
47
+ | Pedals and gearshift | `pedal_throttle`, `pedal_brake`, `pedal_clutch`, `gearshift` | no | If they are missing, the runtime derives sockets. **Never fabricate parts the source lacks** (pedals, fog lamps, gauge needles). Ship without them and record that. |
48
+ | Doors | `door_lf`, `door_rf` (plus `door_lr` and `door_rr`). Door glass is its own node, such as `doorglass_lf`. | for opening doors | Each moving panel is its own node. Glass that blends cannot share a primitive with opaque paint. |
49
+ | Bonnet and boot | `bonnet`, `boot`. Aliases: `hood`, `trunk`, `tailgate`. On a rear-engined car, `boot` is the engine lid. | for opening lids | Separate nodes; hinge data goes in the package (section 6). |
50
+ | Glass | `glass`, the static glazing in one node, with `alphaMode: BLEND`. | yes, if it has windows | Glass never casts a shadow and is protected from the cabin distance gate. |
51
+ | Lamps | One node per light **function**, each with its own unshared material. Section 4 lists them. | for working lights | A shared lamp material lit the whole headlight when only the indicator should have lit. |
52
+ | Exhaust | `exhaust` | if an exhaust add-on should replace it | An add-on can only hide nodes that exist. |
53
+ | Add-on hide targets | Any node that an add-on replaces, such as an engine cover, intercooler or wing. | per add-on plan | Each one costs a draw. **Plan the add-on set before merging.** |
54
+ | Cabin | Interior geometry may live in `bodyshell`. A separate cabin node must be named `cabin`, never `interior`. | no | `interior` is the engine's own cabin root name, so the node collides with it. |
55
+
56
+ - Names are normalised before matching. The runtime strips a common prefix (`subik_door_FL` →
57
+ `door_FL`), Blender `.001` suffixes and namespaces. `_fl` ≡ `_lf`. `wheel_0..3` are lf, rf, lr, rr.
58
+ Near-miss names produce warnings, so read them.
59
+ - Do not use these reserved names: `chassis`, `vehicle`, `interior`, `road-wheel`, `helix-rootparts`.
60
+ - Make every material inside one node agree on render state: glass nodes blend and everything else
61
+ is opaque. Drop `KHR_materials_transmission`, `ior`, `clearcoat`, `volume`, `specular` and
62
+ `sheen`. Prevents: one node drawing twice, and transmission re-rendering the whole scene.
63
+ - A node referenced by the package (in parts, articulation or `componentBuild`) must exist in the
64
+ **default scene**. Prevents: a spoiler that existed in the GLB but never rendered.
65
+
66
+ ## 3. Pivots and origins
67
+
68
+ | Node | Origin must be | Prevents |
69
+ | --- | --- | --- |
70
+ | Each wheel (`rim_*`, and `tyre_*` if separate) | **The hub centre.** That is the centre of the tyre cylinder, with geometry local to it, spinning about local X. Mirror the right side by geometry or by a 180° Y rotation. | **Wheel add-ons appearing at the vehicle centre.** A socket-frame add-on anchors at the hidden node's origin, in both the customizer and the world. The 993 baked every wheel at an identity node, so all four add-on copies landed at (0,0,0) while every structural check passed. |
71
+ | The wheel group's bounding-box centre | The hub. The runtime re-pivots the adopted wheel group on its box centre. | Wheels orbiting the axle as they spin. |
72
+ | Steering wheel | Its hub, on the column axis. | A wheel that orbits instead of turning. |
73
+ | Doors and lids | Anything you like. The hinge is declared numerically in the package (section 6). | — |
74
+ | Parts that add-ons bind to (`boot` for wings) | Keep the node; add-ons use a `bind` matrix against it. | A wing that does not follow the lid. |
75
+
76
+ **Measure camber from a fit of the tread cylinder plus a spin sweep at runtime, never from a PCA
77
+ of the rim's vertices.** Spoke and dish asymmetry fakes 2–3° of camber on a straight wheel.
78
+
79
+ ## 4. Lamps
80
+
81
+ - **One node per light function**, each with its **own** material that no other node shares.
82
+ The 993's set was `headlamps`, `foglights`, `taillamps`, `chmsl`, `reverselights`,
83
+ `indicator_l`, `indicator_r` and `numberplate`. Merge left and right of one function into one
84
+ node when they use the same image.
85
+ - **Material authoring.** Set `emissiveFactor` to the lit colour,
86
+ `KHR_materials_emissive_strength: 0` so the lamp is untouched when off, and give it an explicit
87
+ `emissiveTexture`: a white 8×8, or the source's on-state map in the same UV space. This prevents
88
+ two failures. The presenter only raises `emissiveIntensity` and never writes a colour, so a lamp
89
+ with no `emissiveFactor` lights black. And with no emissive map it borrows the albedo, so a
90
+ dark atlas texel lights dark.
91
+ - **What actually lights a lens** is a row in `specialty.componentBuild.materials` that drives
92
+ `emissive` from a signal. The row's `slot` matches a mesh name, a material name or **any named
93
+ ancestor**. Never point a row at a housing node that has lamp children, or every lamp under it
94
+ lights together.
95
+ - **Coronas and beams.** Additive glow sprites are drawn at lamp *sockets*, and up to two spotlight
96
+ beams are drawn on the camera's own car. The socket names are `headlight_l/r`, `lowbeam_l/r`,
97
+ `highbeam_l/r`, `taillight_l/r`, `brakelight_l/r/c`, `reverselight_l/r`, `indicator_lf/rf/lr/rr`,
98
+ `foglight_fl/fr/r`, `plate_light`, `interiorlight` and `beacon_1..4`. Corona colours come from a
99
+ preset and cannot be authored.
100
+ - **`lights.lamps[]` in the package is validated but nothing reads it at runtime.** Fill it
101
+ honestly, but it lights nothing.
102
+ - Brake, reverse, tail and plate lamps are derived from driving state. Headlights, indicators,
103
+ hazards and fog lamps need specialty actions (section 6).
104
+ - **Indicators cover the whole car, both ends of each side.** A real car flashes at the front,
105
+ the rear and, where it has them, the side repeaters. `indicator_l` and `indicator_r` must each
106
+ contain a front lens and a rear lens (one node per side holding both meshes, as the 993's do),
107
+ and every repeater-like lens is either inside that node or has its own `signal_L` / `signal_R`
108
+ row. Judge the **lit geometry**, not node names: the bounds of the meshes the indicator rows
109
+ resolve to must reach the front and the rear quarter of the car. An amber rear lens painted into
110
+ the body texture can never flash (round 8's F40 lit its front pair only; the owner found it by
111
+ pressing the hazard key and looking at the back). `validate_vehicle` fails
112
+ `LAMP_INDICATOR_REAR_MISSING` (naming any unlit amber lens at the rear), `LAMP_NODE_UNLIT` /
113
+ `LAMP_REPEATER_UNLIT` (a lens-like node no row lights) and `LAMP_FUNCTION_NO_ROW` (a lamp node
114
+ without the rows its function needs: low and high beam, fog, tail and brake, reverse), and
115
+ `preview_vehicle` presses each lamp key and fails `vehicle.lamps.each_function_lights` naming
116
+ the lens that stays dark. Model the rear lens as its own mesh with its own emissive material.
117
+ - **A car that genuinely has no rear indicators** (rare) sets
118
+ `lights.noRearIndicators` to a sentence saying why, with its source. It turns the error into a
119
+ warning that quotes the reason. It never hides a rear lens the model does carry.
120
+ - **Brake lamp at a standstill is expected with a driver seated.** The platform holds the brake
121
+ for a seated driver who touches nothing (auto-hold, `VehicleDriveAbility` `AUTO_HOLD`), and the
122
+ brake lamp follows the applied brake, as on a car with Auto Hold: lit at a stop, off the moment
123
+ the car is driven or reversed (measured on the 993 and the F40). An unoccupied parked car's brake
124
+ lamp is off. This is not an authoring defect and no gate flags it; do not remove the brake
125
+ rows to hide it. (Known platform nit: the hold, and so the lamp, also stays on with the
126
+ ignition off while the driver is seated.)
127
+ - Keep a saturated red emissive below about 2, because ACES tone mapping drifts it orange. Ship
128
+ only the lamps the source actually has.
129
+
130
+ ## 5. Seats, sockets and the cabin
131
+
132
+ - A seat socket is the occupant's **H-point (hip joint)**. It is not the cushion and not the
133
+ floor. Ids are `seat_dside_f` (driver, on the +X / left side), `seat_pside_f`, `seat_dside_r1`
134
+ and `seat_pside_r1`. Only `seat_dside_f` drives.
135
+ - **Declare every seat in `mesh.sockets`** and in the GLB's host manifest,
136
+ `asset.extras.HELIX_vehicle.sockets` (section 12), in the model frame. The class preset derives seats from the cabin anyway; a sedan derives four. If the seat
137
+ nearest an approaching player is undeclared, the player cannot board. A boat with two declared
138
+ seats was unboardable this way.
139
+ - **Seats only grow.** A declared seat can never be removed in a later version, and the backend
140
+ refuses it (`vehicle_identity_changed`). Add rear seats only if an adult's head clears the glass.
141
+ - Authored H-points are trusted down to 0.12 m above the floor. Aim for: crown to roof lining
142
+ ≥ 10 mm (20 mm achieved on the 993), pelvis ≤ 0.12 m above the cushion, soles within
143
+ −0.05 to +0.30 m of the floor, and the driver's hand within 0.07 m of the rim. These are the
144
+ visual-QA occupant thresholds.
145
+ - To lower an occupant, move the seat socket. Change it everywhere it is declared: the package
146
+ `mesh.sockets`, the GLB's `asset.extras.HELIX_vehicle.sockets` and any `seat_*` node.
147
+ - The cabin around the seats (seat shells, dash, legible gauges, the real steering wheel design,
148
+ pedals, door cards) and how the runtime's `cabinFit` moves a seat that does not fit are in
149
+ `read_skill({ name: "helix-vehicles", reference: "cabin" })`.
150
+ - Other anchors are `engine`, `exhaust`, `cam_chase`, `cam_hood`, `numberplate`, `gearshift` and
151
+ `ignition`. Socket resolution priority is: manifest `sockets` > named node > skin joint >
152
+ auto-detected > class preset. A mesh with no recognised names still spawns, with preset
153
+ geometry.
154
+ - **The exit point** is half the car's width plus 0.7 m outward from the seat side, at ground
155
+ level.
156
+ - **Seat occupancy lives on the vehicle entity**, never on the player. Read the vehicle to know
157
+ who sits where.
158
+
159
+ ## 6. Controls, doors and the specialty block
160
+
161
+ - A package **without `specialty`** gets the platform default controls, which are Drive and Exit
162
+ only. Its light, horn, wiper, ignition and door keys do nothing, and **its declared doors and
163
+ lids can never open**.
164
+ - Opening a panel requires `specialty.componentActions.actions[<panel signal>]`. Panels come from
165
+ `mesh.articulation.panels[]`:
166
+
167
+ ```
168
+ { id, role: door|bonnet|boot, signal, nodes[], hinge{pivot, axis, closedDeg, openDeg},
169
+ collider{kind:'visual-bounds-box', minHalfExtentM}, dynamics{detents[], latchFraction, latchAngularSpeedRadS},
170
+ fidelity{physical[], detachable[]} }
171
+ ```
172
+
173
+ The pivot is in the model frame, and the axis is unit length to within ±1e-3. **A node may
174
+ belong to only one panel.** Doors use axis Y, so a left door opens to about −62°. Lids use
175
+ axis X. Keep `mesh.parts` consistent with `panels[].nodes`, or two code paths disagree about
176
+ which nodes swing.
177
+ - **Articulate every panel the real car opens**: both doors, the bonnet or hood (or front
178
+ clamshell), the boot, trunk or tailgate, and the engine cover or rear clamshell (those last
179
+ three all declare `role: "boot"`). Each is its own node split from the body shell, with the
180
+ **node origin on the hinge line**, then declared with `role`, axis `[1, 0, 0]`, a `pivot` on
181
+ that line and `openDeg` of about 50 to 60 in the sign that lifts the free edge, plus the `hood`
182
+ or `trunk` action. A lid left in the body shell can never open: round 8's F40 articulated only
183
+ its doors and the owner's bonnet and boot keys did nothing. `validate_vehicle` WARNS
184
+ (`PANEL_ROLE_MISSING_DOOR`, `PANEL_ROLE_NO_NODE_BONNET` / `_BOOT` when the model has no separate
185
+ lid, `PANEL_ROLE_UNDECLARED_BONNET` / `_BOOT` when it has one nobody articulates), each naming the
186
+ missing role with hinge guidance. It stays a warning because a model that lacks lid geometry is a
187
+ lesser car, not an invalid one; fix it by splitting the lid. The preview sweeps every declared
188
+ panel to its limit, so a declared lid is exercised and an undeclared one is not.
189
+ - When the specialty carries a `componentBuild`, its `articulators` rows drive the panels instead
190
+ of `mesh.articulation`. Keep the two consistent: the gate reads the declared panels, and the car
191
+ moves by the rows.
192
+ - **The practical route** is to reuse the published `helix-convertible` control bundle, as the
193
+ 993, Subaru and Gavril do. Copy a reference package's whole `specialty` block, then rename its
194
+ node and seat references to yours. The reference is
195
+ `read_skill({ name: "helix-vehicles", reference: "reference-package" })`.
196
+ - `specialty.componentActions` must **exactly equal** the protocol compiled from
197
+ `componentBuild` (its interacts and signal bit spans); the backend refuses any difference. The
198
+ safe edit is: keep every signal and action, and only rename row `slot`s and articulator nodes to
199
+ yours. An action your car lacks (fog with no fog lamp) then lights nothing, which is harmless. If
200
+ you do remove rows or signals, paste the `componentActions` and digest that `validate_vehicle`
201
+ prints.
202
+ - `componentBuild.digest` must equal the digest of its rows and signals. It is fnv1a64 over sorted
203
+ `stable` JSON, and the algorithm is in the reference. Any edit to a row changes it. A wrong
204
+ digest is refused.
205
+ - Hand-authoring a new control bundle requires a registry bundle and its sha256. Ids beginning
206
+ `helix-vehicle-controls-*` are reserved.
207
+ - Platform keys, which work only when the specialty declares the matching call:
208
+
209
+ | Key | Action |
210
+ | --- | --- |
211
+ | L | headlights |
212
+ | K | fog |
213
+ | `,` | indicator left |
214
+ | `.` | indicator right |
215
+ | `/` | hazards |
216
+ | H | horn |
217
+ | J | wipers |
218
+ | U | cabin light |
219
+ | Y | ignition |
220
+ | O, P, `;`, 0 | doors |
221
+ | `-` | bonnet |
222
+ | `=` | boot |
223
+
224
+ ## 7. Materials, paint and Palette
225
+
226
+ - Ship **baked PBR in the GLB**. The platform vehicle path does not resolve Palette references at
227
+ runtime.
228
+ - Use Palette families as the authoring vocabulary, especially for the BeamNG importer, which
229
+ requires `--input palette=`. Stamp each material with
230
+ `extras.helixVehicleMaterialSemantic = { contract: "helix.vehicle-material-semantic/1", familyId }`.
231
+
232
+ | familyId | Casts shadows |
233
+ | --- | --- |
234
+ | `vehicle-paint-solid`, `vehicle-paint-metallic`, `vehicle-paint-pearl`, `vehicle-paint-matte` | yes |
235
+ | `carbon-fiber`, `tire-rubber`, `aluminum-brushed` | yes |
236
+ | `vehicle-glass`, `headlamp-lens`, `taillamp-lens` | no |
237
+ | `chrome`, `stainless-steel`, `brake-rotor`, `brake-caliper-painted` | no |
238
+ | `vehicle-interior-plastic`, `vehicle-leather`, `vehicle-carpet`, `dashboard-soft-touch`, `vehicle-upholstery-fabric`, `vehicle-instrument-display` | no |
239
+ | `underbody-coating` | no |
240
+
241
+ **Do not collapse everything to one material.** If you do, every mesh reports one family, and a
242
+ one-piece car reads as "interior" and vanishes at distance. Draws are counted per primitive, so
243
+ more materials cost nothing extra.
244
+ - **Paint.** Give the body paint material `extras.helixVehicleRole: "paint"`. Owner paint
245
+ overrides fan out to it, and add-ons that carry paint inherit the host's colour, roughness and
246
+ metalness from it. An add-on with a paint role on a host that has no paint role throws.
247
+ `baseColorFactor` is **linear**: an sRGB paint value written straight in reads pale (a red reads
248
+ salmon). Paint targets (metallic and roughness for solid, metallic and matte paint) and how a
249
+ single layer reads as clear-coated are in
250
+ `read_skill({ name: "helix-vehicles", reference: "appearance" })`.
251
+ - Do not leave glTF's default metallic 1 on tyres or rims. They render black without a rich
252
+ environment. Use tyres at metallic 0 and roughness ~0.6, and rims at metallic ≤ 0.6 with the
253
+ albedo lifted to 0.55–0.6.
254
+ - **Textures.**
255
+ - Use KTX2 with a full mip chain. Mipless KTX2 is refused for every kind.
256
+ - Declare `KHR_texture_basisu` in both `extensionsUsed` and `extensionsRequired`.
257
+ - Encode data maps (metallicRoughness, occlusion) as **linear**. An sRGB-tagged roughness atlas
258
+ sampled 0.20 as 0.033.
259
+ - Keep colour maps sRGB.
260
+ - Every primitive whose material binds a texture must have `TEXCOORD_0`, or it samples texel
261
+ (0,0) of whatever atlas it binds.
262
+ - **Never bind a normal map as base colour.** The guard runs for add-ons, wearables and avatars,
263
+ but not for the vehicle host. Look at the render.
264
+ - These canonical vehicle material slots are the only legal add-on `patch` targets:
265
+ `paint_primary`, `paint_secondary`, `trim`, `chrome`, `grille`, `glass`, `light_lens`,
266
+ `wheel_rim`, `wheel_tyre`, `caliper`, `interior_primary`, `interior_seats`, `engine_bay` and
267
+ `undercarriage`.
268
+
269
+ ## 8. Budgets
270
+
271
+ | Limit | Vehicle | Add-on | Where it is enforced |
272
+ | --- | --- | --- | --- |
273
+ | Draw calls (one per glTF primitive per placing node) | **24** wall. Your floor is your node count (about 22 for a coupé); the platform's 12 "target" is rarely reachable for a full car | 8 | Upload door and Continuum seal. At seal they are **summed over every GLB in the closure**. |
274
+ | Triangles | **90,000**, summed over the closure | 20,000 | Upload and seal |
275
+ | Materials | 48 per GLB | 4 | Upload and seal |
276
+ | Texture edge | 2048 px, never bypassed | 2048 px | Upload and seal |
277
+ | File size | 16 MiB | 6 MiB | Upload |
278
+ | Skin or animation | refused: a vehicle is a static mesh | warn | Upload |
279
+ | Textures | KTX2 recommended | **KTX2 with mips required** for new add-ons | Upload |
280
+ | Composed car | — | ≤ 12 realised add-on draw calls | Compose/save |
281
+
282
+ - **Ship one drawn GLB** in the closure. Lower quality tiers belong in separate Packages, or must be
283
+ tagged with unique tier runtime-profile ids so that the seal judges them as alternatives.
284
+ Packages created from 2026-09-13 onwards get no legacy grace.
285
+ - A realistic floor for a sports coupé is 22. The 993 used 22 nodes: body, glass, exhaust, 2 doors,
286
+ 2 door glasses, 2 lids, steering, 4 wheels and 8 lamps, with 84,548 triangles under three
287
+ 2048² KTX2 atlas pages.
288
+ - Do not plan budgets around runtime batching. The seal counts per primitive.
289
+
290
+ ## 9. The vehicle package (`helix.vehicle-package/2`)
291
+
292
+ ```
293
+ { schemaVersion: "helix.vehicle-package/2", packageVersion: 2, id, displayName, make?, model?, years, market?,
294
+ provenance: { distributable: true, note }, citations: [{ id, kind, ... }],
295
+ mesh: { source: "universal-item", ref: "$self", classId, wheelVisuals, parts[], sockets?, visual?, articulation? },
296
+ physics: {...}, simulation?: { format: 1, fixedStepHz, qualityInvariant: true, land: {...} },
297
+ targets: {...}, audio: {...}, lights: {...}, vfx: [], specialty?: {...} }
298
+ ```
299
+
300
+ - **Every number in `physics` and `targets` is `Sourced`:** `{ value, unit, sources: [citationIds] }`
301
+ or `{ value, unit, derivedFrom: "<the arithmetic>" }`, never neither. A cited id must exist in
302
+ `citations`. Citation kinds are `manufacturer`, `instrumented-test`, `reference-database`,
303
+ `measured-fixture` and `engineering-literature`. Label assumptions in `note`. Prevents: invented
304
+ physics that nobody can audit.
305
+ - **The publish gate** requires `provenance.distributable: true`, a `mesh.source` that is not
306
+ `fixture`, and **`mesh.wheelVisuals` declared**. Use `"author-static"` when your GLB carries
307
+ real wheels; the runtime adopts them when all four resolve. The backend also requires `id`,
308
+ `displayName`, `years`, `mesh.classId` and `mesh.parts[]`. `classId` is one of `sedan`,
309
+ `supercar`, `pickup`, `suv`, `van`, `truck`, `bus`, `cruiser`, `kart` or `lightplane`; unknown
310
+ ids fall back to `sedan`. It decides the preset seat rows (`supercar` and `kart` have 1, `sedan`
311
+ has 2) and the plausible length range.
312
+ Each part is `{ id, node, role: body|door|bonnet|boot|glass|lamp|mirror|wing|interior|other,
313
+ consumed }`. Wheels are never parts.
314
+ - **Validator errors stop the car from spawning, and publish does not catch them.** The rules:
315
+ - the torque curve has at least 2 points in strictly ascending rpm, and should reach the limiter
316
+ - `limiterRpm > idleRpm`
317
+ - `reverseRatio < 0`
318
+ - at least one forward gear
319
+ - `targets.*.tolerance` is in (0, 1)
320
+ - induction numbers are positive, with the wastegate ≤ maxBoost
321
+ - no boost is double-counted: if the curve already contains it, set `torqueCurveIncludesBoost`
322
+ - audio and lamp rules (section 10)
323
+ - articulation hinge axes are unit length
324
+
325
+ **Run `validate_vehicle` (the `helix vehicle validate` command) before every publish** (section 12).
326
+ - **v1 vs v2.** With `simulation.land`, the car runs landV2 in every world. The platform **never
327
+ derives** `simulation.land` for you. Without it, the car runs v1: a lumped engine, an always-on
328
+ automatic gearbox and no manual mode. Brakes, suspension, steering, aero, mass and centre of mass
329
+ always come from `physics`.
330
+ - **Fields.** All are SI units: m, kg, N·m, N/m, s.
331
+
332
+ | Block | Fields |
333
+ | --- | --- |
334
+ | `chassis` | `mass`, `weightDistributionFront`, `centreOfMassHeight` (above ground), `groundToBodyOrigin`, `wheelbase`, `trackFront`, `trackRear`, `length`, `width`, `height`, `wheelRadius`, `tyreSize`, `inertiaMultiplier` |
335
+ | `engine` | `torqueCurve` (`[rpm, N·m]` at the crank), `idleRpm`, `limiterRpm`, peak power and torque with their rpm, `drivetrainEfficiency`, `coastTorque`, `cylinders`, `strokes`, `speedLimiterKph` (informational; not enforced) |
336
+ | `induction?` | turbo or supercharger fields |
337
+ | `gearbox` | `ratios`, `reverseRatio`, `finalDrive`, `shiftTime`, `autoUpRpm`, `autoDownRpm` |
338
+ | `driveBiasFront` | nominal front share of drive torque at zero slip: 0 = RWD, 1 = FWD, AWD strictly between and equal to `centre.torqueSplit` |
339
+ | `brakes` | `maxTorque` (whole car), `biasFront`, `handbrakeTorque`, `handbrakeAxle` |
340
+ | `aero` | real `dragCoefficient`, `frontalArea`, `airDensity`, `downforceCoefficient` |
341
+ | `grip` | `peak`, `sliding`, `lateralMultiplier`, `biasFront`, `loadSensitivityExponent` |
342
+ | `suspension` | `rideHzFront`, `rideHzRear`, `dampingRatio`, `bumpScale`, `reboundScale`, `staticRideHeight`, `bumpTravel`, `antiRollBarFront`, `antiRollBarRear` |
343
+ | `steering` | `roadWheelLockDeg`, `rackTime` |
344
+ | top level | `linearDamping` **= 0 when aero is modelled** (0.02 removed 65 km/h of top speed), `angularDamping` |
345
+
346
+ In `simulation.land`, the driveline holds the engine LUTs and inertia, the clutch (capacity
347
+ ≈ 1.6 × peak torque), the gearbox kind (`manual` or `sequential` enables manual mode), shift
348
+ delay and auto rpms. Differentials (`front`, `rear`, `centre`) each take a type, a torque split
349
+ and a `gearRatio`; **the final drive lives on the diff in v2**. It also holds the optional
350
+ turbine, `abs`, optional `tractionControl` (engine 0.3.204+; see below), and `tyreByAxle[]` with `muLut` and **a `radius` per axle**: staggered tyres
351
+ are modelled there, with `physics.chassis.wheelRadius` at the driven axle's radius (it sets the
352
+ rolling radius and gearing; the physics contacts use it at all four corners, so the other axle's
353
+ contact sits off its drawn tyre by the radius difference). Visual QA judges spin per axle. Derivation is in
354
+ `read_skill({ name: "helix-vehicles", reference: "physics-from-specs" })`.
355
+ - **Keyboard launch fails (wheelspin, wall hit, no clean 0-100)?** Author
356
+ `simulation.land.tractionControl` (opt-in; absent means none), e.g. `{ targetSlip: 0.2, cutRate: 25,
357
+ restoreRate: 3, minTorque: 0.45, upshiftTorqueRamp: true }`. Declare it as the real system or a
358
+ playability aid, then re-run `simulate_vehicle`; the report's `traction control:` line shows it.
359
+ When and how to tune: physics-from-specs, "Drivable on a keyboard" and "Traction control".
360
+ - **`groundToBodyOrigin` must equal `wheelRadius + staticRideHeight`**, and it is **locked forever
361
+ after the first publish**. Your first mint fixes it, so measure ride height with
362
+ `preview_vehicle` before that mint. If a later version needs a different stance, keep the
363
+ equality by changing `staticRideHeight` (= locked value − `wheelRadius`). Never change the locked
364
+ field: the 993's version 1.0.2 did, and it was sealed but unusable.
365
+ - **Targets.** These are the published behaviour with tolerances:
366
+
367
+ | Target | Notes |
368
+ | --- | --- |
369
+ | `zeroToSixtyMphS` | |
370
+ | `zeroToHundredKphS` | |
371
+ | `quarterMileS` | |
372
+ | `topSpeedKph` | the ungoverned figure (the platform car runs ungoverned) |
373
+ | `ungovernedTopSpeedKph` | |
374
+ | `lateralG` | |
375
+ | `brakingSixtyToZeroM` | |
376
+
377
+ Only `grip.peak` and `suspension.*` are ours to tune toward the targets. Everything else is the
378
+ real car's. The engine is the maker's **declared crank figure** (both torque curves, `peakTorqueNm`,
379
+ `peakPowerKw`): losses go in `drivetrainEfficiency` and drag, never an inflated curve
380
+ (`TORQUE_CURVE_EXCEEDS_DECLARED_CRANK_FIGURES`). An AWD car's split lives on `simulation.land.driveline.centre`
381
+ (`physics-from-specs`, "All-wheel drive").
382
+ - Declare `chassis.length` and `chassis.width`. The spawn footprint search uses them to find a
383
+ pose clear of kerbs and walls; the default is 1.9 × 4.3 m.
384
+ - The chassis collider is one full-length box with **0.16 m** kerb clearance.
385
+
386
+ ## 10. Audio
387
+
388
+ - **Format.** WAV (`audio/wav`) only. Each runtime asset is
389
+ `{ name, url, checksumSha256, bytes, mediaType: "audio/wav" }`, where `url` is the public
390
+ content URL of that WAV as an object of the vehicle's own Continuum Package.
391
+ - **Author rows with only `name` and `file`** (the WAV beside the package or in `audio/`).
392
+ `publish_vehicle` uploads each file, verifies its public URL serves the exact bytes, and writes
393
+ `url`, `checksumSha256`, `bytes` and `cid` before the mint. Never hand-write a URL.
394
+ - **A `cid`-only row fails closed and the car is silent.** So does a 404, a re-encoded file or
395
+ an MP3. `check_vehicle_audio` catches all of these, decodes every sample the runtime selects,
396
+ and refuses digital silence. Run it before publishing and on the published item id.
397
+ - MP3, OGG and data URIs are not played.
398
+ - **The hard rule: vehicle audio is sourced, never synthesised.** Never generate or synthesise a
399
+ vehicle sound: no resynthesis, no `generate_audio`, not even for a slot nothing provides.
400
+ Always use real recorded sound, from BeamNG mods as much as possible. Whatever the mesh source,
401
+ find the most accurate simulator mod of the exact car (BeamNG first; Assetto Corsa / ACC,
402
+ rFactor 2, Automobilista 2 where theirs is more accurate) and extract its audio with
403
+ `import_vehicle_audio` (`helix vehicle audio-import`). Every slot and bank carries a
404
+ `provenance` `{ kind: "sim-mod" | "sim-official" | "recording", sim, mod, modUrl?, sourceFile,
405
+ sourceSha256, event?, extractor }`. `check_vehicle_audio` reports it and **fails** audio that is
406
+ synthesised or generated (`AUDIO_SYNTHESISED`), a car whose engine audio is unsourced
407
+ (`AUDIO_ENGINE_UNSOURCED`), and **any empty slot or slot that would fall back to a platform
408
+ default** (`AUDIO_SLOT_UNSOURCED`); `publish_vehicle` refuses the same. **A car ships a sourced
409
+ recording for every one of the 31 slots below** (`engineLadder`, `exhaustLadder`, `idle`,
410
+ `startup`, `shutdown`, `shiftUp`, `shiftDown`, `clutch`, `transmissionWhine`, `turboSpool`,
411
+ `blowOff`, `intake`, `revLimiter`, `backfire`, `suspension`, `impactLight`, `impactMedium`,
412
+ `impactHeavy`, `tyreSqueal`, `tyreScrub`, `tyreSpin`, `tyreRollSlow`, `tyreRollFast`,
413
+ `indicatorTick`, `skid`, `brakeSqueal`, `handbrake`, `reverseBeep`, `horn`, `doorOpen`,
414
+ `doorClose`); the audio reference has the slot-by-slot sourcing table.
415
+ - **Engine sound** is a bank:
416
+ - `audio.bank.assets[]` holds the WAVs, each with role `vehicle.audio.bank.sample`.
417
+ - `audio.bank.blends.<NAME>` is `{ eventName, samples: [[[file, rpm], …], …] }`: rpm-ordered
418
+ ladders crossfaded by load.
419
+ - `engineBlend` and `exhaustBlend` name the blends.
420
+ - `sampleCylinderCount`, `strokes` and `crankLayout` (`"flat-plane"` | `"cross-plane"`,
421
+ default `"flat-plane"`) describe the *recorded* engine and its crank. `crankLayout` selects
422
+ the firing fundamental `check_vehicle_audio` judges each rung's pitch against: `C·rpm/120`
423
+ even-firing (flat-plane, and every inline/boxer engine), or `rpm/60` for a cross-plane V8.
424
+ A rung an octave or more off its rpm label is refused.
425
+ - The mix knobs are `engineGainDb`, `exhaustGainDb`, `minLoadMix`, `maxLoadMix` and `muffling`.
426
+ - Every sample a selected blend names must be in `bank.assets`.
427
+ - The 993 used 11 recorded rungs from 1,768 to 6,388 rpm.
428
+ - **Slots.** Every slot is declared as
429
+ `{ slot, buffers[], assets?[], consumed, reason?, absent? }`. An unconsumed slot **requires a
430
+ `reason`** and still carries its sourced recording. **The only empty slots** are
431
+ `consumed: false, absent: "not-fitted"` with a reason, for a component the real car physically
432
+ lacks (turbo slots without a turbo, `reverseBeep`, `clutch` on an automatic or CVT, combustion
433
+ slots on an EV, whose engine ladder is a recording of the real motor), and
434
+ `absent: "combined-with-engine"` for `exhaustLadder` alone, beside a sourced engine ladder, when
435
+ the mod records one combined set. Anything else empty is `AUDIO_SLOT_UNSOURCED`; a `not-fitted`
436
+ claim the physics contradicts is `AUDIO_SLOT_FITMENT_MISMATCH`. Slots played on the platform:
437
+
438
+ | Slot | Runtime trigger |
439
+ | --- | --- |
440
+ | `startup`, `shutdown` | ignition on/off |
441
+ | `shiftUp`, `shiftDown` | gear change |
442
+ | `clutch` | pedal click |
443
+ | `revLimiter` | repeats at the limiter |
444
+ | `turboSpool` | loop on shaft rpm |
445
+ | `blowOff` | boost dump edge |
446
+ | `backfire` | on-load → overrun edge |
447
+ | `suspension` | suspension events |
448
+ | `impactLight` / `Medium` / `Heavy` | collisions |
449
+ | `tyreSqueal`, `tyreScrub` | slip loop |
450
+ | `tyreSpin` | wheelspin loop |
451
+ | `tyreRollSlow` / `Fast` | speed crossfade |
452
+ | `skid` | skid onset |
453
+ | `brakeSqueal` | light braking at walking pace |
454
+ | `handbrake` | pull |
455
+ | `indicatorTick` | relay, twice per blink |
456
+ | `horn` | needs a specialty horn action |
457
+ | `engineLadder` / `exhaustLadder` | through the bank |
458
+
459
+ **Carried unconsumed:** `idle` (the runtime idles on the bottom rungs), `transmissionWhine` (played
460
+ only with a declared native pitch) and `intake` (no intake layer) still carry the car's own
461
+ sourced recording with `consumed: false` and the reason. **Doors are required**: `doorOpen` and
462
+ `doorClose` each carry a sourced recording (the engine lane is wiring the door edge). There is no
463
+ bonnet, boot or engine-cover slot yet, so a panel sound is not supported. The runtime's own default
464
+ reverse beep is a platform default and does not satisfy `reverseBeep`: source one, or declare it
465
+ `not-fitted`. Never author a stand-in.
466
+ - **Find the most accurate sim mod, by measurement.** Compare candidate mods against real
467
+ recordings of the car (firing-fundamental pitch track vs rpm, spectral centroid, roughness,
468
+ idle character, limiter cadence) and take the one that matches; the commands are in
469
+ `read_skill({ name: "helix-vehicles", reference: "audio" })`.
470
+ - **A slot the best mod lacks** is filled, in order, from another BeamNG mod of the same engine or
471
+ car family (then the BeamNG base game's stock banks and `vehicles/common` sounds), another
472
+ simulator's mod, a real recording of the car. Never another car's sound under this car's label,
473
+ and never a stand-in; a car with an unsourced slot is not publishable. Report it as blocked and
474
+ say what you tried.
475
+ - **Platform defaults are never an answer.** With no `audio.bank` the runtime plays a synthesised
476
+ oscillator engine (bank `inline4` and the other generic banks in `engine-core`), and for any
477
+ slot a car leaves out it plays its own generated default sound pack, synthetic starter and
478
+ runtime noise for tyres, skids and the handbrake. None of that is a sound for a published car.
479
+ Do not rely on any of it and do not omit a bank or a slot to get it: the gate fails a car that
480
+ would fall back to one (`AUDIO_ENGINE_UNSOURCED`, `AUDIO_SLOT_UNSOURCED`).
481
+ - **A world with no `@helix/engine-core/audio` import-map entry plays no car audio at all.**
482
+ Test sound in a platform-hosted world that has it.
483
+
484
+ ## 11. Add-ons
485
+
486
+ - **A car ships with a parts set.** At least a wheel set, an exhaust, an ECU, a gearbox,
487
+ suspension, brakes and anti-roll bars, plus a spoiler where the real car has one. The car is not
488
+ done until its customizer shows them as compatible and for sale (`check_vehicle_host`). The list
489
+ and the method are in `read_skill({ name: "helix-vehicles", reference: "addons" })`.
490
+ - An add-on is an `add_on` item. Required fields:
491
+ - `fits = { slotType, requires{ sockets[], paths[], pointers[], nodes[], skeleton: null }, only{ items: [<host item ids>] } }`
492
+ - `ops[]`, at most 32
493
+ - a Marketplace route or `noMarketplace`
494
+ - **`fits.only.items` must name at least one host.** Universal add-ons are refused. Every op target
495
+ must also appear in `requires`.
496
+ - **Op verbs.**
497
+ - `tune`: `{ v:"tune", path, op: "mul"|"add", value }`. Numeric paths take only `mul` or `add`;
498
+ `set` on a number is first-party only. Categorical paths, such as `drivetrain.forcedInduction.kind`,
499
+ take only `set`. A `mul`
500
+ on a value that is 0 does nothing, so use `add`. A `mul` on a block the host lacks is refused,
501
+ so a turbo on an NA car cannot ship; there is no `provides` verb for creators.
502
+ - `hide`: `{ v:"hide", nodes:[glob] }`
503
+ - `mount`: `{ v:"mount", row }`
504
+ - `patch`: `{ v:"patch", at:"/materials/<slot>/…" }`
505
+ - **Main slot types.**
506
+
507
+ | Slot type | Mount socket |
508
+ | --- | --- |
509
+ | `vehicle.wheel@1` | fills all 4 of `wheel_*`; the 993 pattern hides `rim_*` |
510
+ | `vehicle.tyre@1` | — |
511
+ | `vehicle.spoiler@1` | `boot` |
512
+ | `vehicle.bodykit@1`, `bumper_f@1`, `bumper_r@1` | `bodyshell` |
513
+ | `vehicle.exhaust@1` | `exhaust` |
514
+ | `vehicle.engine@1` | `engine` / `bonnet` |
515
+ | `vehicle.ecu@1`, `transmission@1`, `driveline@1`, `suspension@1`, `brakes@1`, `arb@1`, `steering@1` | tune-only (no model) |
516
+ | `vehicle.exhaust_note@1`, `blowoff@1` | sound only |
517
+ | `vehicle.headlight@1`, `taillight@1` | lamp sockets |
518
+ | `vehicle.seats@1` | seat sockets |
519
+ | `vehicle.steering_wheel@1` | `steering` |
520
+ | `vehicle.plate@1` | `numberplate` |
521
+
522
+ - **Anchoring is the add-on GLB's `scene.extras.helixMeshFrame`** (contract `helix.mesh-frame/1`).
523
+ - `{ authoringFrame: "socket", outboardAxis: "+x" }`: the mesh is metric and **centred on the
524
+ mount**, which is the hub for wheels. One mesh serves every corner, turned to face outboard.
525
+ It requires the host's wheel origins at the hubs (section 3).
526
+ - `{ authoringFrame: "host", bind: { socket, root, matrix } }`: the vertices are already in the
527
+ host's frame, as for wings, kits and exhausts. Without `bind`, the part stays static at the
528
+ root. **If the host is ever re-framed (raised or moved), every host-frame add-on must be
529
+ shifted and republished.**
530
+ - A socket that does not resolve is reported and not drawn.
531
+ - **A hide needs a host grant, sealed into the car.** The standard slot board grants only
532
+ `wheel_stock_*` and `exhaust_stock_*`-style globs. To let add-ons hide your real node names
533
+ (`rim_*`, `exhaust`), declare them in the car's `publish.json`:
534
+ `"hostGrantExtensions": { "whl.wheel": { "hide": ["rim_*"] }, "pwr.exhaust": { "hide": ["exhaust"] } }`.
535
+ `publish_vehicle` seals them into the package root, and the backend checks each pattern against
536
+ the GLB's nodes. Without the grant, a wheel add-on whose `requires.nodes` lists `rim_*` fits zero
537
+ hosts; `check_vehicle_addon` reports it with the backend's own fit count, and
538
+ `publish_vehicle_addon` stops (`HOST_GRANT_MISSING`). The board (sockets, nodes, tunable paths,
539
+ grants) is derived **once, at mint**, from the GLB's `asset.extras.HELIX_vehicle`, the package's
540
+ physics and those grants (section 12). A version move does not rebuild it, so plan the add-on set
541
+ and its grants before the first mint.
542
+ - **A replacing part must hide what it replaces.** An exhaust add-on hides `exhaust`. Hide
543
+ targets must exist on the host.
544
+ - **Tune-only parts ship no model.** An ECU, gearbox, driveline, coilover kit, sway-bar set, brake
545
+ kit or steering rack draws nothing, so its directory holds only `addon.json`, with no
546
+ `model.glb`. Engine, intake, induction, exhaust and tyre-compound parts may also be tune-only.
547
+ The backend admits a meshless part in those slots when **every op is a `tune`**, with at least
548
+ one. A `hide`, `mount` or `patch` op needs a model. The server pictures the part with a slot
549
+ card. Never ship a placeholder box into a tune slot: a model saved into one once made the whole
550
+ car refuse to spawn.
551
+ - **Every required path must be on the host.** A part fits only if each path in
552
+ `fits.requires.paths` is in the host board's `surface.paths`. Cars published since 2026-10-03
553
+ expose every path their vehicle package resolves. Older cars may expose none, so a performance
554
+ part fits zero hosts. `check_vehicle_addon` reports this as `HOST_PATHS_MISSING`.
555
+ - **Prove the part does something before you publish it.** `check_vehicle_addon` measures every
556
+ `tune` op on each host with the live runtime's own physics and add-on fold. It judges each op on
557
+ a figure that op can move:
558
+
559
+ | Ops on | Judged on |
560
+ | --- | --- |
561
+ | engine, gearbox, induction, mass, aero drag | 0-100 km/h, 0-60 mph, quarter mile, top speed |
562
+ | `drivetrain.brakes.maxTorque` / `biasFront` | 60-0 mph at 30% pedal (torque-limited) and at full pedal (ABS, tyre-limited) |
563
+ | `drivetrain.brakes.handbrake*` | handbrake-only deceleration |
564
+ | `land.antiRollBar.*` | skidpad body roll (anti-roll bars barely move steady-state grip) |
565
+ | suspension, tyre grip, downforce | skidpad g and roll, braking, acceleration |
566
+
567
+ A part that moves nothing it governs is an error (`ADDON_DRIVE_NO_EFFECT`). An op that undoes
568
+ the rest of its part is a warning (`ADDON_DRIVE_OP_CANCELS`). For example, the 993's first brake
569
+ kit added +0.079 front bias. On its own that lengthened the full-pedal stop by 6%, which
570
+ cancelled the kit's 28% extra brake torque. Raising only `limiterRpm` gains little when the
571
+ torque curve ends at the old limiter, because the engine holds the last point flat. Give an ECU
572
+ a `torqueCurve` op as well.
573
+ - **Performance on landV2.** Engine torque curve, limiter, idle, coast, efficiency, gear ratios,
574
+ reverse, shift time, final drive (lands on the diffs), drive bias (centre split, AWD only) and
575
+ turbo boost/wastegate/spool are projected into landV2. Brakes, suspension, anti-roll, mass, aero
576
+ and steering act directly.
577
+
578
+ These are **inert on landV2**: `densityLapse`, induction kind, `referenceRpm`, `gamma`,
579
+ `spoolDownTime`, turbo ops on a car with no `driveline.turbine`, and probably tyre-grip ops.
580
+ The live truth is `controller.landDriveline.spec`; `controller.tuning.drivetrain` is not proof.
581
+ - **Gearing.** Judge a gearing change on the whole run, including shift count and shift time.
582
+ Shorter ratios made a 993 *slower* to 100 km/h because they added two 0.25 s shifts.
583
+ - **Wheel sets.** Use the same radius as the host's tyre: refit radially, or tune
584
+ `contacts.*.radius`. Give them the material fixes from section 7.
585
+ - The upload refuses add-on textures that are not KTX2 with mips. A primitive with a texture must
586
+ have UVs. **Never synthesise a new UV unwrap onto a borrowed atlas.**
587
+ - `publish_item` **does not auto-seal vehicle add-ons** (only character add-ons). Publish them
588
+ with `publish_vehicle_addon`, which seals and pins the `addon` Package (section 12).
589
+
590
+ ## 12. Publish, pin, seal, list
591
+
592
+ 1. **Host manifest.** The final GLB carries **`asset.extras.HELIX_vehicle`**:
593
+ `{ version: 1, class, sockets: { <socket id>: [x, y, z], … } }` in the model frame, with every
594
+ seat, the four wheel hubs, `steering`, `engine`, `exhaust`, the lamp sockets and `boot` /
595
+ `bonnet`. The backend reads **only `asset.extras`** (scene or root `extras` are ignored at
596
+ upload, although the runtime reads them), and derives the car's slot board from it **once, at
597
+ mint**: host kind, sockets, node names, and the tunable paths the package's physics resolves.
598
+ Without it the car mints as a silent non-host whose customizer shows "0 compatible", and a
599
+ later version does not fix it. Export snippets:
600
+ `read_skill({ name: "helix-vehicles", reference: "host-manifest" })`.
601
+ 2. **Validate and simulate.** `validate_vehicle({ vehiclePackage, glb })` must exit 0; it refuses a
602
+ vehicle GLB with no host manifest. `simulate_vehicle` must meet every target within tolerance.
603
+ Audio rows backed by a local WAV are judged as if uploaded.
604
+ 3. **Preview, then fix the stance, the cabin and the look.** `preview_vehicle({ target: <dir> })`
605
+ renders the unpublished directory in the real runtime, prints ride height,
606
+ wheel-to-hub, arch and cabin numbers, and writes the fixed appearance poses to compare with
607
+ reference photos. The first mint locks `groundToBodyOrigin` and the slot board, so this is the
608
+ last cheap fix.
609
+ 4. **Publish.** `publish_vehicle({ dir, dryRun: true })`, then without `dryRun`. One command
610
+ uploads the WAVs and writes their URLs, seals the `vehicle` Package (the package as root, the
611
+ model, every WAV), makes the item pinned to it (the only door that writes the checksum-pinned
612
+ `properties.vehiclePackage`), and reads it all back, the slot board included. The seal
613
+ re-checks the budgets over the closure. It is resumable: after any failure, fix the cause and
614
+ re-run the same call. The descriptor it sealed is in `<dir>/.helix-publish/`. `publish.json`
615
+ `provenance.kind` is `authored`, `imported` or `generated`. **If it answers NOT AVAILABLE**
616
+ (the Package-first cutover), the bundled CLI has no vehicle mint: finish every gate and report
617
+ the mint as blocked; never improvise it.
618
+ 5. **Host check.** `check_vehicle_host({ itemId })`: host kind `vehicle`, non-zero sockets, nodes
619
+ and paths.
620
+ 6. **Visual QA.** Try Now, the customizer and visual QA draw the package's `mesh.visual` derived
621
+ rendition, which `publish_vehicle` pins to the GLB it uploaded (`artifactRef` = its absolute
622
+ `https` content URL; `mesh.ref` bound to the item). Visual QA
623
+ (`visual_qa_item_hosted`, or `visual_qa_item` with a QA sign-in) must pass before any Marketplace
624
+ listing, distribution or version move (`VISUAL_QA_REQUIRED`).
625
+ 7. **Distribute.** Call `create_item_distribution` with a price. The server enforces a minimum
626
+ price, which `quote` reports; the 993's parts sold at 250 LIX and up.
627
+ 8. **Add-ons** (required, section 11). For each, an `addon.json` (`title`, `fits`, `ops`) and
628
+ `model.glb` in a directory:
629
+ 1. `check_vehicle_addon({ dir })`: contract, anchoring, host hubs, hide grants, the
630
+ backend's fit dry run and, for a part with no model, the web customizer's meshless guard on
631
+ the exact definition publish would seal (`ADDON_CUSTOMIZER_REFUSES`). It must say it fits
632
+ every host.
633
+ 2. `publish_vehicle_addon({ dir })`: the full check, then it uploads the model (none for a
634
+ tune-only part), seals the `addon` Package, makes the item from it, and reads back that
635
+ each host lists it.
636
+ 3. `visual_qa_item_hosted({ itemId: <add-on> })` renders it fitted on every host. Look at the
637
+ wheel close-ups.
638
+ 4. Distribute, then `check_vehicle_host` on the car: every required part fits and is for sale,
639
+ and every part passes the web customizer's own guard (it names any part the customizer
640
+ would refuse to equip, with the customizer's message).
641
+ 9. **Changing a minted car: always a new version of the same item.** Never re-run a mint, never
642
+ mint a second item, and never use `publish_item` for a car that exists.
643
+ - The new version is POSTed with its pin and `vehiclePackage` **together**, plus a `reason`
644
+ of 1–500 characters. Moving one without the other draws stale bytes.
645
+ - These fields must stay identical: `id`, `mesh.source`, `mesh.ref`, `mesh.classId`,
646
+ `mesh.wheelVisuals`, specialty module id, bundle id and `driverSeat`, `groundToBodyOrigin`,
647
+ and whether `simulation.land` is present. **A locked `Sourced` field is compared as a whole
648
+ object, including `note`, `sources` and `derivedFrom`**: copy it byte for byte from the
649
+ published package. Editing only the note of `groundToBodyOrigin` was refused with a 409.
650
+ - These may only grow: parts ids, socket keys, renditions, panels, seat vars, actions and
651
+ signals.
652
+ - Versions cannot be deleted. Use the next patch version.
653
+ - `publish_vehicle({ dir, itemId, version, reason })` seals the version staged into the car's
654
+ own Package (a changed GLB is uploaded and pinned) and moves the item. On a listed item the
655
+ move first needs a pass from `visual_qa_item({ packageVersion, vehiclePackage })`; the tool
656
+ says so, prints the commands, and the same call resumes at the move.
657
+ - **Activate a car version only by that resume** (`helix vehicle publish <dir> --item <id>
658
+ --package-version <v> --reason ...`): it moves the pin and `properties.vehiclePackage`
659
+ together. **Never `helix item set-version` for a vehicle**: it moves the pin only, so the car
660
+ keeps serving its old package (until the CLI says otherwise).
661
+ - A version move does not rebuild the slot board: new sockets or hide grants need a fresh car.
662
+ 10. **The Package pin is what draws.** After a repair, check every surface: hosted world,
663
+ customizer, Try Now and thumbnail.
664
+
665
+ ## 13. Verification before publish (a gate, not advice)
666
+
667
+ The platform's visual-QA gate passes a vehicle only when **every required measured assert is
668
+ present and passes**: `vehicle.wheels.at_hubs` (within 3 cm), `vehicle.wheels.arch_clearance` (no
669
+ body in the tyre volume; lowest body point ≥ 5 cm), `vehicle.wheels.spin_with_speed` (0.8–1.25,
670
+ per wheel against its axle's `tyreByAxle` radius),
671
+ `vehicle.steering.wheel_rotates` (front wheels ≥ 10°, steering wheel ≥ 30°),
672
+ `vehicle.articulation.reaches_limits` (within 5° of declared travel), `vehicle.seats.addressable`,
673
+ `vehicle.occupants.penetration` (on an occupant run), `frame.non_empty`,
674
+ `scene.no_nan_or_invisible`, `render.textured` and `scale.sane_for_category` (1.2–30 m); the
675
+ occupant asserts block whenever present (engine current, boarded, head clearance, hips on seat,
676
+ feet on floor, hands on the wheel); **and** the vision critic passes every check (the item matches
677
+ its title, intact render, textures, wheels seated, ride height, symmetry, cabin,
678
+ articulation, steering, motion, and the occupant checks: head clearance, seat contact, no
679
+ clipping, hands on the wheel). The full list, with thresholds, is in
680
+ `read_skill({ name: "helix-vehicles", reference: "qa" })`; the cabin thresholds are in its
681
+ `cabin` reference.
682
+
683
+ - **Only a submitted or hosted run is a gate result.** `visual_qa_item_hosted`, or
684
+ `visual_qa_item` submitting with a QA sign-in, records the backend's verdict and runs the critic.
685
+ A local run never runs the critic, so its "PASS" proves nothing about the gate.
686
+ - **When a gate artefact blocks a correct car, report it and never distort the car.** Accuracy
687
+ wins: keep the real dimensions, ride height and tyre sizes, record the evidence (verdict, frame,
688
+ measurement), and report it as a platform defect. Round 1's F40 raised its body 36 mm to lift a
689
+ QA camera over a test wall, still failed, and ended up less accurate.
690
+ - The gate **passes vacuously on a car with no declared panels**. It **never plays audio** and
691
+ never checks physics against the real car. Those are on you.
692
+
693
+ Before submitting:
694
+
695
+ - **Preview before the first mint** (`preview_vehicle`), and **look at the renders yourself**:
696
+ the fixed poses beside reference photos at the same poses (front, side, rear, top, both 3/4
697
+ views, cockpit), every wheel close-up, inboard/x-ray, each door open, steering at lock, and every
698
+ seat occupied. Do it for the base car **and for each add-on fitted on it**. Before publishing an
699
+ add-on, `preview_vehicle({ target: <car dir>, addon: [<dir>] })` (or `{ item: <published car
700
+ id>, addon: [<dir>] }`) fits it in the real runtime and checks it lands on its anchor (every
701
+ wheel within 1 cm of its hub), not at the car centre. A platform flag
702
+ saying "parts seated" is not evidence. The 993's flags said seated while all four wheels sat at
703
+ the car centre.
704
+ - Check the car **parked and unboarded** as well as boarded. Tyres within ±1 cm of the road, and no
705
+ tyre in the arch at rest, at 0.07 m bump, at full lock, and at lock with bump.
706
+ - Drive it in a world **with kerbs**, and time 0–100 against `targets` beside a control car in the
707
+ same world. Discard any sample after a wheel leaves the ground.
708
+ - Listen: in a world with vehicle audio, check ignition, idle ladder, revs under load, shifts,
709
+ limiter and horn.
710
+
711
+ ## 14. What the runtime does not do (do not promise these)
712
+
713
+ - **Not rendered:** `lights.lamps[]`, lamp colour (corona colours are a preset), and
714
+ `speedLimiterKph`. The live car is ungoverned; only the `simulate_vehicle` instrument reads the
715
+ field, so author `null`, set `targets.topSpeedKph` to the ungoverned figure, and expect a
716
+ `SPEED_LIMITER_IS_A_MARKET_GOVERNOR` warning otherwise. No add-on can toggle a limiter.
717
+ - **Audio slots whose package sample never plays:** door, idle, intake, whine and reverse-beep
718
+ (the runtime's own reverse beep still sounds).
719
+ - **No Palette resolution at runtime.**
720
+ - **Vehicles cannot float or fly** from a package. The air and water domains only change controls.
721
+ - **Not available to creators:** a `provides` verb, so a turbo cannot be added to an NA car, and
722
+ `set` ops.
723
+ - **Physics:** tyre-grip add-ons are probably inert on landV2, and `simulation.fixedStepHz` is
724
+ validated but has no confirmed consumer.
725
+ - **Behaviours of the `multiplayer-vehicles` example world that published cars never get:** its
726
+ fixed node-name rig, its derived landV2, its Palette projection, its one-shot sound pack and its
727
+ speed governor. Never cite that template as proof a published vehicle will behave the same way.