Skip to content

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 on TrimDef.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 on VehicleDef.AssetId.
  • The asset's single Model child is a Folder that contains one Model per trim, each tagged with a NumberValue/IntValue whose value is the trim's VehicleId. 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_id they purchase, while preview/spec helpers may still resolve a trim from the finalized assembly with VehicleDef: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-4x2
  • assembly/frame/electric-4x2
  • assembly/cab/standard
  • assembly/drivetrain/6-7
  • assembly/vocation/stakebed-standard
  • assembly/snow-plow/snow-plow-standard
  • accessory/front-bumper/standard
  • accessory/grille/standard
  • accessory/grille/standard-w-sensor
  • accessory/fog-lights/standard
  • accessory/side-mirrors/c-loop
  • tuning/frame/standard-4x2
  • tuning/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. frame for the Sierra HT) does not need one — it mounts at the vehicle base origin.
  • AssemblySocketAttachments (Folder of Attachments): where child assembly parts mount. One attachment per socket id listed under assembly_sockets in the definition.
  • AccessorySocketAttachments (Folder of Attachments): where child accessory parts mount. One attachment per socket id listed under accessory_sockets in 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 (Folder of Attachments): for accessories that host child accessories (e.g. a front bumper with sockets for grille and fog lights). One attachment per socket id listed under accessory_sockets in 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 is 90. Values are clamped to 90-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 by Motor6D.Transform and 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. Use 0.008 to 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__SkipBodyWeld so the A-Chassis body-weld pass does not weld the pivot or moving mirror parts directly to the DriveSeat.
  • Set FoldingMirrorSide on the moving model to exactly "Left" or "Right".
  • Put one dedicated hinge BasePart inside it. SS is 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
  1. Confirm the vehicle definition uses AssemblySystem = "assembly-v1".
  2. Remove AutomaticFoldingMirrors from assembly-v1 trims; it is ignored by the assembly folding system.
  3. Remove legacy FoldMirrors, FoldMirrorsScript, Motors, and MirrorWeldForMotors instances from the assembly templates.
  4. Put FoldingMirrorAngle and FoldingMirrorMaxVelocity on each moving mirror model.
  5. Confirm the left and right moving mirror models have both the folding-mirror and skip-body-weld tags.
  6. Confirm exactly one left and one right moving mirror model, with explicit side attributes and one tagged pivot inside each.
  7. Remove all authored VehicleAssembly__FoldingMirrorAnchor tags and parts that existed only to serve as folding anchors.
  8. If the moving mirror contains lights, tag its inner light container with VehicleLighting__LightContainer.
  9. Spawn the vehicle with the folding option already equipped.
  10. On the server, confirm DriveSeat.FoldMirrors was created, the vehicle has a VehicleAssembly__FoldingMirrorRuntime folder containing two generated anchors, and each pivot has one generated VehicleAssembly__FoldingMirrorMotor6D plus generated VehicleAssembly__FoldingMirrorWeld joints.
  11. Set DriveSeat.FoldMirrors.Value to true and then false, 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 and AssemblyOptions reference this value.
  • path — colon-delimited path beneath the asset's assembly/ folder, e.g. "frame:standard-4x2". It commonly matches id, 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 = true means the socket must be filled when the part is installed, display_name is the UI label for the socket, and options lists accessory definition ids that may fill it.
  • liveries — optional livery definitions for this part.
  • variant_resolver (optional) — (finalAssembly) -> string. When present, the asset entry at assembly/<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.