0.2 Series

releases changelog upgrading

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 script0.2 actions {}
emit "x"emit = x
start_mode "m"start_mode = module.m
end_modeend_mode = []
play_show "intro", 10play_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 scope now means player, not global. Such variables reset with each game instead of persisting across games. Add scope = "global" wherever you relied on the old behavior. Nothing reports this one.
  • scope = "game" is removed (it used to mean global). Replace it with global, machine or session, 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.10.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 device event {} and properties {} sub-blocks, and audio { ball_rolling / ball_drop / ball_collision };
  • a singular action {} in an event_handler — use actions {};
  • 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.target that 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 --config path 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.