---
title: "Assemblies: slots, parts and swapping them at runtime"
description: "Attach parts to named slots (sockets, mount points, hardpoints) on an entity, choose which part fills each slot from Luau…"
section: "Topics"
slug: "topics-assemblies"
canonical: "https://origozero.ai/docs/topics-assemblies"
updated: "2026-10-26T04:54:19.410725253+00:00"
tags: ["documentation", "guide"]
---

# Assemblies: slots, parts and swapping them at runtime

- **`Slot`**: a named place a part fills. It goes on an (usually empty) child
  entity standing where the part attaches: the part is built as a child of
  that entity, at its transform. It states the `kind` of part it takes
  (`wheel`, `engine`), the asset `tags` a part must carry, a `default` part,
  and optionally a `joint` that binds the part's body to the body the slot is
  on.
- **`Part`**: on the root of a part's bundle, `fits` lists the slot kinds the
  part fits. A part from another world's package carries this with it.
- **`Assembly`**: on the root the slots belong to. Its `config` says what fills
  each slot, by the slot's path, and its methods fill, empty, swap, resolve and
  explain.

`Humanoid` role sockets (`righthand`, `head`, ...) are slots of the same family:
an `Assembly` on a humanoid lists them, and filling one seats the part the way
`Humanoid:attach` does.

## Build an assembly

```luau
-- Parts: one bundle each, with a Part saying which slots it fits
local function makePart(name, kind, meshEntity)
    meshEntity.component.add("Part", { fits = { kind } })
    return asset.create("bundle", name, { entity = meshEntity })
end

-- The body: an Assembly on the root, a Slot on each mount point
local body = entity.spawn("Buggy")
body.component.add("Assembly")
for _, m in ipairs({
    { "wheel_fl", "wheel", { x = -0.8, y = 0.3, z =  1.2 } },
    { "wheel_fr", "wheel", { x =  0.8, y = 0.3, z =  1.2 } },
    { "engine",   "engine", { x = 0, y = 0.5, z = 1 } },
    { "seat",     "seat",   { x = 0, y = 0.6, z = 0 } },
}) do
    local mount = entity.spawn(m[1], { parent = body.id, position = m[3] })
    mount.component.add("Slot", { kind = m[2] })
end

-- Save the body as a bundle; every placed instance carries the slots
local buggy = asset.create("bundle", "Buggy", { entity = body })
```

Place it the usual way (`entity.spawn("car").component.add("Asset", { source =
buggy })`): the `Assembly` and the slots arrive with it.

## Fill, swap and read it

```luau
local asm = car.component.get("Assembly")

asm:configure({
    wheel_fl = "parts.SportWheel", wheel_fr = "parts.SportWheel",
    engine = { part = "parts.V8", turbo = "parts.Turbo" },   -- a part's own slots nest
    seat = "parts.BucketSeat",
})

asm:fill("wheel_fl", "parts.OffroadWheel")   -- rebuilds that wheel alone
asm:empty("seat")                            -- no seat, default included
asm:fill("seat", nil)                        -- back to the slot's default

for _, s in ipairs(asm:resolve()) do         -- what fills what, as a tree
    print(s.path, s.part, s.source, s.fill)
end
```

A slot's **path** is its name under the path of the slot whose part it stands
in: the V8 bundle carries a `turbo` slot, so in the `engine` slot that slot is
`engine/turbo`. The configuration holds every path, including slots of a part
swapped away, so putting the V8 back brings its turbo back.

Swapping one part rebuilds that slot's part (and the parts in its slots) and
nothing else: every other part keeps its entity and whatever state was written
on it.

## What fits, and why not

```luau
asm:fits("wheel_fl", "parts.V8")
-- false, "slot 'wheel_fl' takes parts of kind 'wheel'; 'parts.V8' fits { engine }"
asm:candidates("engine")       -- every bundle this world holds that fits
wheelSlot.tags = { "offroad" } -- the slot now takes only parts tagged offroad
```

`fill` raises with the same sentence for a part that does not fit, and names
the slots there are for a path no slot answers to. `configure` checks every
part first and changes nothing when one does not fit.

## Undo, multiplayer and saving

`config` is a synced, saved field, so:

- each `fill`, `empty` and `configure` is one undoable step;
- the entity's owner changes it and every peer builds the same parts from it
  (another peer's call is refused, naming the owner);
- it is saved with the scene, and the parts are rebuilt from it on load. The
  part entities are runtime children and are never written into the scene.

## Joints

A slot whose `joint` names Joint fields (`{ kind = "hinge", axis = { 1, 0, 0 }
}`) adds a `Joint` to the part's root, `connected` to the slot, so a part with
its own body (a trailer, a door, a swinging arm) is held to the body the slot
stands on by that joint. Without one, the part rides the slot as a child.

## Where to read more

`types/component` covers components in general; the `Slot`, `Part` and
`Assembly` component READMEs list every field and method, and the `bundle`
type README covers saving an assembly or a part as a bundle.
