0.2 Series
The 0.2 line makes .cade a native-HCL language end to end. Mode logic is
written as structured actions {} blocks instead of embedded script, any
reference can be written fully qualified, and a table can be split into
per-domain source files. It also closes a long tail of configuration that used
to parse cleanly and then do nothing — most of those cases now produce an error
or a warning instead.
That makes this the first release that asks something of a table written for 0.1. Read Upgrading from 0.1 before you move a table across.
For the entry-by-entry record, see the Changelog.
v0.2.0 “Whiffle” — 2026-10-04
Upgrading from 0.1
Work through these in order. Most of them fail loudly — a parse error, a load error or a warning that names the retired spelling — so a table that loads cleanly afterwards has been converted. Two of them do not fail: the new default variable scope and the change to how scoring rules share a trigger both alter behavior silently, so check those by hand.
Mode event handlers
The quoted trigger label and the do = <<-EOT … EOT script body are gone; both
now fail with a parse error that names the retired spelling. The trigger moves
into a when attribute and the script becomes an actions {} block.
Before:
on "device.frenzy_target.hit" {
do = <<-EOT
start_mode "frenzy"
EOT
}After:
on {
when = device.frenzy_target.hit
actions {
start_mode = module.frenzy
}
}Inside actions {}, each verb’s value is its argument list:
| 0.1 script | 0.2 actions {} |
|---|---|
emit "x" | emit = x |
start_mode "m" | start_mode = module.m |
end_mode | end_mode = [] |
play_show "intro", 10 | play_show = ["intro", 10] |
set var.x = … | set_variable { name = "x" value = … } |
The same conversion applies to on_start, on_end, and timer on_expire /
on_complete bodies. An unknown block inside events {} is now an error.
Known issue in v0.2.0: four mode verbs — award_points, play_show,
stop_show and trigger — parse inside a mode’s actions {} but do nothing
when the mode runs. Award points from a score block, and start a light show
from a module-level event_handler, until a patch release fixes this.
Variable scope — check this by hand
The scopes are global, machine, session, player and ball.
- An omitted
scopenow meansplayer, notglobal. Such variables reset with each game instead of persisting across games. Addscope = "global"wherever you relied on the old behavior. Nothing reports this one. scope = "game"is removed (it used to meanglobal). Replace it withglobal,machineorsession, whichever you meant.- An unknown scope is an error.
Variable types
The type labels are int, float, bool, string, list, map and
probability. The alias spellings — integer, double, number, boolean,
text and array — are now errors:
# 0.1
variable "integer" "x" { }
# 0.2
variable "int" "x" { }Scoring rules that share a trigger — check this by hand
Every scoring rule bound to a trigger now fires, in priority order. In 0.1, when two rules shared a trigger only the higher-priority one scored (or, at equal priority, the scoring config failed to compile). If your table used priority to override one rule with another, remove the rule you don’t want — otherwise both now score.
Coil tuning
Coil tuning lives in settings {}. On a coil, default { power, duration },
settings { trigger_time }, main {}, hold {}, pulse {} and
properties { pulse_ms } are no longer read. Use
settings { pulse_ms, strength, hold_power, recycle_ms, max_pulse_ms }, where
strength and hold_power are whole percentages:
# 0.1
default { power = 0.8 }
# 0.2
settings { strength = 80 }While you are in the hardware configuration: hardware {} addresses are now
plain hex bytes, as the drivers read them — coil = "C01" becomes
coil = "01".
Device categories
An unknown device category — for example a subtype written where the category goes — is now an error instead of a device that no driver backs:
| 0.1 | 0.2 |
|---|---|
device "led" "matrix" "gi" | device "light" "led" "gi" |
device "ramp" … | device "switch" "ramp" … |
Tables stored as separate unit files
In a table stored as separate unit files, the scoring unit is renamed from
rules.cade to scoring.cade, and a new modes.cade unit holds module,
mode and interaction_context blocks. A single-file table loads as before.
Configuration that never took effect
A table that loaded under 0.1 with any of these constructs now gets a warning or an error. None of them did anything before, so the fix is to remove or convert them rather than to restore a behavior:
- unrecognized top-level blocks and device sub-blocks, including
game_audio,audio_effect,audio_group, the deviceevent {}andproperties {}sub-blocks, andaudio { ball_rolling / ball_drop / ball_collision }; - a singular
action {}in anevent_handler— useactions {}; - a labelled
set_variable "x" {}; - unknown attributes in a
flow; - a scoring rule or handler with no trigger;
- a score expression whose operand types don’t match;
- an unsupported
selection_method; - a lightshow
output.targetthat names a device rather than a layout.
audio_system {} engine tuning moves out of .cade and into the audio {}
block of cade.conf.
cade.conf
The runtime configuration file is validated at startup:
- an invalid block now stops startup instead of falling back to defaults;
- a
--configpath that does not exist is an error; analytics { backend_type = "grpc" }is rejected — it never sent events anywhere.
Settings that used to be ignored now take effect, which can change what a
running cabinet does: enabled = false in the history {} and analytics {}
blocks really disables recording, and CADE_* environment variables now
override the file. logging {} now reaches every command.
Install and download scripts
Releases now ship .tar.gz / .zip archives plus .deb, .rpm and .apk
packages instead of bare binaries. A script that downloads a
bare binary needs updating. Each archive also comes with an SBOM. See the
Downloads page for the current files.
Writing references
Any reference can now be written fully qualified —
device.<category>.<subtype>.<name>.<action> — with * in the name position as
an explicit group, as in device.switch.rollover.*.hit. References are written
as bare traversals rather than quoted strings, and device lists accept tags:
tags = [tag.bumper]. Short spellings are still accepted, so this is something
you can adopt rather than something you must.
Device action verbs take the device as an attribute:
pulse_coil {
device = device.coil.standard.reset
}The older label form, pulse_coil "reset" {}, still parses.
# and // comments now work inside action and function bodies, and mode
functions {} bodies are checked when the table loads rather than on their
first call.
Variables
Variables gain list, map and working string types, with indexing
(var.scores[var.mode]) and list.* / map.* functions. All nine documented
decay and growth attributes — auto_decay, decay_interval, grow_amount and
the rest — now work on variables. set_variable evaluates expressions such as
var.credits - 1, not just literals.
Modes and player input
An interaction_context is a reusable block that, while a mode is active,
remaps player input with an input_map — including held_for(...) holds and
release edges — drives displays, and inhibits devices.
Known issues in v0.2.0: a device_control block written on an
interaction_context parses but never inhibits anything — put device_control
on the mode that uses the context instead. And the context’s actions — mapped
edges, release edges, held_for(...) holds and its displays — take effect only
in a browser host such as showcade. Under cade run and the console, an edge
the context maps is consumed and nothing happens, so keep hardware tables on
ordinary mode event handlers for now.
For multiball and add-a-ball, ball_watermark { target = … } on a mode holds a
minimum ball count in play and tops it up. A module { mode {} } can now be
composed inside an assembly.
Several things a 0.1 mode could declare did not work, and now do: event
handlers inside a module that has a mode fire, mode.<name>.active evaluates
correctly, a module’s shows { start / end } play, and play_show and
stop_show work.
Rules and scoring
repeat_count and repeat_window on an event_handler detect tap-passes.
Shot and sequence signals now run in production, with combo chaining, all_down,
per-step points, an enabled gate, and a working fire_signal.
Cade now owns drop-bank reset and publishes bank_complete. launch_ball
auto-plunges a served ball through a shooter-lane auto-launch coil.
High scores come built in: a board, a high_score.qualified event, initials
entry both in the console and on machine hardware, and a correction command.
On machine hardware, entry is driven by an insert_initials mode whose event
handlers emit initial_prev_letter, initial_next_letter and
initial_confirm. An insert_initials mode built on an interaction_context
works only in a browser host in v0.2.0 (see the known issues under
Modes and player input).
Scoring is also faster: score conditions are compiled once rather than re-evaluated from scratch on every event, and handling an event no longer allocates memory.
Displays
0.2 adds a dot-matrix display pipeline: a 128×32 shaded frame buffer, a priority-ordered compositor, image and sprite sources, and a text rasterizer.
display_show blocks carry authored trigger criteria and arbitrate by
priority, blend and opacity; the display_show and stop_display_show actions
start and stop them, and each element has a visible attribute.
display_text resolves ${…} when it fires, so a live score reaches the
display.
Audio
Oscillators gain amplitude modulation (target = "<osc>.amp") alongside FM.
route { ordered = true } rotates clips in order, probability_method selects
among the four probability mapping methods, and there is a new
reset_selection action. A modulation loop is now rejected as you author it rather
than when the synth compiles, and labelled background_music blocks are read
instead of dropped.
Editing in a browser host
For editors built on the runtime, source units can be validated without
applying and formatted, a table can be split block by block into the multi-file
layout, and the combined source can be exported. The runtime publishes its
event vocabulary — including the system.* lifecycle events — with
fully-qualified event names and what each reference parameter refers to. The
rules graph now covers modes, interaction contexts, variable writes and action
verbs, and all of them can be edited. Display frames, display descriptors, show
frames and a session export with its full event tree are readable from the
browser build.
cade fmt and editor writes no longer corrupt bare hex literals or drop
comments.
Running on hardware
Declared coils, switches, servos and flippers are configured on every driver. Device commands can be sent to a single connected session, and each event records which session it came from.
With cade run, mode timers, emit in mode actions, firmware light shows and
device commands now take effect — in 0.1 they did not. On the console, routed
device audio is no longer silent.
Console, replays and tooling
cade console validate-config checks a console configuration file, and
--log-level and --log-format work on every subcommand. In the TUI, history
rows are scoped to the current game and switch events are grouped by ball again.
Replays line up: deferred engine work, mode timers, parse-error order and the configuration identity hash are now deterministic, and the hash no longer depends on file names, source positions or how many files a table uses. A recorded session is now saved when the run is stopped, not only at game over.