Vehicle Assets¶
How vehicle assets are organized in Studio, by asset shape. The shape is
determined by the vehicle's AssemblySystem field in
src/Shared/VehicleDef/Vehicles/<vehicle>/init.luau (plus, for legacy shapes,
whether AssetId is set on the vehicle or on each trim).
The asset shape kinds match the VehicleAssetDef union in
src/Shared/VehicleDef/Types.luau: legacy_trim, legacy_vehicle,
assembly_v1.
For non-physical DriveSeat configuration overlays on assembly_v1 vehicles,
see Assembly Vehicle Tuning.
Asset shapes¶
legacy_trim — per-trim asset¶
AssemblySystem = "Legacy". Asset id lives onTrimDef.AssetId.- The uploaded asset's single Model child is the trim vehicle.
- One asset upload per trim.
legacy_vehicle — per-vehicle asset¶
AssemblySystem = "Legacy". Asset id lives onVehicleDef.AssetId.- The asset's single Model child is a Folder that contains one Model per
trim, each tagged with a
NumberValue/IntValuewhose value is the trim'sVehicleId. The runtime extracts the right model by matching that value. - One asset upload per vehicle, shared across trims.
assembly_v1 — per-vehicle, assembled at runtime¶
AssemblySystem = "assembly-v1". One asset upload per vehicle. Every trim of the vehicle is built by composing parts from this same asset.- Trim selection is not stored anywhere in the asset. Dealership packages
carry the
vehicle_idthey purchase, while preview/spec helpers may still resolve a trim from the finalized assembly withVehicleDef:findTrimByAssembly(vehicleKey, assembly)so derived paint/rim options can follow live assembly picks.
Asset hierarchy¶
Model "<asset-id>" -- serialized RBXM/LocalVehicles container
└── Folder "<vehicle-asset-name>" -- runtime assembly asset
├── assembly/ -- Folder
│ └── <assembly_node_id>/ -- Folder per assembly tree node
│ └── <part_id> -- Model OR Folder of variant Models (see below)
└── accessory/ -- Folder (flat — every accessory part lives here)
└── <accessory_node_id>/ -- Folder per accessory socket id
└── <part_id> -- Model (geometry + attachments)
The RBXM's root must be the outer Model, with exactly one vehicle asset
Folder inside it. VehicleModelManager clones LocalVehicles/<asset-id> and
passes its first child to the assembly builder. If the vehicle asset Folder is
serialized as the RBXM root instead, the manager passes assembly or another
content folder to the builder, producing an asset is missing 'assembly'
folder runtime error. Name the outer Model after the local pseudo-asset id or
published asset id.
Accessory part assets are flat: a part that fills a socket on another
accessory (e.g. a grille on the front-bumper) lives at the top level under
its own socket id, exactly like a part that fills a socket on an assembly part.
There are no nested accessory/ folders. What records the mount-parentage (the
grille mounts onto the bumper's socket, not the drivetrain's) is the install
nesting in the package's base_accessories, not the asset layout.
Examples for sierra_ht_2026:
assembly/frame/standard-4x2assembly/frame/electric-4x2assembly/cab/standardassembly/drivetrain/6-7assembly/vocation/stakebed-standardassembly/snow-plow/snow-plow-standardaccessory/front-bumper/standardaccessory/grille/standardaccessory/grille/standard-w-sensoraccessory/fog-lights/standardaccessory/side-mirrors/c-looptuning/frame/standard-4x2tuning/drivetrain/6-7
The folders above show the usual asset paths. Assembly/accessory/tuning
definition ids are stable authored identifiers used by package data and saved
data. Definition paths point at the asset hierarchy and are allowed to change
when the Studio asset is reorganized. In many current vehicles the id and path
are intentionally the same string, but code should treat them as separate
fields.
The optional tuning/ folder is not physical assembly. Tuning models are
partial DriveSeat overlays used to replace sounds, add VehicleDataPatch
modules, or replace VehicleData; see
Assembly Vehicle Tuning.
Part model conventions¶
Every asset part is addressed through a definition entry. The definition's
id is the stable identifier used by trims, packages, saved assembly data, and
customization assignments. The definition's path is a colon-delimited address
under the uploaded asset's assembly/ or accessory/ folder. The asset model
at that path must conform to the definition: display name, sockets, paint
surfaces, variants, and other metadata live in source, not in the RBXM path
name alone.
Assembly part shape (Models under assembly/<node>/<part>):
- MountAttachment (
Attachment): how this part mounts onto its parent. When installed, it is aligned to the matching attachment of the parent's socket folder. The root assembly part of a vehicle (e.g.framefor the Sierra HT) does not need one — it mounts at the vehicle base origin. - AssemblySocketAttachments (
FolderofAttachments): where child assembly parts mount. One attachment per socket id listed underassembly_socketsin the definition. - AccessorySocketAttachments (
FolderofAttachments): where child accessory parts mount. One attachment per socket id listed underaccessory_socketsin the definition.
Variant-shaped assembly parts. If a part needs to render differently
depending on what else is installed (e.g. a frame that takes a different
shape when paired with a tall dumpbed vocation), the asset entry at
assembly/<node>/<part> is a Folder of variant Models instead of a single
Model. Each variant Model is named with its variant id (e.g. variant-1,
variant-2) and conforms to the same assembly part shape above. The variant
is selected at assembly time by the part's variant_resolver in
assembly_definitions.luau — see that section below. All variants of a part
must expose the same socket layout (same attachment names on
AssemblySocketAttachments and AccessorySocketAttachments); geometry,
mesh, paint surfaces, etc. can differ freely.
Accessory part shape — each accessory part is a Model at
accessory/<node>/<part>, containing:
- MountAttachment (
Attachment): same as above. - AccessorySocketAttachments (
FolderofAttachments): for accessories that host child accessories (e.g. a front bumper with sockets for grille and fog lights). One attachment per socket id listed underaccessory_socketsin the definition. Omit (or leave empty) if the part has no accessory_sockets.
An accessory that hosts child accessories carries only its sockets here;
the child parts themselves are ordinary top-level entries under
accessory/<child_socket_id>/<part_id> (e.g. accessory/grille/standard),
resolved exactly like any other accessory. There is no per-part accessory/
sibling folder — the layout is flat. At assembly time the child's
MountAttachment is aligned to the parent's AccessorySocketAttachments/<socket_id>,
which is what places the grille on the bumper rather than on the bumper's parent.
Accessories cannot host assembly children — only accessory children — so they
never have an AssemblySocketAttachments folder.
Custom accessory socket billboards¶
Accessory socket billboards normally render at the socket attachment itself:
AccessorySocketAttachments/<socket_id>. If the default position or view angle
is not good for editing, add a Folder named CustomBillboardAttachments under
that socket attachment.
Each direct child of CustomBillboardAttachments should be an Attachment.
The customization UI will render one billboard location per custom attachment,
all opening the same accessory socket. Use this when a socket needs separate
left/right/front/rear click targets, or when the default socket attachment is
inside geometry.
Set the custom attachment's CFrame so its LookVector points toward the camera direction where the billboard should be visible. The UI picks the best matching billboard in the group and only shows it when the camera is inside that attachment's visibility cone.
Optional attributes:
VisibilityAngle(number): width, in degrees, of the visibility cone for that billboard attachment. Default is90. Values are clamped to90-360.
Example:
AccessorySocketAttachments
`-- mirrors -- Attachment for the socket
`-- CustomBillboardAttachments -- Folder
|-- LeftView -- Attachment, VisibilityAngle = 150
`-- RightView -- Attachment, VisibilityAngle = 150
Folding mirrors¶
assembly-v1 folding-mirror capability and mechanical configuration are owned by
each moving mirror model. The moving model may be nested inside a greater
assembly/accessory item. Folding mirrors are not configured by the vehicle
trim. AutomaticFoldingMirrors remains a Legacy-only field.
Do not put FoldMirrors, FoldMirrorsScript, motor-creation scripts, mirror
weld scripts, prebuilt Motor/Motor6D joints, or prebuilt mirror welds in an
assembly-v1 template. The server discovers a valid installed mirror pair,
builds its joints, and creates DriveSeat.FoldMirrors at runtime. Each client
observes that replicated state and animates the generated Motor6D transforms
locally, so folding does not depend on vehicle network ownership. The presence
of that BoolValue is the capability signal used by the keyfob and UI.
Moving mirror configuration¶
Each tagged moving mirror Model must contain its side's pivot and define these
attributes:
FoldingMirrorAngle(number, required): positive fold-angle magnitude in degrees. The client converts it to the radians required byMotor6D.Transformand applies it positively on the left and negatively on the right.FoldingMirrorMaxVelocity(number, required): positive animation velocity in radians per 1/60-second step. The client uses it to derive the transform tween duration. Use0.008to match common older mirror timing.
Put these attributes on each moving mirror model, not on its pivot or on the greater assembly/accessory template containing the mirror. Left and right may define different values when their mechanics require it.
Moving mirror models and pivots¶
For each side:
- Put all parts that rotate into one moving
Model. - Tag that moving model with
VehicleAssembly__FoldingMirror. - Tag that moving model with
VehicleChassis__SkipBodyWeldso the A-Chassis body-weld pass does not weld the pivot or moving mirror parts directly to theDriveSeat. - Set
FoldingMirrorSideon the moving model to exactly"Left"or"Right". - Put one dedicated hinge
BasePartinside it.SSis the existing naming convention, but the name is not required. - Tag the hinge part with
VehicleAssembly__FoldingMirrorPivot. - Place and orient the pivot so its local Z axis is the intended rotation axis.
The runtime requires exactly one tagged left pivot and one tagged right pivot
on the assembled vehicle. It welds every other descendant BasePart of each
moving model to that side's pivot. Every moving model must carry the explicit
tag; no Mirror-name fallback is supported.
Runtime hinge anchors¶
Do not author fixed hinge anchors or use the obsolete
VehicleAssembly__FoldingMirrorAnchor tag. After composing the assembly, the
server creates a clean invisible Part at each pivot's exact position and
orientation, places it outside the moving mirror model, and welds it to
DriveSeat. The folding Motor6D then connects that generated fixed anchor to
the authored pivot.
Generating anchors removes coupling between a mirror option and the frame or body option that supports it. The asset only defines the moving geometry, hinge transform, side, angle, and speed.
Example self-contained accessory:
accessory/mirrors/chrome-mirrors-w/-power-fold -- Model
|-- MountAttachment
|-- LeftMirror -- Model
| VehicleAssembly__FoldingMirror
| VehicleChassis__SkipBodyWeld
| FoldingMirrorSide = "Left"
| FoldingMirrorAngle = 70
| FoldingMirrorMaxVelocity = 0.008
| |-- SS -- BasePart
| | VehicleAssembly__FoldingMirrorPivot
| |-- Housing -- visual BasePart/MeshPart
| `-- Glass -- visual BasePart/MeshPart
`-- RightMirror -- Model
VehicleAssembly__FoldingMirror
VehicleChassis__SkipBodyWeld
FoldingMirrorSide = "Right"
FoldingMirrorAngle = 70
FoldingMirrorMaxVelocity = 0.008
|-- SS -- BasePart
| VehicleAssembly__FoldingMirrorPivot
|-- Housing
`-- Glass
A non-folding option in the same socket should omit the folding configuration
attributes and folding tags entirely. Selecting that option therefore produces
no DriveSeat.FoldMirrors runtime capability.
Optional sound¶
The extracted assembly-v1 DriveSeat may contain a Sound named Mirror.
When present, the runtime plays it on a requested state change and stops it
after the configured runtime window. The sound is optional; its absence does
not prevent folding and does not raise an error. Initial setup does not play it.
Auxiliary mirror lights¶
The new vehicle lighting system discovers light parts outside Body.Lights
through models or folders tagged VehicleLighting__LightContainer. The direct
children of a tagged container are matched by name against the vehicle's
LightSystemData entries and registered with the same behavior as ordinary
body lights.
For an illuminated folding mirror, tag the inner container whose direct
children are the named light parts. Do not move the mirror into Vehicle.Misc;
that path remains available for legacy vehicle mirrors.
Spawn and testing checklist¶
- Confirm the vehicle definition uses
AssemblySystem = "assembly-v1". - Remove
AutomaticFoldingMirrorsfrom assembly-v1 trims; it is ignored by the assembly folding system. - Remove legacy
FoldMirrors,FoldMirrorsScript,Motors, andMirrorWeldForMotorsinstances from the assembly templates. - Put
FoldingMirrorAngleandFoldingMirrorMaxVelocityon each moving mirror model. - Confirm the left and right moving mirror models have both the folding-mirror and skip-body-weld tags.
- Confirm exactly one left and one right moving mirror model, with explicit side attributes and one tagged pivot inside each.
- Remove all authored
VehicleAssembly__FoldingMirrorAnchortags and parts that existed only to serve as folding anchors. - If the moving mirror contains lights, tag its inner light container with
VehicleLighting__LightContainer. - Spawn the vehicle with the folding option already equipped.
- On the server, confirm
DriveSeat.FoldMirrorswas created, the vehicle has aVehicleAssembly__FoldingMirrorRuntimefolder containing two generated anchors, and each pivot has one generatedVehicleAssembly__FoldingMirrorMotor6Dplus generatedVehicleAssembly__FoldingMirrorWeldjoints. - Set
DriveSeat.FoldMirrors.Valuetotrueand thenfalse, or use the keyfob Mirrors action. On every client, both mirrors should move symmetrically regardless of the vehicle's network owner.
Before publishing the asset, run the assembly asset validator:
mise exec -- lune run .lune/verifyVehicleAssemblyAsset.luau "src/ServerStorage/LocalVehicles/<asset>.rbxm"
It validates the mirror pair, degree/speed configuration, explicit moving-model and pivot tags, the absence of authored anchors, light-container ownership, and legacy runtime artifacts.
Assembly accessory purchases currently exit customization and spawn a freshly assembled vehicle, so spawn-time discovery covers installed-option changes. If in-place assembly/accessory replacement is added later, the folding service must be notified to rebuild the pair and runtime capability.
Legacy vehicles keep their existing trim-controlled AutomaticFoldingMirrors,
DriveSeat.FoldMirrors, and legacy motor/script layout. Do not apply this
assembly-v1 asset setup to Legacy vehicle assets.
VehicleDef sibling files¶
Every AssemblySystem = "assembly-v1" vehicle directory under
src/Shared/VehicleDef/Vehicles/<vehicle>/ contains the following files,
co-located with init.luau. The *_definitions.luau files are data
registries read on demand by whatever system needs them.
init.luau¶
The vehicle definition function — returns the VehicleDef with its trims.
The VehicleDef sets BaseAssemblyTree (see below). Each trim of an assembly-v1
vehicle sets AssemblyOptions inline (see below).
BaseAssemblyTree (VehicleDef field)¶
A single AssemblyTreeNode set inline on the VehicleDef (in init.luau)
describing the slots that exist on this vehicle and their parent/child
relationships — the assembly skeleton shared by all of the vehicle's trims,
before accessories are applied. Slots may be required = false and may declare
overrides = "..." to indicate that, when filled, they replace some other slot
(e.g. the Sierra HT snow-plow slot overrides accessory::drivetrain:front-bumper).
AssemblyOptions (TrimDef field)¶
Set inline on each assembly-v1 trim in init.luau — a mapping of assembly-tree
slot id to the list of part ids allowed in that slot for that trim:
AssemblyOptions = {
frame = { "frame:standard-4x2" },
cab = { "cab:standard" },
drivetrain = { "drivetrain:6-7", "drivetrain:8-9" },
vocation = { "vocation:stakebed-standard", "vocation:delivery-style-1", ... },
},
assembly_definitions.luau¶
List of Types.assembly_part_definition(...) entries. The Vehicles loader
indexes the list by each entry's stable id. There should be one entry for
every assembly part referenced by trim AssemblyOptions or package
customization assignments. Per-entry fields:
id— stable catalog/saved-data identifier, e.g."frame:standard-4x2". Package data andAssemblyOptionsreference this value.path— colon-delimited path beneath the asset'sassembly/folder, e.g."frame:standard-4x2". It commonly matchesid, but may differ when the asset hierarchy changes while the stable id should not.display_name— UI string.assembly_sockets— list of socket ids this part exposes for child assembly parts. Required-ness is determined by the assembly tree, not here.accessory_sockets— sockets this part exposes for accessory child parts. Each entry is a{ id, required, display_name, options }record;required = truemeans the socket must be filled when the part is installed,display_nameis the UI label for the socket, andoptionslists accessory definition ids that may fill it.liveries— optional livery definitions for this part.variant_resolver(optional) —(finalAssembly) -> string. When present, the asset entry atassembly/<path>is expected to be a Folder of variant Models (see "Variant-shaped assembly parts" above). The resolver reads the finalized assembly (slot -> part id map) and returns the variant id of the child Model to use. Pure function; no side effects.
assembly_definitions.luau is auto-loaded by the Vehicles loader for
assembly-v1 vehicles when present and exposed as VehicleDef.AssemblyDefinitions.
In practice, assembly-v1 vehicles should include definitions for every assembly
part they can build; the model builder and validators rely on those definitions
to find asset paths and socket metadata.
accessory_definitions.luau¶
Same source/list shape as assembly_definitions.luau, but for accessory parts
(the ones under accessory/ in the asset). The loader indexes entries by
stable id; each entry's path points beneath the asset's accessory/
folder. Matching id and path is common, but not required.
Accessory parts have their own asset shape (see "Accessory part shape" above):
a MountAttachment and, when the accessory hosts child accessories, an
AccessorySocketAttachments folder. The asset must conform to its
definition. An accessory's accessory_sockets use the same
{ id, required, display_name, options } shape as on assembly parts, and the
parts that fill those sockets are plain top-level entries in this same
definition list (e.g. a front-bumper:standard with a grille socket is
filled by grille:standard, grille:standard-w-sensor, ... entries), not
nested on the bumper's def.
tuning_definitions.luau¶
Optional registry for non-physical DriveSeat overlay models under tuning/ in
the asset. Each entry is built with Types.tuning_definition({ id, path,
display_name }), where path is a colon-delimited path under the asset's
tuning/ folder. The Vehicles loader exposes the map as
VehicleDef.TuningDefinitions.
Tuning models do not need MountAttachment, AssemblySocketAttachments, or
AccessorySocketAttachments. They are copied onto the assembled DriveSeat;
see Assembly Vehicle Tuning.
Where to find the runtime¶
- Vehicle/trim loading + assembly auto-loading:
src/Shared/VehicleDef/Vehicles/init.luau. - DriveSeat tuning overlay docs: Assembly Vehicle Tuning.
- Assembly-to-trim helper used by preview/spec/validation paths:
VehicleDef:findTrimByAssembly(vehicleKey, assembly). - Purchased trim identity for package purchases:
Package.vehicle_id. - Per-vehicle tree + per-trim options:
VehicleDef:getAssemblyConfig(vehicleId). - Asset kind for the dealership catalog:
src/Shared/VehicleDef/DealershipCatalog/initCatalogEntries.luau.