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) |
| 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) |
| 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
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
[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 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,
+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); }
}
/* 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 volatile int said;
@@ -8998,6 +9013,8 @@ static void on_agent_header(unsigned *r)
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);
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)
+3 -1
View File
@@ -1,10 +1,12 @@
#!/bin/sh
# 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.
# The shipper's own complaints land in /dump/logshipper.log (a few lines a boot).
case "$1" in
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)
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 36 "START BUTTON" level=0 hit=0 game=0',
], 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