Reusing Config with Assemblies

assemblies reuse config fragments

Reusing Config with Assemblies

Real tables repeat themselves. Two flippers wired the same way. A bank of three drop targets that score identically. Four trough switches that differ only by hardware ID. Authored by hand, each copy is a place a typo can hide and a place you have to remember to change when the rule changes.

An assembly is the fix: you write the repeated group of blocks once, mark the bits that differ as parameters, and stamp out an instance per flipper, target, or lane with a use block. This guide takes a duplicated config and refactors it into an assembly step by step, then covers the full surface — parameters, instance generators, and tag propagation.

It assumes you’re comfortable with devices, variables, and scoring rules. If not, walk through Table Design Basics first.

Start with the duplication

Here are three standup targets. Each one is a switch, a lamp, a per-ball hit counter, and a scoring rule. The blocks are identical except for the hardware IDs, a position label, and the point value:

# --- Left target ---
device "switch" "standard" "left_sw" {
  id          = 0x30
  debounce_ms = 4
}
device "light" "standard" "left_lamp" { id = 0x50 }
variable "int" "left_hits" {
  initial = 0
  scope   = "ball"
}

score "event" "left_scored" {
  when   = device.left_sw.hit
  points = 5000
}

# --- Center target ---
device "switch" "standard" "center_sw" {
  id          = 0x31
  debounce_ms = 4
}
device "light" "standard" "center_lamp" { id = 0x51 }
variable "int" "center_hits" {
  initial = 0
  scope   = "ball"
}

score "event" "center_scored" {
  when   = device.center_sw.hit
  points = 10000
}

# --- Right target --- (and a third near-identical copy)

Three copies, and every copy is a maintenance liability. Add a “lamp flashes on hit” rule and you have to add it three times, correctly, in three places.

Extract the pattern into an assembly

An assembly "name" { … } block holds the repeated blocks once. Everything that varied between copies — the IDs, the label, the points — becomes a parameter, referenced inside the body as param.<name>:

assembly "target" {
  parameter "int"    "switch_id"   { required = true }
  parameter "int"    "lamp_id"     { required = true }
  parameter "string" "position"    { required = true }
  parameter "int"    "base_points" { default  = 5000 }   # optional — falls back to 5000

  device "switch" "standard" "sw" {
    id          = param.switch_id
    debounce_ms = 4
  }

  device "light" "standard" "lamp" {
    id = param.lamp_id
  }

  variable "int" "hit_count" {
    initial = 0
    scope   = "ball"
  }

  score "event" "scored" {
    when   = device.self.sw.hit   # "self" = this instance's own sw device
    points = param.base_points
  }
}

An assembly definition on its own produces nothing — it’s a template. You stamp out concrete blocks with use, passing a value for each parameter:

use "target" "left_target" {
  switch_id   = 0x30
  lamp_id     = 0x50
  position    = "left"
  base_points = 5000
}

use "target" "center_target" {
  switch_id   = 0x31
  lamp_id     = 0x51
  position    = "center"
  base_points = 10000
}

use "target" "right_target" {
  switch_id = 0x32
  lamp_id   = 0x52
  position  = "right"
  # base_points omitted → defaults to 5000
}

The rule now lives in exactly one place. Add the lamp-flash behavior to the assembly body and all three targets get it.

Parameters

Each parameter "<type>" "<name>" { … } declares one input. The type label matches Cade’s value types — the common ones are int, number, string, bool, list, and map. Inside the assembly body, read the value as param.<name>.

FieldRequiredWhat it does
requirednoWhen true, every use must supply this argument. Defaults to false. Don’t combine with default.
defaultnoValue used when a use block omits the argument.
descriptionnoHuman-readable note describing the parameter.
validationnoConstraint on accepted values, for example validation = "enum:easy,normal,hard".
parameter "string" "mode" {
  description = "Difficulty preset for this lane"
  default     = "normal"
  validation  = "enum:easy,normal,hard"
}

Referring to an assembly’s own blocks

Inside the body, an instance refers to its own generated blocks with device.self.<name> (and the same self form for other block kinds). This is what keeps three instances from cross-talking — device.self.sw.hit in the target assembly resolves to this instance’s switch, never a sibling’s:

score "event" "scored" {
  when   = device.self.sw.hit      # this instance's switch only
  points = param.base_points
}

When expanded, each instance’s blocks are renamed <instance>__<block> — so left_target’s switch becomes left_target__sw and center_target’s becomes center_target__sw. The self references are rewritten to match, guaranteeing isolation no matter how many instances you stamp out.

What can go inside an assembly

An assembly body isn’t limited to devices and scoring — it can hold any of the table’s building blocks, and every one is namespaced per instance on expansion:

BlockWhat it contributes to each instance
deviceHardware — switches, lights, coils
variablePer-instance state (e.g. scope = "ball" or "game")
constantNamed fixed values
score "event"Scoring rules triggered by the instance’s events
signalSwitch sequences / shots over the instance’s devices
event_handlerReactions to the instance’s events, with an actions { … } body
audio_clipSounds the instance can play
route / selectionPer-instance audio routing, addressed with owners = device.self.<name>
useAnother assembly, nested inside this one (see Nesting assemblies)

Each is renamed <instance>__<block> on expansion just like devices, and self references inside them resolve to the instance’s own blocks. For example, a constant, signal, event_handler, and audio_clip defined in a target assembly’s body become left_target__MAX_HITS, left_target__combo, and so on for instance left_target.

Generating many instances at once

When you need N near-identical instances, you don’t have to write N use blocks. Two generator meta-arguments expand a single use into many. They’re mutually exclusive — use one or the other.

count — a fixed number of instances, suffixed __0, __1, …:

use "trough_switch" "opto" {
  count = 4
}
# → opto__0, opto__1, opto__2, opto__3
#   devices: opto__0__sw, opto__1__sw, 

for_each over a list — one instance per element, suffixed by the value:

use "pop_bumper" "bumper" {
  base_points = 500
  for_each    = ["top", "left", "right"]
}
#  bumper__top, bumper__left, bumper__right

for_each over a map — one instance per key, with per-instance values:

use "drop_target" "target" {
  for_each = {
    red    = { id = 0x30, points = 5000 }
    yellow = { id = 0x31, points = 3000 }
  }
}
#  target__red, target__yellow  (keys expand in sorted order)

Nesting assemblies

An assembly can use another assembly in its body, so you compose larger units from smaller ones. The namespaces chain: a lamp device inside an indicator assembly, nested into a target assembly as instance ind, expands to <target-instance>__ind__lamp.

assembly "indicator" {
  parameter "int" "lamp_id" { required = true }
  device "light" "standard" "lamp" { id = param.lamp_id }
}

assembly "target" {
  parameter "int" "sw_id"   { required = true }
  parameter "int" "lamp_id" { required = true }

  device "switch" "standard" "sw" { id = param.sw_id }

  # nest the indicator assembly, passing a parameter through
  use "indicator" "ind" { lamp_id = param.lamp_id }
}

use "target" "left" {
  sw_id   = 0x30
  lamp_id = 0x50
}
#  devices: left__sw, left__ind__lamp

Nesting is bounded for safety: assemblies may nest up to 10 levels deep, and a cycle — two assemblies that use each other — is rejected at load time with the offending path, e.g. circular assembly nesting: b -> a -> b.

Tags propagate from three sources

Tags are how you address groups of devices later (for device_control selectors, bulk operations, dashboards). An assembly unions tags from three places onto every device it generates:

  1. Assembly-level tags — applied to every device in every instance.
  2. Instance-level tags on the use block — applied to every device in that instance.
  3. Device-level tags inside the body — applied to that one device.
assembly "bumper" {
  tags = ["autofire"]                       # ① every device, every instance

  parameter "int" "switch_id" { required = true }
  parameter "int" "coil_id"   { required = true }

  device "switch" "bumper" "sw" {
    id   = param.switch_id
    tags = ["sensor"]                        # ③ just this switch
  }
  device "coil" "bumper" "coil" { id = param.coil_id }
}

use "bumper" "pop_1" {
  switch_id = 0x40
  coil_id   = 0x30
  tags      = ["upper_playfield"]            # ② every device in pop_1
}
# pop_1's switch ends up tagged: autofire, upper_playfield, sensor
# pop_1's coil   ends up tagged: autofire, upper_playfield

The result is the deduplicated union — no need to repeat a shared tag on every block.

Referencing an instance from outside

Other blocks reach an assembly’s generated devices by their expanded names. You can write the logical form device.assembly.<instance>.<block> and Cade rewrites it to <instance>__<block> during expansion:

use "flipper" "left_flipper" {
  button_id = 0x11
  coil_id   = 0x01
}
use "flipper" "right_flipper" {
  button_id = 0x12
  coil_id   = 0x02
}

# A combo that needs both flipper buttons:
signal "combo" "double_flip" {
  switches = [
    device.assembly.left_flipper.button,
    device.assembly.right_flipper.button,
  ]
}

Assemblies, fragments, and modules

Three blocks reduce repetition, each for a different shape of reuse:

BlockReusesReach for it when…
assembly / useA parameterized group of hardware + rulesYou have physical repeats — flippers, targets, lanes — that differ only by ID, label, or value.
fragmentA bundle of scoring values pulled into score rulesYou repeat the same points / condition values across multiple score "event" rules.
moduleA bundle of game logic (modes, scoring, variables, audio)You’re packaging a behavior — a multiball mode, a tilt handler — not a piece of hardware.

Where to go next

  • See every block and attribute — the Table Configuration reference covers assembly / use alongside devices, scoring, and platforms.
  • Write the expressions inside points and conditionWriting Expressions.
  • Look a term up — the Glossary has quick definitions for assembly, fragment, and module.