Configure Visual Pinball and Cade

tutorial vpx visual-pinball grpc

Configure Visual Pinball and Cade

Level IntermediateTime 30 min

You’ll connect a Visual Pinball X table to Cade so the simulated playfield drives a real Cade configuration: switch hits arrive over the gRPC bridge and score through your rules, while Cade streams coil and light commands back to VPX. No physical hardware required.

Before you start:

  • Cade installed and on your PATH — see Getting Started.
  • You’ve written a table before — if not, do First Steps with Cade first.
  • A build of Visual Pinball X with the Cade bridge plugin, running the example table. The plugin is what lets VPX speak to Cade; with it installed, VPX connects to Cade automatically once the runtime is configured below.

Cade and VPX split the work: VPX simulates the playfield (physics, the ball, flipper response) and Cade runs the rules (scoring, modes, lights, ball lifecycle). They talk over a gRPC bridge — VPX sends switch events in, Cade sends coil and light commands back, exactly as it would with a physical controller.

On the Cade side you’ll author two files in one directory — a machine definition (.cade) describing the table, and a runtime config (cade.conf) pointing Cade at VPX. On the VPX side, the table script shrinks to almost nothing.

Define the machine

Create a working directory for the table. A machine definition opens with the table’s identity, a game block, and the devices VPX exposes. The bridge matches a VPX object to a Cade device by name — so you name each device exactly what VPX calls it (left_flipper, Bumper1, …) and Cade recognizes it. Here’s a representative slice of vpinball_example_table.cade:

vpinball_example_table.cade
name    = "VPinball Example Table"
version = "1.0.0"

game {
  name           = "VPinball Example Table"
  max_players    = 4
  balls_per_game = 3
}

# Flippers. VPX runs the flipper physics locally for zero latency; Cade enables
# them at ball start, disables them on tilt, and records the hits for scoring.
# grpc_action names the VPX button that drives the flipper.
device "flipper" "standard" "left_flipper" {
  id          = 1
  grpc_action = "left_flipper"
  hardware {
    coil   = "C01"
    switch = "S01"
  }
  settings {
    strength  = 75
    hold_time = "250ms"
  }
}

device "flipper" "standard" "right_flipper" {
  id          = 2
  grpc_action = "right_flipper"
  hardware {
    coil   = "C02"
    switch = "S02"
  }
  settings {
    strength  = 75
    hold_time = "250ms"
  }
}

# A pop bumper is a switch — the skirt that closes when the ball strikes it.
# The name (Bumper1) matches the VPX object; the table has five, Bumper1–Bumper5.
# VPX fires the bumper coil locally; Cade just scores the hit.
device "switch" "bumper" "Bumper1" {
  id = 10
}

Score the hits

A device only emits events; scoring is a separate rule. Write one score "event" block per bumper, keyed on that bumper’s own event:

vpinball_example_table.cade
# 100 points for every bumper hit. One rule per bumper — the file repeats this
# for Bumper1 through Bumper5.
score "event" "Bumper1_hit" {
  when   = device.Bumper1.activated
  points = 100
}

The blocks above are excerpts. The full file — the two flippers, all five pop bumpers (Bumper1Bumper5), and a scoring rule for each — is ready to use. Expand it to read the whole thing, or hit Copy to grab it straight to your clipboard:

vpinball_example_table.cade
# VPinball Example Table — simplified for the "Configure VPX and Cade" tutorial.
# Just the flippers, the drain, and the five pop bumpers, scoring 100 points per
# hit. No audio and no other playfield devices, to keep the first run easy to
# follow.
#
# Device names match the VPX object names exactly (left_flipper, Bumper1, …) —
# that's how the bridge matches a VPX device to a Cade device, and how each
# bumper's hit reaches its scoring rule.

name    = "VPinball Example Table"
version = "1.0.0"

game {
  name           = "VPinball Example Table"
  max_players    = 4
  balls_per_game = 3
}

# Flippers put the ball into play. VPX runs the physics locally; Cade enables
# them at ball start and records the hits. (The hardware coil/switch addresses
# are only used when driving real hardware.)
device "flipper" "standard" "left_flipper" {
  id          = 1
  grpc_action = "left_flipper"
  hardware {
    coil   = "C01"
    switch = "S01"
  }
  settings {
    strength  = 75
    hold_time = "250ms"
  }
}

device "flipper" "standard" "right_flipper" {
  id          = 2
  grpc_action = "right_flipper"
  hardware {
    coil   = "C02"
    switch = "S02"
  }
  settings {
    strength  = 75
    hold_time = "250ms"
  }
}

# The five pop bumpers. A bumper is declared as a switch — the skirt that closes
# when the ball strikes it — named Bumper1–Bumper5 to match the VPX objects. VPX
# fires the bumper coil locally; Cade just scores the hit.
device "switch" "bumper" "Bumper1" {
  id = 10
}

device "switch" "bumper" "Bumper2" {
  id = 11
}

device "switch" "bumper" "Bumper3" {
  id = 12
}

device "switch" "bumper" "Bumper4" {
  id = 13
}

device "switch" "bumper" "Bumper5" {
  id = 14
}

# The drain. Declared with subtype "trough" so Cade treats a hit on this device
# as the ball draining (ends the ball / game). The name must match the VPX
# element exactly — this table's drain kicker is named "Drain". Without this
# entry Cade only knows the default key "trough", which never matches "Drain",
# so drains are never detected.
device "switch" "trough" "Drain" {
  id = 20
}

# 100 points for every bumper hit. One rule per bumper, keyed on the device's
# own event (device.Bumper1.activated, …).
score "event" "Bumper1_hit" {
  when   = device.Bumper1.activated
  points = 100
}

score "event" "Bumper2_hit" {
  when   = device.Bumper2.activated
  points = 100
}

score "event" "Bumper3_hit" {
  when   = device.Bumper3.activated
  points = 100
}

score "event" "Bumper4_hit" {
  when   = device.Bumper4.activated
  points = 100
}

score "event" "Bumper5_hit" {
  when   = device.Bumper5.activated
  points = 100
}

Save it as vpinball_example_table.cade in your table directory. It’s deliberately small — just bumpers, no audio or other playfield devices — so the first run is easy to follow; you’ll build it out from here. The snippets above are taken straight from it, so the device names already match the VPX objects.

Point Cade at VPX

The machine definition says nothing about how to run the table. That goes in a separate runtime config. Create cade.conf alongside the .cade file:

cade.conf
# Bridge to VPX: Cade hosts a gRPC server; VPX connects to it.
platform "grpc" "vpx_bridge" {
  port              = 50051
  enable_gateway    = true
  enable_reflection = true
}

# Web server for health checks and the debug dashboard.
web {
  enabled = true
  port    = 8080
}

logging {
  level  = "info"
  format = "text"
}

Trim the table script

Bridged to Cade, the VPX table’s VBScript stays deliberately tiny. Flippers, bumpers, slingshots, and scoring are all handled by the bridge and your Cade rules — no Sub …_Hit handlers, no manual balls-in-play counter. Two things the bridge can’t do for you remain in the script: the player’s manual plunger launch, and removing the ball that physically falls into the drain. That’s the only glue you write in vpinball_example_table.vbs:

vpinball_example_table.vbs
Option Explicit

Dim EnableRetractPlunger
EnableRetractPlunger = False ' True: retract a button/key plunger at a linear speed — pull
                             ' back, hold at maximum for one second, then return to rest

Sub Table1_KeyDown(ByVal keycode)
    If keycode = PlungerKey Then
        If EnableRetractPlunger Then
            Plunger.PullBackandRetract
        Else
            Plunger.PullBack
        End If
    End If
End Sub

Sub Table1_KeyUp(ByVal keycode)
    If keycode = PlungerKey Then
        Plunger.Fire
    End If
End Sub


Sub Drain_Hit()
    Drain.DestroyBall
End Sub

Validate the configuration

From your table directory, check both files before running:

cade validate
Checkpoint
Validation prints no errors. With no arguments it checks every .cade file in the current directory. If it reports an unresolved reference, confirm the name in each score rule (Bumper1Bumper5) matches a device block — and that those names match the VPX objects exactly.

Run it and watch the event tree

Launch the console from the table directory:

cade console

The console opens on the Logs tab and auto-detects the platform "grpc" block in cade.conf, starting the gRPC bridge on port 50051. Press F2 to switch to the Events tab — the live event tree, where every event the table fires appears in real time.

Now bring up the table in VPX: open File ▸ New ▸ Full Example Table and launch it. With the bridge plugin installed it connects to Cade automatically. Then play a game the normal way:

  • Press 5 to drop a coin and add a credit.
  • Press 1 to start the game.

Each action lands in the event tree as it happens — the game and ball spans appear, then your hits stream in underneath, annotated with the points they scored:

VPinball Example Table ·  RUNNING · v1.0.0
────────────────────────────────────────────────────────────────────────
 Ball in Play  player1  400  Ball 1/3
switch.hit ▁▁▂▄█▁▃▁▁▁ 11   flipper.pressed ▁▁▁█▁▁▁▁▁▁ 6
────────────────────────────────────────────────────────────────────────
· [-] Player 1  Ball 1/3  Score: 400  Events: 11
╰── · [-] Ball 1  Score: 400  (8s)  Events: 11
    ├── · +1.204s device.Bumper1.activated [switch] +100
    ├── · +2.451s switch.hit <left_flipper>
    ├── · +3.310s device.Bumper3.activated [switch] +100
    ├── · +4.882s device.Bumper2.activated [switch] +100
    ╰── · +6.013s device.Bumper1.activated [switch] +100
Checkpoint
After pressing 5 then 1, a new game span roots the tree and shows Ball 1/3 for Player 1. As you knock the ball around the pop bumpers in VPX, a device.BumperN.activated … +100 leaf streams in for each hit and the score climbs 100 at a time. That’s the full loop: VPX simulates the playfield, the switch event crosses the bridge, your rule scores it, and Cade streams light and coil commands back.

Where to go next

You’ve bridged a simulated table end to end. From here it’s the same shape as any Cade table — more devices, more rules.

  • Map the rest of the tableMigrating from Visual Pinball translates each VBScript pattern (targets, drop banks, combos, audio) into HCL.
  • Know the driver — the Visual Pinball driver page documents the supported capabilities, the hybrid flipper model, and every platform "grpc" setting.
  • Add soundAudio & Sound covers spatial audio, so you drop the VPX PlaySound pan/fade math.
  • Build out scoringRecipes has ready-made blocks for multiball, combos, and multipliers.