feat: log the game's high-score report as a gameplay event

When the game builds its high-score table report for Stern's agent
(/api/v1/game/high_score_events - the endpoint the Insider gate already sits
on), the logger writes an HSTD line with all four players' scores. This is
the report the game sends when a game has changed the table, so the line
marks a high-score event with no new per-game RE. The entries themselves
(initials, position) live deeper in the report body and stay uncaptured for
now - that needs locating the report builder or the in-memory table.

Tests: the endpoint trigger and the HSTD line lift into the host harness
(the endpoint pointer is a 32-bit register, so the harness maps its string
below 4 GiB and the tests run on the Linux leg like the port-gate ones).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-22 22:31:15 -04:00
co-authored by Claude Code
parent 95e7317b81
commit 11d42dbb36
4 changed files with 104 additions and 1 deletions
+2
View File
@@ -20,6 +20,7 @@ why, and the game runs stock.
| 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) | | 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) | | 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) | | 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) |
| High-score reports (`HSTD`): the game's own high-score table report, with all players' scores | **built, deploy pending** — fires when the game sends `/api/v1/game/high_score_events` to Stern's agent; the entries (initials) need new per-game RE (below) |
The machine currently runs the v3 build (events, shots, modes, awards, switch 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` / log). The v3 work needed more than a rebuild: the port had no `switch_edge` /
@@ -90,6 +91,7 @@ reboots:
[pad] gamelog: 2026-10-12T20:45:12.123Z MODE END 5 (Island Scene Escape the Swamp) player=1 score=... mode_score=... dur=...ms [pad] gamelog: 2026-10-12T20:45:12.123Z MODE END 5 (Island Scene Escape the Swamp) player=1 score=... mode_score=... dur=...ms
[GAMELOG] 2026-10-12T20:45:12.123Z AWARD EXTRA BALL (id=13) player=1 <- next ball logs as "ball N (EXTRA BALL)" [GAMELOG] 2026-10-12T20:45:12.123Z AWARD EXTRA BALL (id=13) player=1 <- next ball logs as "ball N (EXTRA BALL)"
[GAMELOG] 2026-10-12T20:45:12.123Z AWARD id=<n> player=1 score=... <- every award screen [GAMELOG] 2026-10-12T20:45:12.123Z AWARD id=<n> player=1 score=... <- every award screen
[GAMELOG] 2026-10-12T20:45:12.123Z HSTD high-score report - P1=95,421,190 P2=0 P3=0 P4=0
``` ```
Bus event ids (King Kong 0.97): `game_start`=0x4d, `ball_start`=0x25, Bus event ids (King Kong 0.97): `game_start`=0x4d, `ball_start`=0x25,
+17
View File
@@ -8671,6 +8671,21 @@ static void gl_switch_log(unsigned id, unsigned level, int hit, int playing)
if (fd >= 0) { write(fd, line, len); close(fd); } if (fd >= 0) { write(fd, line, len); close(fd); }
} }
/* GAMELOG: the game's own high-score report (HSTD_REPORT — the /api/v1/game/high_score_events
* the agent gate sees being built): the players' scores as the game reports its table. */
static void gl_hstd_report(void)
{
char tbuf[40], buf[128], one[32];
unsigned p, n = 0;
gl_rfc3339(tbuf, sizeof tbuf);
buf[0] = 0;
for (p = 1; p <= 4; p++) {
pm_commas(one, sizeof one, pm_score(p));
n += (unsigned)pm_snprintf(buf + n, sizeof buf - n, "%sP%u=%s", n ? " " : "", p, one);
}
pm_log("[GAMELOG] %s HSTD high-score report - %s", tbuf, buf);
}
static void on_switch_edge(unsigned *r) static void on_switch_edge(unsigned *r)
{ {
static volatile int said; static volatile int said;
@@ -8998,6 +9013,8 @@ static void on_agent_header(unsigned *r)
const char *s = (const char *)(unsigned long)r[1]; const char *s = (const char *)(unsigned long)r[1];
if (s && s[0] == '/') str_copy(insider_endpoint, sizeof insider_endpoint, s, INSIDER_ENDPOINT_MAX - 1); if (s && s[0] == '/') str_copy(insider_endpoint, sizeof insider_endpoint, s, INSIDER_ENDPOINT_MAX - 1);
else insider_endpoint[0] = 0; else insider_endpoint[0] = 0;
if (insider_blocked(insider_endpoint) == 2) /* the high-score table report */
gl_hstd_report();
} }
static int on_agent_begin(unsigned *r) static int on_agent_begin(unsigned *r)
+3 -1
View File
@@ -1,10 +1,12 @@
#!/bin/sh #!/bin/sh
# log_shipper - tail /dump/*.log and push them to Loki (see shipper/README.md). # log_shipper - tail /dump/*.log and push them to Loki (see shipper/README.md).
# Config comes from the environment; defaults in the binary match the machine. # Config comes from the environment; defaults in the binary match the machine.
# The shipper's own complaints land in /dump/logshipper.log (a few lines a boot).
case "$1" in case "$1" in
start) start)
start-stop-daemon -S -b -m -p /run/logshipper.pid -x /usr/local/bin/logshipper start-stop-daemon -S -b -m -p /run/logshipper.pid -x /bin/sh \
-- -c 'exec /usr/local/bin/logshipper >>/dump/logshipper.log 2>&1'
;; ;;
stop) stop)
start-stop-daemon -K -p /run/logshipper.pid start-stop-daemon -K -p /run/logshipper.pid
+82
View File
@@ -406,3 +406,85 @@ def test_gl_switch_log_writes_the_ports_buttons(tmp_path):
'2026-10-13T12:00:00.000Z SW 62 "RIGHT FLIPPER BUTTON" level=1 hit=1 game=1', '2026-10-13T12:00:00.000Z SW 62 "RIGHT FLIPPER BUTTON" level=1 hit=1 game=1',
'2026-10-13T12:00:00.000Z SW 36 "START BUTTON" level=0 hit=0 game=0', '2026-10-13T12:00:00.000Z SW 36 "START BUTTON" level=0 hit=0 game=0',
], text ], text
# ---- the high-score report event ------------------------------------------------------
_HSTD_PRELUDE = r"""
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <sys/mman.h>
static void event_line(const char *s) { (void)s; }
static void lamp_line(const char *s) { (void)s; }
static void rule_line(const char *s) { (void)s; }
static void port_line(const char *s) { (void)s; }
static void say(const char *fmt, ...) { (void)fmt; }
static void on_agent_header(unsigned *r);
static void gl_rfc3339(char *buf, unsigned cap) { snprintf(buf, cap, "2026-10-22T23:59:01.123Z"); }
int pm_snprintf(char *out, unsigned long cap, const char *fmt, ...)
{
va_list ap;
int n;
va_start(ap, fmt);
n = vsnprintf(out, cap, fmt, ap);
va_end(ap);
return n;
}
void pm_log(const char *fmt, ...)
{
char b[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(b, sizeof b, fmt, ap);
va_end(ap);
printf("PMLOG %s\n", b);
}
static void pm_commas(char *out, unsigned cap, unsigned long long v)
{
snprintf(out, cap, "%llu", (unsigned long long)v);
}
static unsigned long long pm_score(unsigned player) { return player == 1 ? 137615960 : 0; }
int main(int argc, char **argv)
{
static unsigned r[4];
/* the runtime reads the endpoint pointer as a 32-bit register: the path must live below 4 GiB */
char *path = mmap((void *)0x08000000, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
if (path == (char *)-1) { printf("mmap failed\n"); return 1; }
snprintf(path, 4096, "%s", argv[1]);
r[1] = (unsigned)(unsigned long)path;
on_agent_header(r);
return 0;
}
"""
def _hstd_source():
src = _src()
helpers = "\n".join(_lift(src, s) for s in (
"static int str_eq(", "static void str_copy(", "static int hexval(", "static uint64_t number("))
insider = _between(src, "#define INSIDER_ENDPOINT_MAX", "static void on_agent_header(unsigned *r)")
fn = "\n".join(_lift(src, s) for s in (
"static void gl_hstd_report(void)", "static void on_agent_header(unsigned *r)"))
return _HSTD_PRELUDE + helpers + "\n" + insider + "\n" + fn
def test_a_high_score_report_logs_an_hstd_event_with_the_scores(tmp_path):
if sys.platform == "darwin":
pytest.skip("the runtime's pointers are 32-bit registers; darwin arm64 maps nothing "
"below 4 GiB - the Linux leg runs these")
out = _host_run(tmp_path, _hstd_source(),
args=("/api/v1/game/high_score_events",))
assert "HSTD high-score report" in out, out
assert "P1=137615960 P2=0 P3=0 P4=0" in out, out
def test_other_agent_reports_log_no_hstd_event(tmp_path):
if sys.platform == "darwin":
pytest.skip("32-bit pointer registers: see the first HSTD test")
for path in ("/api/v3/game/session_start", "/ingest/v1/game/game_events", "junk", ""):
out = _host_run(tmp_path, _hstd_source(), args=(path,))
assert "HSTD" not in out, "%s logged an HSTD event" % path