The v3 runtime's edge logger could never arm: the port had no switch_edge / switch_drain sites, no switch_records/switch_count data and no switch_* record-layout values, and switches_arm() skips a port without switch lines. Located in the King Kong LE 0.97 ELF with the decryptor's portswitch.switch_source() (generation-B framework): switch_edge 0x0055fa04, switch_drain 0x005606e4, switch_records 0x008ecc78, switch_count 0x0093437c; record layout 384/32/24/8/26/28 as on the other generation-B builds. Added LOCKDOWN/START/flipper button switch lines on bits the game's shot dispatch never sends, so the logger arms and the buttons become named shots. Deployed to the machine and verified in play: the boot log arms the drain (switch_edge 0x0055fa04, drain 0x005606e4..0x00560864) and /dump/switch.log logged ~6000 in-game edges, both edges of all four flipper buttons, named. Also fixes the runtime's mangled GAMELOG comment (broke the build) and the switch_names.inc entry count in the README. Co-Authored-By: Claude Code <noreply@anthropic.com>
11 KiB
stern-event-agent
A passive gameplay-event logger for Stern Spike 2 pinball, developed against King Kong LE 0.97. It records the game's own events with millisecond timestamps — the game is not modified, nothing scores, nothing blocks.
The logger is a PAD mode: a small shared object (mode.so) that the game
process loads via LD_PRELOAD. It hooks function sites inside the game, and
every hook is verified against the first two machine instructions of that
function before it is touched — a wrong or missing port hooks nothing, logs
why, and the game runs stock.
Status (as of 2026-10-12)
| Part | State |
|---|---|
Bus events (EV): game/ball/bonus start/end, tilt, all players' scores at bonus end |
deployed on the machine, verified in play |
Shots (SHOT): every named shot dispatched while a ball is up |
deployed on the machine, verified in play |
Stock modes (gamelog: MODE START/END): the game's own 22 modes with score delta |
deployed on the machine, verified in play (mode_score is approximate) |
Award screens (AWARD id=…): every award the game shows, extra-ball tracking |
deployed on the machine, verified in play — except the extra-ball award id is not yet confirmed (below) |
Switch/button log (/dump/switch.log, SW lines with generated names) |
deployed on the machine, verified in play (flippers on both edges; sites located with portswitch.switch_source and added to the port) |
The machine currently runs the v3 build (events, shots, modes, awards, switch
log). The v3 work needed more than a rebuild: the port had no switch_edge /
switch_drain sites or switch_* values, so the runtime's edge logger could
never arm. They were located in the King Kong LE 0.97 ELF with the decryptor's
pinball_decryptor.plugins.stern.portswitch.switch_source() (generation-B
framework) and added to ports/king_kong_le-0.97.port.
Files
| File | What it is |
|---|---|
gamelog.c |
the logger: a passive PAD mode. It never pm_begin()s, never blocks, allocates nothing — it only watches and writes [GAMELOG] lines |
pad_mode_runtime.c |
the PAD Modes SDK runtime, patched: (a) a GAMELOG section — passive stock-mode and award watching, exporting gl_award_seq() / gl_last_award_id() to modes; (b) the per-edge switch logger to /dump/switch.log; (c) the Insider Connected gate made passive. Our exact changes vs upstream: docs/runtime-patch.diff |
switch_names.inc |
King Kong LE 0.97 switch id → name table (104 entries, ids 1–104), #included by the runtime's switch logger. Generated from the game's own data (method: swnames.py in the decryptor repo); fix names here if any are wrong |
pad_mode.h, pad_stock.h |
unmodified SDK headers (needed by the build) |
build_mode.sh |
unmodified SDK build script: compiles the runtime + your mode into one -nostdlib shared object and refuses banned libc calls |
event_probe.c, event_read.py |
the event-bus measurement instrument and its log reader — how bus event ids are identified for another game (MODE_SDK.md, "Measuring events on another game") |
ports/king_kong_le-0.97.port |
the King Kong LE 0.97 port: 121 verified sites, 55 named shots, 6 bus events. Installed on the machine as /usr/local/padmode/game.port |
docs/runtime-patch.diff |
this project's changes to the upstream runtime (base: pinball-asset-decryptor commit 7faf8f08, the last upstream change to that file) |
What gets logged
/dump/mode.log (deployed)
Append-only, millisecond stamps relative to the runtime loading, survives reboots:
[pad] port ...: king_kong_le 0.97, 121 sites, 55 shots <- runtime armed (all hook words verified)
[GAMELOG] up: game king_kong_le 0.97, 55 named shots; event ids 77/37/52/40/200/198; extra_ball_award=13
[GAMELOG] +2408751ms EV game_start - GAME START
[GAMELOG] +2408751ms EV ball_start player=1 ball=1 score=0
[GAMELOG] +2424007ms SHOT X1 player=1 ball=1 score=7,349,830 <- all 55 named shots
[GAMELOG] +2644003ms EV ball_end player=1 ball=1 score=91,153,090
[GAMELOG] +2644003ms EV bonus_end
[GAMELOG] +2644003ms SCORES P1=95,421,190 P2=0 P3=0 P4=0
[GAMELOG] +...ms EV tilt_warning player=1
[GAMELOG] +...ms EV tilt player=1 ball=2
[pad] gamelog: MODE START 5 (Island Scene Escape the Swamp) player=1 score=...
[pad] gamelog: MODE END 5 (Island Scene Escape the Swamp) player=1 score=... mode_score=... dur=...ms
[GAMELOG] +...ms AWARD EXTRA BALL (id=13) player=1 <- next ball logs as "ball N (EXTRA BALL)"
[GAMELOG] +...ms AWARD id=<n> player=1 score=... <- every award screen
Bus event ids (King Kong 0.97): game_start=0x4d, ball_start=0x25,
ball_end=0x34, bonus_end=0x28, tilt_warning=0xc8, tilt=0xc6. This build
has no bus id for game over (the last ball_end/bonus_end is the close)
or for bonus start.
Extra balls: the game re-fires ball_start for an extra ball. The runtime's
award-screen hook counts the awards, and one whose id matches
extra_ball_award (default 13, overridable with a value extra_ball_award
line in the port file) makes the replayed ball log as ball N (EXTRA BALL)
instead of advancing the count.
Mode detail: the runtime polls each of the game's 22 mode objects' ACTIVE
state per tick and logs START/END with the player's score at each edge —
mode_score= is the score delta across the mode (approximate when other
scoring runs simultaneously).
Insider Connected: stock. The upstream runtime vetoes score/session reports
whenever modes are installed; this build makes that gate passive — reports
flow exactly as stock (and are logged once), and the logger adds no scores of
its own. Known cosmetic issue: the boot banner still prints the upstream "score
reports stay on this machine" line — the later passive gate line is the truth.
/dump/switch.log (implemented, not yet deployed)
The runtime's switch-edge handler writes every edge of every switch to
/dump/switch.log — buttons included, attract included:
<ms> SW <id> <NAME> level=<0|1> hit=<0|1> game=<0|1>
level is the switch's raw state after the edge, hit is the game's own
"counts as a hit" verdict, game whether a player is up. Names come from
switch_names.inc; an id not in the table logs ?.
Build
Cross-compile on macOS in a Debian container (armhf, hard-float, -nostdlib):
docker run --rm --platform linux/arm64 -v "$PWD:/work" -w /work debian:12 \
sh -c 'apt-get update -qq && apt-get install -y -qq gcc-arm-linux-gnueabihf >/dev/null \
&& bash build_mode.sh -o mode.so gamelog.c'
build_mode.sh compiles pad_mode_runtime.c + gamelog.c into one object
(~228 KB) and fails the build if any pm_ symbol does not exist or any banned
libc call is made (allocation, threads, sleeping, stdio, processes). The few
libc calls that are used (clock_gettime, open, write, close, …) resolve
from the game's own libc when the object loads.
The event probe builds without the runtime:
build_mode.sh -p -o event_probe.so event_probe.c
Install / deploy (the machine)
The machine (hostname stern-spike2) has root SSH (key-only, port 22). The
logger lives on the rootfs, which is normally read-only:
| Path on the machine | From this repo |
|---|---|
/usr/local/padmode/mode.so |
the built mode.so |
/usr/local/padmode/game.port |
ports/king_kong_le-0.97.port |
/etc/init.d/game_monitor |
boot hook: preloads mode.so into the game only if it exists (stock script kept at game_monitor.stock) |
Deploy a new build:
scp mode.so root@stern-spike2:/tmp/
ssh root@stern-spike2 'mount -o remount,rw /; cp /tmp/mode.so /usr/local/padmode/mode.so;
chmod 755 /usr/local/padmode/mode.so; sync; mount -o remount,ro / || true; reboot'
Live view: ssh root@stern-spike2 'tail -f /dump/mode.log'
Housekeeping: /dump is a 532 MiB rw partition shared with crash dumps and
connectivity logs; mode.log grows a few MB per long session (a few hundred
SHOT lines per ball is normal). Pull it over SSH and rotate as needed;
: > /dump/mode.log truncates it safely while running (same for switch.log
once deployed).
Rollback: rm -rf /usr/local/padmode, restore /etc/init.d/game_monitor.stock,
reboot — the machine is stock again (SSH remains).
Measuring events for another game
event_probe.c hooks the game's event bus (subscribe / dispatch) and its
tick, and logs every registration and per-window dispatch counts to
/dump/event.log. Procedure (the full version is MODE_SDK.md, "Measuring
events on another game"):
build_mode.sh -p -o event_probe.so event_probe.c- Put the target's
subscribe/dispatch/ticksites (address + first two instruction words) in/dump/event.sites— the SDK'sport_words.py(in the decryptor repo) fills and checks those words from the game's ELF. - Preload
event_probe.sowithPAD_MODE_SO(beside the stock runtime — its trampoline keeps the moved words where the runtime still verifies them). - Play; at interesting moments
echo <label> > /dump/event.markto close a window (and open the next);echo <id> > /dump/event.forcestarts/stops a stock mode the way a rule does. event_read.py /dump/event.log— which ids fired in which window, with handlers, priorities and callers.
The ids you name that way become the event lines of a port (the six in
king_kong_le-0.97.port were found this way, in the SDK's emulator).
Unfinished work / known issues
- The extra-ball award id is unconfirmed. Default 13 is a static guess from
the game's object-name tables; every award id is logged, so earn an extra
ball, read the
AWARDline, and pin it down (or override it in the port). - The master SD image does not contain the logger — it was installed over
SSH onto the card in the machine. Making it survive a re-flash needs a
debugfs inject of
mode.so+game.port+ the hookedgame_monitorinto the master image. - No network streaming yet. Logs are pulled over SSH; the plan is for the
mode to ship them itself (
socket()is not on the build's banned list). - Cosmetic: the boot banner still prints the upstream "score reports stay on this machine" line (see Insider Connected above).
mode_scoreis an approximation (see above).
Provenance
Built on the PAD Modes SDK
(pinball-asset-decryptor/tools/spike2_emu/modes/sdk/ — the
mattlqx/pinball-asset-decryptor Gitea repo, itself a fork of
davidvanderburgh/pinball-asset-decryptor; the SDK docs there — MODE_SDK.md,
MODE_PARAMETERS.md, MODE_LIMITS.md — apply to everything in this repo).
Everything in this repo is unmodified SDK content except:
gamelog.c(new)switch_names.inc(new)pad_mode_runtime.c(upstream runtime + the patch indocs/runtime-patch.diff)README.md
ports/king_kong_le-0.97.port, event_probe.c, event_read.py,
build_mode.sh and the headers are the SDK's, unmodified (the port is
emulator-proven by the SDK repo authors).
The machine's SSH setup (dropbear + sftp-server on the Spike 2) is documented
separately in the local pinball-assets/README.md and is not part of this
project — this logger uses that SSH, it does not provide it.