Files
stern-event-agent/docs/runtime-patch.diff
T
mattlqxandClaude Code abc881f913 feat: add passive GAMELOG event logger for Spike 2 King Kong LE 0.97
Snapshot of the stern-event-agent work (as of 2026-10-12):

- gamelog.c: passive PAD mode logging bus events (game/ball/bonus/tilt),
  all named shots and all-players' scores to /dump/mode.log
- pad_mode_runtime.c: the SDK runtime + a GAMELOG section (passive
  stock-mode and award watching, gl_award_seq()/gl_last_award_id()),
  a per-edge switch logger to /dump/switch.log (switch_names.inc), and
  a passive Insider Connected gate; docs/runtime-patch.diff shows the
  exact changes vs the upstream runtime
- event_probe.c / event_read.py: event-bus measurement tooling for
  identifying bus event ids on another game
- ports/king_kong_le-0.97.port: 121 verified sites, 55 named shots, 6
  bus events (emulator-proven by the SDK repo authors)

Deployed and verified on the machine: events, shots, stock modes and
awards. The /dump/switch.log feature is implemented but NOT yet
deployed or verified, and the extra-ball award id (default 13) is
unconfirmed. See README.md for build/install/log usage and unfinished
work.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-12 13:57:12 -04:00

161 lines
7.2 KiB
Diff

--- /Users/matt/src/pinball-asset-decryptor/tools/spike2_emu/modes/sdk/pad_mode_runtime.c 2026-10-11 18:38:42
+++ /Users/matt/src/pinball-assets/.ssh-build/sdk-patched/pad_mode_runtime.c 2026-10-12 12:04:22
@@ -7914,6 +7914,90 @@
static void stock_tick(void); /* item 160 */
static void magnet_tick(void); /* PAD-381 */
static void scoop_tick(void); /* PAD-381 */
+
+/* ---- GAMELOG: passive stock-mode and award watching*/
+#ifndef O_WRONLY
+#define O_WRONLY 1
+#define O_CREAT 0100
+#define O_APPEND 02000
+#endif for the gameplay logger.
+ * Added on top of the stock runtime (2026-10-12): logs the game's own modes
+ * (Island Scenes and the rest of the port's block_* set) starting and ending -
+ * with the player's score at each edge, so the mode's score total is the
+ * difference - and every award screen the game shows (extra ball, special,
+ * replay...), exposed to modes through gl_award_seq()/gl_last_award_id(). */
+static unsigned long gl_start_ms[BLOCK_IDS];
+static unsigned long long gl_start_score[BLOCK_IDS];
+static unsigned gl_start_player[BLOCK_IDS];
+static unsigned char gl_running[BLOCK_IDS];
+static unsigned gl_award_v;
+static unsigned long gl_award_n;
+
+unsigned long gl_award_seq(void) { return gl_award_n; }
+unsigned gl_last_award_id(void) { return gl_award_v; }
+
+static void gamelog_mode_log(const char *kind, unsigned id, unsigned player,
+ unsigned long long score, unsigned long dur)
+{
+ char key[24];
+ const char *nm;
+ pm_snprintf(key, sizeof key, "block_name_%u", id);
+ nm = pm_port_text(key);
+ if (dur)
+ say("gamelog: MODE %s %u (%s) player=%u score=%llu mode_score=%llu dur=%lums",
+ kind, id, nm ? nm : "?", player, score,
+ score > gl_start_score[id] ? score - gl_start_score[id] : 0, dur);
+ else
+ say("gamelog: MODE %s %u (%s) player=%u score=%llu",
+ kind, id, nm ? nm : "?", player, score);
+}
+
+static void gamelog_stock_tick(void)
+{
+ unsigned id;
+ if (!pm_in_game()) {
+ for (id = 0; id < BLOCK_IDS; id++)
+ if (gl_running[id]) {
+ gamelog_mode_log("END", id, gl_start_player[id],
+ pm_score(gl_start_player[id]), pm_ms() - gl_start_ms[id]);
+ gl_running[id] = 0;
+ }
+ return;
+ }
+ for (id = 0; id < BLOCK_IDS; id++) {
+ const unsigned *o = (const unsigned *)(unsigned long)block_obj[id];
+ int active;
+ if (!o || !bm_has(block_named, id) || !wizm_vfn(o, "stock_slot_active")) continue;
+ active = ((int (*)(const void *))(unsigned long)wizm_vfn(o, "stock_slot_active"))(o) & 0xff;
+ if (active && !gl_running[id]) {
+ gl_start_ms[id] = pm_ms();
+ gl_start_player[id] = pm_player();
+ gl_start_score[id] = pm_score(pm_player());
+ gamelog_mode_log("START", id, gl_start_player[id], gl_start_score[id], 0);
+ } else if (!active && gl_running[id]) {
+ gamelog_mode_log("END", id, gl_start_player[id],
+ pm_score(gl_start_player[id]), pm_ms() - gl_start_ms[id]);
+ }
+ gl_running[id] = (unsigned char)(active != 0);
+ }
+}
+
+/* the game's award screens (extra ball, special, replay...): logged, never refused */
+static void on_award_screen(unsigned *r)
+{
+ gl_award_v = r[0];
+ gl_award_n++;
+ say("gamelog: AWARD id=%u player=%u score=%llu", r[0], pm_player(), pm_score(pm_player()));
+}
+
+static void gamelog_arm(void)
+{
+ struct site *as = site("award_screen");
+ if (as && as->ok && hook(as->addr, on_award_screen))
+ say("gamelog: award screen hooked at 0x%08x", as->addr);
+ else
+ say("gamelog: award screen NOT hooked");
+}
static void on_tick(unsigned *r)
{
@@ -7948,6 +8032,7 @@
roster_deferred_tick();
stock_generic_tick(); /* item 164: the game's base play, for the mode table route */
stock_tick(); /* item 160: the game's own rules' counts-as (after the modes: a probe wraps first) */
+ gamelog_stock_tick(); /* GAMELOG: the game's own modes' start/end edges */
lamps_tick(); /* named inserts: their patterns (lights section) */
}
@@ -8547,6 +8632,21 @@
mask = edge.mode_mask ? *(const unsigned short *)(unsigned long)edge.mode_mask : 0u;
hit = edge_is_hit(level, flags, pol, edge.mode_mask != 0, mask);
playing = pm_in_game(); /* attract, a tilt: no player up for a mode to score */
+ /* GAMELOG: every edge to /dump/switch.log (buttons included, attract included) */
+ {
+ static const struct { unsigned id; const char *name; } gl_sw[] = {
+#include "switch_names.inc"
+ };
+ unsigned k;
+ char nm[40] = "?", line[160];
+ int fd, len;
+ for (k = 0; k < sizeof gl_sw / sizeof gl_sw[0]; k++)
+ if (gl_sw[k].id == id) { pm_snprintf(nm, sizeof nm, "%s", gl_sw[k].name); break; }
+ len = pm_snprintf(line, sizeof line, "%lu SW %u %s level=%u hit=%d game=%d\n",
+ pm_ms(), id, nm, level & 1u, hit, playing);
+ fd = open("/dump/switch.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
+ if (fd >= 0) { write(fd, line, len); close(fd); }
+ }
if (edge_log_left > 0 && __sync_sub_and_fetch(&edge_log_left, 1) >= 0)
say("edge: switch %u level %u (flags 0x%04x, polarity 0x%04x, mode mask 0x%04x, in game %d)%s", id, level & 1u,
flags, pol, mask, playing, hit && playing ? " - a hit" : "");
@@ -8781,18 +8881,17 @@
static int on_agent_begin(unsigned *r)
{
- int i = insider_blocked(insider_endpoint);
- unsigned n;
- (void)r;
- if (!i) return 0;
- n = ++insider_dropped[i - 1];
- if (n == 1)
- say("insider: %s is not sent to Insider Connected (the game logs a failed message and plays on)",
- insider_endpoint);
- else if ((n & 15) == 0)
- say("insider: %s not sent, %u times", insider_endpoint, n);
+ /* GAMELOG build: passive gate - Insider Connected stays fully stock. Every endpoint the
+ * game sends is logged once (the machine's own reports are genuine; this build adds no
+ * scores of its own), nothing is refused. */
+ (void)r;
+ if (insider_endpoint[0] && insider_blocked(insider_endpoint) && insider_dropped[0] < 1) {
+ insider_dropped[0] = 1;
+ say("insider: passive gate - %s (and all reports) go to Insider Connected as stock",
+ insider_endpoint);
+ }
insider_endpoint[0] = 0;
- return 1;
+ return 0;
}
/* 1 = the gate is up. 0 = this port cannot keep scores off Insider Connected: NOTHING is hooked
@@ -9266,6 +9365,7 @@
}
display_arm(); /* item 154 display: the clip_play hook, display priority */
block_arm(); /* PAD-347: a mode may keep the game's modes from starting */
+ gamelog_arm(); /* GAMELOG: watch the game's award screens */
backdrop_arm(); /* hud-layers: a clip behind the HUD */
ck_arm(); /* PAD-530: a mode's clip comes back after the game's */
status_bar_arm(); /* PAD-521: the game's own status bar */