feat: multi-game port selection — the machine says which game it is on
The runtime tried one port file (game.port) and refused it if the game's words did not match. Now it tries every *.port in /usr/local/padmode/ports and /dump/ports (then the single files, the old layout), resetting the port tables between candidates, and keeps the first whose core sites verify: a wrong candidate hooks nothing, so trying it is safe, and the port that verifies names the game (pm_game/pm_version). The switch log's buttons and names come from the port's switch lines now — the baked-in King Kong button list and switch_names.inc are gone (the port lines carry the same ids and names, so King Kong's switch.log is unchanged). Boot log: each refused candidate says its path and the game/version it declares; no candidates at all says so; a game update whose port is not deployed logs the refusal and runs stock. tests/test_multi_game.py lifts the new code verbatim and compiles it for the host: selection among candidates, table reset between them, the getdents64 *.port scan (it caught an inverted suffix test and an off-by-one), and the port-driven switch log. The address-dependent tests need a Linux host (the port's site addresses are 32 bits; darwin arm64 maps nothing below 4 GiB). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+131
-46
@@ -17,6 +17,7 @@ extern long read(int, void *, __SIZE_TYPE__);
|
||||
extern long write(int, const void *, __SIZE_TYPE__);
|
||||
extern int close(int);
|
||||
extern int unlink(const char *);
|
||||
extern void *memset(void *, int, __SIZE_TYPE__);
|
||||
extern int vsnprintf(char *, __SIZE_TYPE__, const char *, va_list);
|
||||
extern int clock_gettime(int, void *);
|
||||
extern int mprotect(void *, __SIZE_TYPE__, int);
|
||||
@@ -148,7 +149,9 @@ void pm_commas(char *out, unsigned cap, uint64_t v)
|
||||
* switch <id> <mask> <name ...> a switch whose hit is a shot: the switches section below
|
||||
* callout <role> <id> scene <role> <40-hex id>
|
||||
* text <name> <rest of line> anything else a mode may want
|
||||
* Looked for at /usr/local/padmode/game.port (a card) then /dump/game.port (the rig). */
|
||||
* The candidates are every *.port in a ports directory (the machine says which game it is on by
|
||||
* which port's sites verify - port_select), then the single files, the old one-game layout. */
|
||||
static const char *const PORT_DIRS[] = { "/usr/local/padmode/ports", "/dump/ports" };
|
||||
static const char *const PORT_FILES[] = { "/usr/local/padmode/game.port", "/dump/game.port" };
|
||||
|
||||
/* The port is read in PORT_CHUNK pieces, a line at a time, up to PORT_MAX bytes. It was ONE read
|
||||
@@ -175,7 +178,7 @@ struct text { char name[40]; char value[160]; };
|
||||
struct switch_shot { unsigned id; uint64_t mask; };
|
||||
|
||||
static struct {
|
||||
char game[32], version[24], path[40];
|
||||
char game[32], version[24], path[128];
|
||||
struct site site[N_SITES]; int n_site;
|
||||
struct named data[N_DATA]; int n_data;
|
||||
struct named value[N_VALUES]; int n_value;
|
||||
@@ -291,23 +294,16 @@ static void port_text_line(char *line)
|
||||
if (*s && *s != '#') port_line(s);
|
||||
}
|
||||
|
||||
/* The port, a chunk at a time: a line longer than 255 characters is cut, a port longer than
|
||||
/* One port file, a chunk at a time: a line longer than 255 characters is cut, a port longer than
|
||||
* PORT_MAX is read to its last whole line before PORT_MAX (port.too_long says so). */
|
||||
static int port_load(void)
|
||||
static int port_read(const char *path)
|
||||
{
|
||||
static char buf[PORT_CHUNK];
|
||||
char line[256];
|
||||
long n, i, j = 0, total = 0;
|
||||
unsigned k;
|
||||
int fd = -1;
|
||||
for (k = 0; k < sizeof PORT_FILES / sizeof PORT_FILES[0]; k++) {
|
||||
fd = open(PORT_FILES[k], O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
str_copy(port.path, sizeof port.path, PORT_FILES[k], 40);
|
||||
break;
|
||||
}
|
||||
}
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return 0;
|
||||
str_copy(port.path, sizeof port.path, path, sizeof port.path);
|
||||
while (total < PORT_MAX && (n = read(fd, buf, sizeof buf)) > 0) {
|
||||
if (n > PORT_MAX - total) n = PORT_MAX - total;
|
||||
total += n;
|
||||
@@ -434,8 +430,7 @@ static void maps_read(void)
|
||||
/* 1 when [addr, addr + len) lies inside mappings with every bit of `how`: one mapping, or a run of
|
||||
* CONTIGUOUS ones (another preloaded object's mprotect of a page for its trampoline splits the game's
|
||||
* code mapping in three, and a site's 8 bytes may cross the split), each with every bit (tests lift it
|
||||
* verbatim) */
|
||||
static int maps_has(unsigned long addr, unsigned long len, unsigned how)
|
||||
* verbatim) */static int maps_has(unsigned long addr, unsigned long len, unsigned how)
|
||||
{
|
||||
unsigned long at = addr, end = addr + len;
|
||||
int i, steps;
|
||||
@@ -450,9 +445,15 @@ static int maps_has(unsigned long addr, unsigned long len, unsigned how)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_game_process(unsigned addr)
|
||||
/* 1 when this process has the game's own executable code mapped: the launcher passes the preload
|
||||
* to every child the game spawns, and those have nothing of the game's mapped. Which game exactly
|
||||
* (that a port's tick lies in it) the port gate says, per site. */
|
||||
static int game_mapped(void)
|
||||
{
|
||||
return maps_has(addr, 8, MAP_R | MAP_X | MAP_GAME);
|
||||
int i;
|
||||
for (i = 0; i < n_maps; i++)
|
||||
if ((maps[i].how & (MAP_R | MAP_X | MAP_GAME)) == (MAP_R | MAP_X | MAP_GAME)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- rule 2: the words, followed through another preloaded object's trampoline --------
|
||||
@@ -7899,6 +7900,7 @@ extern const struct pm_mode *const __stop_pm_modes[] __attribute__((weak, visibi
|
||||
* callbacks not overlapping rest on this, so it is measured on every boot, not assumed. */
|
||||
extern long syscall(long, ...);
|
||||
#define SYS_GETTID 224 /* ARM EABI */
|
||||
#define SYS_GETDENTS64 217 /* ARM EABI; the same number on x86_64 */
|
||||
|
||||
static void note_thread(const char *what, int *said)
|
||||
{
|
||||
@@ -8645,6 +8647,30 @@ static int edge_is_hit(unsigned level, unsigned flags, unsigned polarity, int ha
|
||||
|
||||
static long edge_log_left; /* value switch_edge_log N: log the first N edges (proving a port) */
|
||||
|
||||
/* GAMELOG's switch log: every edge of a switch the port's `switch` lines name, with the port's
|
||||
* own name for it (the buttons - on King Kong the lockdown, start and two flipper buttons). The
|
||||
* runtime carries no per-game button table: a port that names none logs nothing. */
|
||||
static const char *GL_SWITCH_LOG = "/dump/switch.log";
|
||||
static void gl_switch_log(unsigned id, unsigned level, int hit, int playing)
|
||||
{
|
||||
char tbuf[40], nm[40] = "?", line[160];
|
||||
int i, j, fd, len, want = 0;
|
||||
for (i = 0; i < port.n_switch && !want; i++)
|
||||
if (port.sw[i].id == id)
|
||||
for (j = 0; j < port.n_shot; j++)
|
||||
if (port.shot[j].mask == port.sw[i].mask) {
|
||||
pm_snprintf(nm, sizeof nm, "%s", port.shot[j].name);
|
||||
want = 1;
|
||||
break;
|
||||
}
|
||||
if (!want) return;
|
||||
gl_rfc3339(tbuf, sizeof tbuf);
|
||||
len = pm_snprintf(line, sizeof line, "%s SW %u \"%s\" level=%u hit=%d game=%d\n",
|
||||
tbuf, id, nm, level & 1u, hit, playing);
|
||||
fd = open(GL_SWITCH_LOG, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
if (fd >= 0) { write(fd, line, len); close(fd); }
|
||||
}
|
||||
|
||||
static void on_switch_edge(unsigned *r)
|
||||
{
|
||||
static volatile int said;
|
||||
@@ -8665,29 +8691,7 @@ static void on_switch_edge(unsigned *r)
|
||||
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: the buttons a human presses, to /dump/switch.log (34 lockdown, 36 start, 62/63 the
|
||||
* flipper buttons). The upper-flipper button switches (64/65) are wired in parallel with the
|
||||
* main flipper buttons on this cabinet, so they are left out with every non-button switch. */
|
||||
{
|
||||
static const unsigned gl_buttons[] = { 34, 36, 62, 63 };
|
||||
static const struct { unsigned id; const char *name; } gl_sw[] = {
|
||||
#include "switch_names.inc"
|
||||
};
|
||||
unsigned k;
|
||||
char tbuf[40], nm[40] = "?", line[160];
|
||||
int fd, len, want = 0;
|
||||
for (k = 0; k < sizeof gl_buttons / sizeof gl_buttons[0]; k++)
|
||||
if (gl_buttons[k] == id) { want = 1; break; }
|
||||
if (want) {
|
||||
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; }
|
||||
gl_rfc3339(tbuf, sizeof tbuf);
|
||||
len = pm_snprintf(line, sizeof line, "%s SW %u \"%s\" level=%u hit=%d game=%d\n",
|
||||
tbuf, 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); }
|
||||
}
|
||||
}
|
||||
gl_switch_log(id, level, hit, playing); /* GAMELOG: the port's buttons, to /dump/switch.log */
|
||||
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" : "");
|
||||
@@ -8834,6 +8838,82 @@ static int site_check(struct site *x)
|
||||
return x->ok;
|
||||
}
|
||||
|
||||
/* Everything a port's lines fill: emptied before each candidate, so one port's lines never leak
|
||||
* into the next one's tables. */
|
||||
static void port_reset(void)
|
||||
{
|
||||
memset(&port, 0, sizeof port);
|
||||
can = 0;
|
||||
n_event_names = n_site_events = 0;
|
||||
n_lamps = 0;
|
||||
n_stock_rules = 0;
|
||||
}
|
||||
|
||||
/* Which port is this game's? Try each candidate in order: read it, then the gate. A port whose
|
||||
* core sites do not match this build's words hooks nothing, so a wrong candidate is safe to try -
|
||||
* and the first that verifies IS the game's port, its own game/version lines naming the game.
|
||||
* 0 when nothing matched (every refusal said in the log): the game runs stock. */
|
||||
static int port_gate(void);
|
||||
static int port_select(const char *const *files, int n)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < n; i++) {
|
||||
port_reset();
|
||||
if (!port_read(files[i])) continue;
|
||||
if (port_gate()) return 1;
|
||||
say("port %s: says %s %s - not this game's port, skipped", files[i], port.game, port.version);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- the ports directories -----------------------------------------------------------
|
||||
* The candidates, in order: every *.port in a ports directory, then the single files. Read with
|
||||
* the getdents64 syscall by hand: no DIR, no allocation, like everything here. */
|
||||
|
||||
/* the kernel's getdents64 record (linux/dirent.h's is not the same on every libc) */
|
||||
struct port_dirent { unsigned long long ino; long long off; unsigned short reclen; unsigned char type; char name[]; };
|
||||
|
||||
/* the *.port names in one getdents64 buffer, appended to cand as <dir>/<name>; the new count */
|
||||
static long ports_scan_buf(char *buf, long len, const char *dir, char (*cand)[128], long cap, long n)
|
||||
{
|
||||
const unsigned long noff = __builtin_offsetof(struct port_dirent, name);
|
||||
long at = 0;
|
||||
while (at + (long)sizeof(struct port_dirent) <= len) {
|
||||
struct port_dirent *d = (struct port_dirent *)(void *)(buf + at);
|
||||
unsigned long nl = 0;
|
||||
if (!d->reclen || at + d->reclen > len) break;
|
||||
while (noff + nl < d->reclen && d->name[nl]) nl++;
|
||||
if (d->type == 8 /* DT_REG */ && nl > 5 && str_eq(d->name + nl - 5, ".port") && n < cap) {
|
||||
pm_snprintf(cand[n], 128, "%s/%s", dir, d->name);
|
||||
n++;
|
||||
}
|
||||
at += d->reclen;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
#define PORT_CAND_MAX 16
|
||||
|
||||
/* every candidate port file on the machine, in the order port_select tries them */
|
||||
static long ports_scan(char (*cand)[128], long cap)
|
||||
{
|
||||
static char buf[4096];
|
||||
long n = 0, i;
|
||||
for (i = 0; i < (long)(sizeof PORT_DIRS / sizeof PORT_DIRS[0]) && n < cap; i++) {
|
||||
int fd = open(PORT_DIRS[i], O_RDONLY);
|
||||
if (fd < 0) continue;
|
||||
for (;;) {
|
||||
long k = syscall(SYS_GETDENTS64, fd, buf, sizeof buf);
|
||||
if (k <= 0) break;
|
||||
n = ports_scan_buf(buf, k, PORT_DIRS[i], cand, cap, n);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
for (i = 0; i < (long)(sizeof PORT_FILES / sizeof PORT_FILES[0]) && n < cap; i++)
|
||||
str_copy(cand[n++], 128, PORT_FILES[i], 128);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The core sites first: when one is missing, outside the game's code or not this build's, no other
|
||||
* site is read and the port is refused. Then every other site, each switching off only its own
|
||||
* capability. 1 = this game's port. (tests lift it verbatim) */
|
||||
@@ -9325,23 +9405,28 @@ static void sndm_arm(void)
|
||||
__attribute__((constructor))
|
||||
static void pad_mode_start(void)
|
||||
{
|
||||
static char cand[PORT_CAND_MAX][128];
|
||||
static const char *files[PORT_CAND_MAX];
|
||||
const struct pm_mode *m;
|
||||
struct site *tick;
|
||||
long n, i;
|
||||
int modes = 0;
|
||||
if (!port_load()) return; /* no port: stay out of the way */
|
||||
tick = site("tick");
|
||||
if (!tick) return;
|
||||
maps_read(); /* the process's mappings, read once */
|
||||
if (!is_game_process(tick->addr)) return;
|
||||
if (!game_mapped()) return; /* the launcher's children: not the game */
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
log_fd = open("/dump/mode.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
n = ports_scan(cand, PORT_CAND_MAX);
|
||||
if (!n) {
|
||||
say("ports: none found - no *.port in the ports directories and no game.port");
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < n; i++) files[i] = cand[i];
|
||||
if (!port_select(files, (int)n)) return; /* nothing matched: the game runs stock */
|
||||
say("port %s: %s %s, %d sites, %d shots", port.path, port.game, port.version, port.n_site, port.n_shot);
|
||||
if (port.too_long) say("port: longer than %d bytes - the lines after that are not read", PORT_MAX);
|
||||
if (port.dropped)
|
||||
say("port: %d line(s) not read - a table is full (sites %d of %d, data %d of %d, values %d of %d, shots %d of %d, switches %d of %d)",
|
||||
port.dropped, port.n_site, N_SITES, port.n_data, N_DATA, port.n_value, N_VALUES, port.n_shot, N_SHOTS,
|
||||
port.n_switch, N_SWITCHES);
|
||||
if (!port_gate()) return;
|
||||
clipv_arm(); /* PAD-446: before the score gate - a clip variant scores nothing */
|
||||
sndm_arm(); /* PAD-494: nor does a music mode */
|
||||
if (clipv_only || sndm_only) {
|
||||
|
||||
Reference in New Issue
Block a user