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>
175 lines
6.2 KiB
C
175 lines
6.2 KiB
C
/* GAMELOG - a passive gameplay-event logger for Stern Spike 2.
|
|
*
|
|
* Logs, with millisecond timestamps, to /dump/mode.log:
|
|
* EV <event name> - the game's bus events (game start, ball start/end,
|
|
* bonus end, tilt warning, tilt), player, ball, score
|
|
* SHOT <shot name> - every shot the game dispatches while a ball is up
|
|
* SCORES - all players' scores at each bonus end
|
|
* MODE START/END - the game's own modes (Island Scenes and the rest of
|
|
* the port's block_* set), with the score delta earned
|
|
* across the mode
|
|
* AWARD id=<n> - the game's award screens (extra ball = the
|
|
* extra_ball_award value, default 13)
|
|
*
|
|
* Extra balls: an award matching extra_ball_award marks the NEXT ball start
|
|
* as "ball N (EXTRA)" instead of advancing the ball number.
|
|
*
|
|
* The mode never begins(), never blocks, allocates nothing: it only watches.
|
|
*/
|
|
|
|
#include "pad_mode.h"
|
|
|
|
/* -nostdlib: the few libc symbols resolve from the game's own libc at load,
|
|
* exactly as the runtime's and event_probe.c's do. */
|
|
extern int clock_gettime(int, void *);
|
|
|
|
/* the runtime patch exposes the award screens it sees */
|
|
extern unsigned long gl_award_seq(void);
|
|
extern unsigned gl_last_award_id(void);
|
|
|
|
#define CLOCK_MONOTONIC 1
|
|
|
|
struct ts {
|
|
long sec, nsec;
|
|
};
|
|
|
|
static unsigned long ms(void)
|
|
{
|
|
struct ts t;
|
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
|
return (unsigned long)t.sec * 1000ul + (unsigned long)t.nsec / 1000000ul;
|
|
}
|
|
|
|
static unsigned long t0; /* ms at .init, so stamps start near 0 */
|
|
static unsigned ball; /* ball in play, 1-based */
|
|
static unsigned extras_pending; /* extra balls awarded, not yet played */
|
|
static unsigned char in_game;
|
|
static unsigned long award_seq_seen;
|
|
static unsigned extra_ball_award = 13; /* OB_SYS_EXTRA_BALL; port: value extra_ball_award */
|
|
|
|
static int ev_game_start, ev_ball_start, ev_ball_end, ev_bonus_end, ev_tilt_warn, ev_tilt;
|
|
|
|
/* the player's score, comma-separated, into a caller buffer */
|
|
static void score_str(char *out, unsigned cap, unsigned player)
|
|
{
|
|
pm_commas(out, cap, pm_score(player));
|
|
}
|
|
|
|
static void scores_line(const char *tag)
|
|
{
|
|
char buf[128];
|
|
char one[32];
|
|
unsigned p, n = 0;
|
|
|
|
buf[0] = 0;
|
|
for (p = 1; p <= 4; p++) {
|
|
score_str(one, sizeof one, p);
|
|
n += (unsigned)pm_snprintf(buf + n, sizeof buf - n, "%sP%u=%s",
|
|
n ? " " : "", p, one);
|
|
}
|
|
pm_log("[GAMELOG] +%lums %s %s", ms() - t0, tag, buf);
|
|
}
|
|
|
|
static void on_init(void)
|
|
{
|
|
t0 = ms();
|
|
ball = 0;
|
|
in_game = 0;
|
|
extras_pending = 0;
|
|
award_seq_seen = gl_award_seq();
|
|
extra_ball_award = (unsigned)pm_port_value("extra_ball_award", 13);
|
|
ev_game_start = pm_event("game_start");
|
|
ev_ball_start = pm_event("ball_start");
|
|
ev_ball_end = pm_event("ball_end");
|
|
ev_bonus_end = pm_event("bonus_end");
|
|
ev_tilt_warn = pm_event("tilt_warning");
|
|
ev_tilt = pm_event("tilt");
|
|
pm_log("[GAMELOG] up: game %s %s, %d named shots; event ids %d/%d/%d/%d/%d/%d; extra_ball_award=%u",
|
|
pm_game(), pm_version(), pm_shot_count(),
|
|
ev_game_start, ev_ball_start, ev_ball_end,
|
|
ev_bonus_end, ev_tilt_warn, ev_tilt, extra_ball_award);
|
|
}
|
|
|
|
static void on_event(unsigned id)
|
|
{
|
|
const char *name = pm_event_name(id);
|
|
char one[32];
|
|
|
|
if (id == (unsigned)ev_game_start) {
|
|
ball = 0;
|
|
extras_pending = 0;
|
|
in_game = 1;
|
|
pm_log("[GAMELOG] +%lums EV %s - GAME START", ms() - t0,
|
|
name ? name : "game_start");
|
|
} else if (id == (unsigned)ev_ball_start) {
|
|
score_str(one, sizeof one, pm_player());
|
|
if (extras_pending) {
|
|
extras_pending--;
|
|
pm_log("[GAMELOG] +%lums EV %s player=%u ball=%u (EXTRA BALL) score=%s", ms() - t0,
|
|
name ? name : "ball_start", pm_player(), ball, one);
|
|
} else {
|
|
ball++;
|
|
pm_log("[GAMELOG] +%lums EV %s player=%u ball=%u score=%s", ms() - t0,
|
|
name ? name : "ball_start", pm_player(), ball, one);
|
|
}
|
|
} else if (id == (unsigned)ev_ball_end) {
|
|
score_str(one, sizeof one, pm_player());
|
|
pm_log("[GAMELOG] +%lums EV %s player=%u ball=%u score=%s", ms() - t0,
|
|
name ? name : "ball_end", pm_player(), ball, one);
|
|
} else if (id == (unsigned)ev_bonus_end) {
|
|
pm_log("[GAMELOG] +%lums EV %s", ms() - t0, name ? name : "bonus_end");
|
|
scores_line("SCORES");
|
|
} else if (id == (unsigned)ev_tilt_warn) {
|
|
pm_log("[GAMELOG] +%lums EV %s player=%u", ms() - t0,
|
|
name ? name : "tilt_warning", pm_player());
|
|
} else if (id == (unsigned)ev_tilt) {
|
|
pm_log("[GAMELOG] +%lums EV %s player=%u ball=%u", ms() - t0,
|
|
name ? name : "tilt", pm_player(), ball);
|
|
}
|
|
}
|
|
|
|
static void on_shot(unsigned long long shot)
|
|
{
|
|
const char *name;
|
|
char one[32];
|
|
|
|
if (!in_game) /* attract-mode dispatches are noise */
|
|
return;
|
|
name = pm_shot_name(shot);
|
|
score_str(one, sizeof one, pm_player());
|
|
if (name)
|
|
pm_log("[GAMELOG] +%lums SHOT %s player=%u ball=%u score=%s", ms() - t0,
|
|
name, pm_player(), ball, one);
|
|
else
|
|
pm_log("[GAMELOG] +%lums SHOT 0x%llx player=%u ball=%u score=%s", ms() - t0,
|
|
shot, pm_player(), ball, one);
|
|
}
|
|
|
|
static void on_tick(void)
|
|
{
|
|
/* award screens: the runtime hook counts them; log the new ones */
|
|
while (award_seq_seen != gl_award_seq()) {
|
|
award_seq_seen++;
|
|
if (gl_last_award_id() == extra_ball_award) {
|
|
extras_pending++;
|
|
pm_log("[GAMELOG] +%lums AWARD EXTRA BALL (id=%u) player=%u", ms() - t0,
|
|
gl_last_award_id(), pm_player());
|
|
} else {
|
|
char one[32];
|
|
score_str(one, sizeof one, pm_player());
|
|
pm_log("[GAMELOG] +%lums AWARD id=%u player=%u score=%s", ms() - t0,
|
|
gl_last_award_id(), pm_player(), one);
|
|
}
|
|
}
|
|
}
|
|
|
|
static const struct pm_mode gamelog = {
|
|
.name = "GAMELOG",
|
|
.init = on_init,
|
|
.tick = on_tick,
|
|
.event = on_event,
|
|
.shot = on_shot,
|
|
};
|
|
|
|
PM_REGISTER(gamelog);
|