@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.
- 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
package/docs/vehicles.md
ADDED
|
@@ -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.
|