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:
@@ -1,3 +1,4 @@
|
|||||||
# build artifacts
|
# build artifacts
|
||||||
*.so
|
*.so
|
||||||
*.o
|
*.o
|
||||||
|
__pycache__/
|
||||||
|
|||||||
@@ -33,12 +33,37 @@ framework) and added to `ports/king_kong_le-0.97.port`.
|
|||||||
| File | What it is |
|
| File | What it is |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `gamelog.c` | the logger: a passive PAD mode. It never `pm_begin()s`, never blocks, allocates nothing — it only watches and writes `[GAMELOG]` lines |
|
| `gamelog.c` | the logger: a passive PAD mode. It never `pm_begin()s`, never blocks, allocates nothing — it only watches and writes `[GAMELOG]` lines |
|
||||||
| `pad_mode_runtime.c` | the PAD Modes SDK runtime, **patched**: (a) a GAMELOG section — passive stock-mode and award watching, exporting `gl_award_seq()` / `gl_last_award_id()` to modes; (b) the per-edge switch logger to `/dump/switch.log`; (c) the Insider Connected gate made passive. Our exact changes vs upstream: `docs/runtime-patch.diff` |
|
| `pad_mode_runtime.c` | the PAD Modes SDK runtime, **patched**: (a) a GAMELOG section — passive stock-mode and award watching, exporting `gl_award_seq()` / `gl_last_award_id()` to modes; (b) the per-edge switch logger to `/dump/switch.log`, named by the port's `switch` lines; (c) the Insider Connected gate made passive; (d) multi-game port selection (`port_select` — below). Our exact changes vs upstream: `docs/runtime-patch.diff` |
|
||||||
| `switch_names.inc` | King Kong LE 0.97 switch id → name table (104 entries, ids 1–104), `#include`d by the runtime's switch logger. Generated from the game's own data (method: `swnames.py` in the decryptor repo); fix names here if any are wrong |
|
|
||||||
| `pad_mode.h`, `pad_stock.h` | unmodified SDK headers (needed by the build) |
|
| `pad_mode.h`, `pad_stock.h` | unmodified SDK headers (needed by the build) |
|
||||||
| `build_mode.sh` | unmodified SDK build script: compiles the runtime + your mode into one `-nostdlib` shared object and refuses banned libc calls |
|
| `build_mode.sh` | unmodified SDK build script: compiles the runtime + your mode into one `-nostdlib` shared object and refuses banned libc calls |
|
||||||
| `event_probe.c`, `event_read.py` | the event-bus measurement instrument and its log reader — how bus event ids are identified for another game (MODE_SDK.md, "Measuring events on another game") |
|
| `event_probe.c`, `event_read.py` | the event-bus measurement instrument and its log reader — how bus event ids are identified for another game (MODE_SDK.md, "Measuring events on another game") |
|
||||||
| `ports/king_kong_le-0.97.port` | the King Kong LE 0.97 port: 121 verified sites, 55 named shots, 6 bus events. Installed on the machine as `/usr/local/padmode/game.port` |
|
| `tests/test_multi_game.py` | host-compiled tests for the port selection, the ports-directory scan and the switch log (they lift the runtime's own code verbatim; the address-dependent ones need Linux — run them in the Debian container below) |
|
||||||
|
| `ports/king_kong_le-0.97.port` | the King Kong LE 0.97 port: 121 verified sites, 55 named shots, 6 bus events. Installed on the machine as `/usr/local/padmode/ports/king_kong_le-0.97.port` |
|
||||||
|
|
||||||
|
## Multiple games
|
||||||
|
|
||||||
|
One `mode.so` runs on any game that has a port. The machine says which game it is
|
||||||
|
on by which port's sites verify — the runtime tries every candidate in order and
|
||||||
|
keeps the first whose core functions match the running build's instruction words
|
||||||
|
(the same gate that keeps a wrong port from hooking anything):
|
||||||
|
|
||||||
|
1. every `*.port` in `/usr/local/padmode/ports/` and `/dump/ports/` (name order
|
||||||
|
is the directory's; only the right build's port can verify, so the order
|
||||||
|
does not matter), then
|
||||||
|
2. the single files `/usr/local/padmode/game.port` and `/dump/game.port`, the
|
||||||
|
old one-game layout.
|
||||||
|
|
||||||
|
The boot log says what happened:
|
||||||
|
|
||||||
|
```
|
||||||
|
[pad] port /usr/local/padmode/ports/godzilla_le-1.16.port: says godzilla_le 1.16 - not this game's port, skipped
|
||||||
|
[pad] port /usr/local/padmode/ports/king_kong_le-0.97.port: king_kong_le 0.97, 121 sites, 55 shots
|
||||||
|
```
|
||||||
|
|
||||||
|
A game update changes the words, so its old port stops matching — the machine
|
||||||
|
logs the refusal and runs stock until the new game version's port is deployed.
|
||||||
|
The switch log's buttons and their names come from the port's `switch` lines;
|
||||||
|
a port that names none logs no switch edges.
|
||||||
| `docs/runtime-patch.diff` | this project's changes to the upstream runtime (base: `pinball-asset-decryptor` commit `7faf8f08`, the last upstream change to that file) |
|
| `docs/runtime-patch.diff` | this project's changes to the upstream runtime (base: `pinball-asset-decryptor` commit `7faf8f08`, the last upstream change to that file) |
|
||||||
|
|
||||||
## What gets logged
|
## What gets logged
|
||||||
@@ -100,9 +125,9 @@ the main flipper buttons on this cabinet, so they are left out:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`level` is the switch's raw state after the edge, `hit` is the game's own
|
`level` is the switch's raw state after the edge, `hit` is the game's own
|
||||||
"counts as a hit" verdict, `game` whether a player is up. The logged ids are
|
"counts as a hit" verdict, `game` whether a player is up. The logged ids and
|
||||||
the `gl_buttons[]` list in the runtime's switch-edge handler; names come from
|
names are the port's `switch` lines (on King Kong: 34 lockdown, 36 start,
|
||||||
`switch_names.inc`.
|
62/63 the flipper buttons).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -123,6 +148,15 @@ from the game's own libc when the object loads.
|
|||||||
The event probe builds without the runtime:
|
The event probe builds without the runtime:
|
||||||
`build_mode.sh -p -o event_probe.so event_probe.c`
|
`build_mode.sh -p -o event_probe.so event_probe.c`
|
||||||
|
|
||||||
|
The port-selection tests run on any Linux host (the address-dependent ones skip
|
||||||
|
on macOS, whose arm64 maps nothing below 4 GiB):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --platform linux/arm64 -v "$PWD:/work" -w /work debian:12 \
|
||||||
|
sh -c 'apt-get update -qq && apt-get install -y -qq python3 python3-pytest gcc >/dev/null \
|
||||||
|
&& python3 -m pytest tests/test_multi_game.py -q'
|
||||||
|
```
|
||||||
|
|
||||||
## Install / deploy (the machine)
|
## Install / deploy (the machine)
|
||||||
|
|
||||||
The machine (hostname `stern-spike2`) has root SSH (key-only, port 22). The
|
The machine (hostname `stern-spike2`) has root SSH (key-only, port 22). The
|
||||||
@@ -131,7 +165,7 @@ logger lives on the rootfs, which is normally read-only:
|
|||||||
| Path on the machine | From this repo |
|
| Path on the machine | From this repo |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/usr/local/padmode/mode.so` | the built `mode.so` |
|
| `/usr/local/padmode/mode.so` | the built `mode.so` |
|
||||||
| `/usr/local/padmode/game.port` | `ports/king_kong_le-0.97.port` |
|
| `/usr/local/padmode/ports/king_kong_le-0.97.port` | `ports/king_kong_le-0.97.port` (one file per game; see "Multiple games") |
|
||||||
| `/etc/init.d/game_monitor` | boot hook: preloads `mode.so` into the game **only if it exists** (stock script kept at `game_monitor.stock`) |
|
| `/etc/init.d/game_monitor` | boot hook: preloads `mode.so` into the game **only if it exists** (stock script kept at `game_monitor.stock`) |
|
||||||
|
|
||||||
Deploy a new build:
|
Deploy a new build:
|
||||||
@@ -200,8 +234,8 @@ Built on the PAD Modes SDK
|
|||||||
Everything in this repo is unmodified SDK content except:
|
Everything in this repo is unmodified SDK content except:
|
||||||
|
|
||||||
- `gamelog.c` (new)
|
- `gamelog.c` (new)
|
||||||
- `switch_names.inc` (new)
|
|
||||||
- `pad_mode_runtime.c` (upstream runtime + the patch in `docs/runtime-patch.diff`)
|
- `pad_mode_runtime.c` (upstream runtime + the patch in `docs/runtime-patch.diff`)
|
||||||
|
- `tests/test_multi_game.py` (new)
|
||||||
- `README.md`
|
- `README.md`
|
||||||
|
|
||||||
`ports/king_kong_le-0.97.port`, `event_probe.c`, `event_read.py`,
|
`ports/king_kong_le-0.97.port`, `event_probe.c`, `event_read.py`,
|
||||||
|
|||||||
+131
-46
@@ -17,6 +17,7 @@ extern long read(int, void *, __SIZE_TYPE__);
|
|||||||
extern long write(int, const void *, __SIZE_TYPE__);
|
extern long write(int, const void *, __SIZE_TYPE__);
|
||||||
extern int close(int);
|
extern int close(int);
|
||||||
extern int unlink(const char *);
|
extern int unlink(const char *);
|
||||||
|
extern void *memset(void *, int, __SIZE_TYPE__);
|
||||||
extern int vsnprintf(char *, __SIZE_TYPE__, const char *, va_list);
|
extern int vsnprintf(char *, __SIZE_TYPE__, const char *, va_list);
|
||||||
extern int clock_gettime(int, void *);
|
extern int clock_gettime(int, void *);
|
||||||
extern int mprotect(void *, __SIZE_TYPE__, int);
|
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
|
* switch <id> <mask> <name ...> a switch whose hit is a shot: the switches section below
|
||||||
* callout <role> <id> scene <role> <40-hex id>
|
* callout <role> <id> scene <role> <40-hex id>
|
||||||
* text <name> <rest of line> anything else a mode may want
|
* 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" };
|
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
|
/* 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; };
|
struct switch_shot { unsigned id; uint64_t mask; };
|
||||||
|
|
||||||
static struct {
|
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 site site[N_SITES]; int n_site;
|
||||||
struct named data[N_DATA]; int n_data;
|
struct named data[N_DATA]; int n_data;
|
||||||
struct named value[N_VALUES]; int n_value;
|
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);
|
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). */
|
* 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];
|
static char buf[PORT_CHUNK];
|
||||||
char line[256];
|
char line[256];
|
||||||
long n, i, j = 0, total = 0;
|
long n, i, j = 0, total = 0;
|
||||||
unsigned k;
|
int fd = open(path, O_RDONLY);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fd < 0) return 0;
|
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) {
|
while (total < PORT_MAX && (n = read(fd, buf, sizeof buf)) > 0) {
|
||||||
if (n > PORT_MAX - total) n = PORT_MAX - total;
|
if (n > PORT_MAX - total) n = PORT_MAX - total;
|
||||||
total += n;
|
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
|
/* 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
|
* 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
|
* code mapping in three, and a site's 8 bytes may cross the split), each with every bit (tests lift it
|
||||||
* verbatim) */
|
* verbatim) */static int maps_has(unsigned long addr, unsigned long len, unsigned how)
|
||||||
static int maps_has(unsigned long addr, unsigned long len, unsigned how)
|
|
||||||
{
|
{
|
||||||
unsigned long at = addr, end = addr + len;
|
unsigned long at = addr, end = addr + len;
|
||||||
int i, steps;
|
int i, steps;
|
||||||
@@ -450,9 +445,15 @@ static int maps_has(unsigned long addr, unsigned long len, unsigned how)
|
|||||||
return 0;
|
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 --------
|
/* ---- 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. */
|
* callbacks not overlapping rest on this, so it is measured on every boot, not assumed. */
|
||||||
extern long syscall(long, ...);
|
extern long syscall(long, ...);
|
||||||
#define SYS_GETTID 224 /* ARM EABI */
|
#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)
|
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) */
|
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 void on_switch_edge(unsigned *r)
|
||||||
{
|
{
|
||||||
static volatile int said;
|
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;
|
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);
|
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 */
|
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
|
gl_switch_log(id, level, hit, playing); /* GAMELOG: the port's buttons, to /dump/switch.log */
|
||||||
* 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); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (edge_log_left > 0 && __sync_sub_and_fetch(&edge_log_left, 1) >= 0)
|
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,
|
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" : "");
|
flags, pol, mask, playing, hit && playing ? " - a hit" : "");
|
||||||
@@ -8834,6 +8838,82 @@ static int site_check(struct site *x)
|
|||||||
return x->ok;
|
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
|
/* 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
|
* 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) */
|
* capability. 1 = this game's port. (tests lift it verbatim) */
|
||||||
@@ -9325,23 +9405,28 @@ static void sndm_arm(void)
|
|||||||
__attribute__((constructor))
|
__attribute__((constructor))
|
||||||
static void pad_mode_start(void)
|
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;
|
const struct pm_mode *m;
|
||||||
struct site *tick;
|
long n, i;
|
||||||
int modes = 0;
|
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 */
|
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);
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||||
log_fd = open("/dump/mode.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
|
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);
|
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.too_long) say("port: longer than %d bytes - the lines after that are not read", PORT_MAX);
|
||||||
if (port.dropped)
|
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)",
|
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.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);
|
port.n_switch, N_SWITCHES);
|
||||||
if (!port_gate()) return;
|
|
||||||
clipv_arm(); /* PAD-446: before the score gate - a clip variant scores nothing */
|
clipv_arm(); /* PAD-446: before the score gate - a clip variant scores nothing */
|
||||||
sndm_arm(); /* PAD-494: nor does a music mode */
|
sndm_arm(); /* PAD-494: nor does a music mode */
|
||||||
if (clipv_only || sndm_only) {
|
if (clipv_only || sndm_only) {
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
{1, "QR SCANNER STATUS READY"},
|
|
||||||
{2, "QR SCANNER STATUS STRING"},
|
|
||||||
{3, "QR SCANNER STATUS ENCODED DATA"},
|
|
||||||
{4, "QR SCANNER STATUS MISSED STRING"},
|
|
||||||
{5, "QR SCANNER STATUS OVERRUN"},
|
|
||||||
{6, "QR SCANNER STATUS UNSUPPORTED_AP"},
|
|
||||||
{7, "QR SCANNER STATUS FIFO OVERRUN"},
|
|
||||||
{8, "QR SCANNER STATUS CONFIG"},
|
|
||||||
{9, "QR SCANNER STATUS ERROR"},
|
|
||||||
{10, "QR SCANNER STATUS ERR1"},
|
|
||||||
{11, "QR SCANNER STATUS ERR2"},
|
|
||||||
{12, "QR SCANNER STATUS ERR3"},
|
|
||||||
{13, "QR SCANNER STATUS ERR4"},
|
|
||||||
{14, "QR SCANNER STATUS ERR5"},
|
|
||||||
{15, "QR SCANNER STATUS ERR6"},
|
|
||||||
{16, "QR SCANNER STATUS NO READER"},
|
|
||||||
{17, "DIP 1"},
|
|
||||||
{18, "DIP 2"},
|
|
||||||
{19, "DIP 3"},
|
|
||||||
{20, "DIP 4"},
|
|
||||||
{21, "DIP 5"},
|
|
||||||
{22, "DIP 6"},
|
|
||||||
{23, "DIP 7"},
|
|
||||||
{24, "DIP 8"},
|
|
||||||
{25, "SERVICE SELECT"},
|
|
||||||
{26, "SERVICE PLUS"},
|
|
||||||
{27, "SERVICE MINUS"},
|
|
||||||
{28, "SERVICE BACK"},
|
|
||||||
{29, "HEADPHONE DETECT"},
|
|
||||||
{30, "HEADPHONE KIT CABLE DETECT"},
|
|
||||||
{31, "VOLUME ENCODER 1"},
|
|
||||||
{32, "VOLUME ENCODER 2"},
|
|
||||||
{33, "COIN DOOR INTERLOCK"},
|
|
||||||
{34, "LOCKDOWN BUTTON"},
|
|
||||||
{35, "TICKET NOTCH"},
|
|
||||||
{36, "START BUTTON"},
|
|
||||||
{37, "TOURNAMENT START BUTTON"},
|
|
||||||
{38, "TILT PENDULUM"},
|
|
||||||
{39, "LEFT COIN"},
|
|
||||||
{40, "CENTER COIN"},
|
|
||||||
{41, "RIGHT COIN"},
|
|
||||||
{42, "FOURTH COIN"},
|
|
||||||
{43, "FIFTH COIN"},
|
|
||||||
{44, "SIXTH COIN"},
|
|
||||||
{45, "SLAM TILT"},
|
|
||||||
{46, "X1"},
|
|
||||||
{47, "X2"},
|
|
||||||
{48, "X3"},
|
|
||||||
{49, "KONG CAVE VUK"},
|
|
||||||
{50, "X4"},
|
|
||||||
{51, "SHOOTER EXIT"},
|
|
||||||
{52, "UP LEFT FLIPPER EOS"},
|
|
||||||
{53, "UP RIGHT FLIPPER EOS"},
|
|
||||||
{54, "LEFT FLIPPER EOS"},
|
|
||||||
{55, "LEFT OUTLANE-LOWER"},
|
|
||||||
{56, "LEFT INLANE-INNER"},
|
|
||||||
{57, "RIGHT INLANE"},
|
|
||||||
{58, "RIGHT OUTLANE"},
|
|
||||||
{59, "LEFT OUTLANE-UPPER"},
|
|
||||||
{60, "LEFT INLANE-OUTER"},
|
|
||||||
{61, "RIVER EXIT"},
|
|
||||||
{62, "RIGHT FLIPPER BUTTON"},
|
|
||||||
{63, "LEFT FLIPPER BUTTON"},
|
|
||||||
{64, "UP RIGHT FLIPPER BUTTON"},
|
|
||||||
{65, "UP LEFT FLIPPER BUTTON"},
|
|
||||||
{66, "SHOOTER LANE"},
|
|
||||||
{67, "RIGHT SLINGSHOT"},
|
|
||||||
{68, "LEFT SLINGSHOT"},
|
|
||||||
{69, "RIGHT FLIPPER EOS"},
|
|
||||||
{70, "TROUGH 6"},
|
|
||||||
{71, "TROUGH 5"},
|
|
||||||
{72, "TROUGH 4"},
|
|
||||||
{73, "TROUGH 3"},
|
|
||||||
{74, "TROUGH 2"},
|
|
||||||
{75, "TROUGH 1"},
|
|
||||||
{76, "TROUGH JAM"},
|
|
||||||
{77, "RIVER ENTER"},
|
|
||||||
{78, "ISLAND TREASURE"},
|
|
||||||
{79, "U-TURN ROLLOVER"},
|
|
||||||
{80, "LEFT ORBIT SPINNER OPTO"},
|
|
||||||
{81, "POWERUP TARGET"},
|
|
||||||
{82, "KICK TARGET"},
|
|
||||||
{83, "TRAIN ENTER OPTO"},
|
|
||||||
{84, "TRAIN LOCK OPTO"},
|
|
||||||
{85, "KONG CAVE MADE"},
|
|
||||||
{86, "GONG HIT OPTO"},
|
|
||||||
{87, "KONG GONG VUK OPTO"},
|
|
||||||
{88, "RIGHT SPINNER OPTO"},
|
|
||||||
{89, "RIGHT ORBIT"},
|
|
||||||
{90, "CENTER RAMP MADE OPTO"},
|
|
||||||
{91, "LEFT ORBIT ENTRANCE OPTO"},
|
|
||||||
{92, "LEFT RAMP MADE OPTO"},
|
|
||||||
{93, "BIPLANE RAMP MADE OPTO"},
|
|
||||||
{94, "CENTER SHOT ENTER OPTO"},
|
|
||||||
{95, "2.42 MILLION BANANAS"},
|
|
||||||
{96, "4 BANK DROP 1-TOP"},
|
|
||||||
{97, "4 BANK DROP 2"},
|
|
||||||
{98, "4 BANK DROP 3"},
|
|
||||||
{99, "4 BANK DROP 4-BOT"},
|
|
||||||
{100, "PIT TARGET-TOP"},
|
|
||||||
{101, "PIT TARGET-MID"},
|
|
||||||
{102, "PIT TARGET-BOT"},
|
|
||||||
{103, "SPIDER MAGNET FIRED VIRTUAL"},
|
|
||||||
{104, "PUNCHBACK FIRED VIRTUAL"},
|
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
"""Multi-game port selection: which port file is this game's.
|
||||||
|
|
||||||
|
- port_select tries each candidate port in order, resetting the port tables between them, and
|
||||||
|
keeps the first whose core sites verify against the running game - that is how the machine
|
||||||
|
says which game it is on. A wrong candidate hooks nothing (the gate), so trying it is safe.
|
||||||
|
- ports_scan_buf reads the kernel's getdents64 records: every *.port in a ports directory is a
|
||||||
|
candidate, nothing else is.
|
||||||
|
- gl_switch_log writes the human buttons to the switch log, named by the port's own `switch`
|
||||||
|
lines - the switch names are no longer baked into the runtime per game.
|
||||||
|
|
||||||
|
Lifted verbatim from pad_mode_runtime.c and compiled for the HOST (the lift/_host_run helpers
|
||||||
|
are the decryptor repo tests', tests/test_spike2_mode_roster.py). Desk only: no card, no game.
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
RUNTIME = pathlib.Path(__file__).resolve().parents[1] / "pad_mode_runtime.c"
|
||||||
|
|
||||||
|
|
||||||
|
def _src():
|
||||||
|
return RUNTIME.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _lift(src, signature):
|
||||||
|
"""A function's text, verbatim from the runtime (brace-matched from its signature)."""
|
||||||
|
i = src.index(signature)
|
||||||
|
depth = 0
|
||||||
|
for k in range(src.index("{", i), len(src)):
|
||||||
|
depth += {"{": 1, "}": -1}.get(src[k], 0)
|
||||||
|
if depth == 0:
|
||||||
|
return src[i:k + 1]
|
||||||
|
raise AssertionError("unbalanced: " + signature)
|
||||||
|
|
||||||
|
|
||||||
|
def _between(src, start, end):
|
||||||
|
i = src.index(start)
|
||||||
|
return src[i:src.index(end, i)]
|
||||||
|
|
||||||
|
|
||||||
|
def _cc():
|
||||||
|
for c in ("gcc", "cc", "clang"):
|
||||||
|
if shutil.which(c):
|
||||||
|
return c
|
||||||
|
pytest.skip("no host C compiler")
|
||||||
|
|
||||||
|
|
||||||
|
def _host_run(tmp_path, code, flags=(), args=()):
|
||||||
|
cc = _cc()
|
||||||
|
(tmp_path / "t.c").write_text(code)
|
||||||
|
r = subprocess.run([cc, "-std=gnu17", "-Wall", "-Wno-unused-parameter", *flags,
|
||||||
|
"-o", str(tmp_path / "t"), str(tmp_path / "t.c")],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
return subprocess.run([str(tmp_path / "t"), *args], capture_output=True, text=True,
|
||||||
|
timeout=30).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def _port_section(src):
|
||||||
|
"""The port reader, lifted: its tables, port_line and port_read, then the lookups."""
|
||||||
|
helpers = "\n".join(_lift(src, s) for s in (
|
||||||
|
"static int str_eq(", "static void str_copy(", "static int hexval(", "static uint64_t number("))
|
||||||
|
reader = _between(src, "/* The port is read in PORT_CHUNK pieces",
|
||||||
|
"static struct site *site(const char *name)")
|
||||||
|
lookups = "\n".join(_lift(src, s) for s in (
|
||||||
|
"static struct site *site(const char *name)", "static unsigned fn(const char *name)",
|
||||||
|
"static long named(", "static unsigned data(const char *name)", "long pm_port_value("))
|
||||||
|
return helpers + "\n" + reader + "\n" + lookups
|
||||||
|
|
||||||
|
|
||||||
|
_SELECT_PRELUDE = r"""
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#define N_BUS_IDS 208
|
||||||
|
static void port_line(const char *s);
|
||||||
|
static void port_reset(void);
|
||||||
|
static int port_select(const char *const *files, int n);
|
||||||
|
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; } /* not this test's */
|
||||||
|
/* the runtime's parse-state counters outside the port struct; the tables themselves are stubbed */
|
||||||
|
static int n_event_names, n_site_events;
|
||||||
|
static int n_lamps;
|
||||||
|
static int n_stock_rules;
|
||||||
|
static void say(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
printf("SAY ");
|
||||||
|
vprintf(fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
putchar('\n');
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SELECT_TAIL = r"""
|
||||||
|
/* the game's "code": words the sites point at. The port's site addresses are 32 bits (ARM), so
|
||||||
|
* the code page is mapped at a low fixed address instead of living in this binary's data. */
|
||||||
|
#include <sys/mman.h>
|
||||||
|
static unsigned *code;
|
||||||
|
|
||||||
|
static void line(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char b[256];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(b, sizeof b, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
port_line(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void site_at(const char *name, int i)
|
||||||
|
{
|
||||||
|
line("site %s 0x%lx 0x%x 0x%x", name, (unsigned long)&code[i], code[i], code[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core_lines(void)
|
||||||
|
{
|
||||||
|
site_at("tick", 0);
|
||||||
|
site_at("shot_dispatch", 2);
|
||||||
|
site_at("ball_end", 4);
|
||||||
|
site_at("score_add", 6);
|
||||||
|
line("data cur_player 0x5b322c");
|
||||||
|
line("data scores 0x5b3578");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* one candidate port file: game <name>, a matching core only when good. The sites carry THIS
|
||||||
|
* process's code addresses - the file is selected in this same process, as on the machine. */
|
||||||
|
static void write_port(const char *path, const char *game, int good)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
fprintf(f, "game %s\nversion 1.0\n", game);
|
||||||
|
if (good) {
|
||||||
|
fprintf(f, "site tick 0x%lx 0x%x 0x%x\n", (unsigned long)&code[0], code[0], code[1]);
|
||||||
|
fprintf(f, "site shot_dispatch 0x%lx 0x%x 0x%x\n", (unsigned long)&code[2], code[2], code[3]);
|
||||||
|
fprintf(f, "site ball_end 0x%lx 0x%x 0x%x\n", (unsigned long)&code[4], code[4], code[5]);
|
||||||
|
fprintf(f, "site score_add 0x%lx 0x%x 0x%x\n", (unsigned long)&code[6], code[6], code[7]);
|
||||||
|
} else {
|
||||||
|
/* the file's site names the right address with the wrong words: the gate refuses it */
|
||||||
|
fprintf(f, "site tick 0x%lx 0x11111111 0x22222222\n", (unsigned long)&code[0]);
|
||||||
|
fprintf(f, "site shot_dispatch 0x%lx 0x11111111 0x22222222\n", (unsigned long)&code[2]);
|
||||||
|
fprintf(f, "site ball_end 0x%lx 0x11111111 0x22222222\n", (unsigned long)&code[4]);
|
||||||
|
fprintf(f, "site score_add 0x%lx 0x11111111 0x22222222\n", (unsigned long)&code[6]);
|
||||||
|
}
|
||||||
|
fprintf(f, "data cur_player 0x5b322c\ndata scores 0x5b3578\n");
|
||||||
|
fprintf(f, "shot 0x1 X1\nshot 0x2 Left slingshot\n");
|
||||||
|
fprintf(f, "event game_start 0x4d\nevent ball_start 0x25\n");
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *what = argv[1];
|
||||||
|
char maps_text[128];
|
||||||
|
static const char *files[8];
|
||||||
|
int i, n = 0;
|
||||||
|
code = mmap((void *)0x08000000, 4096, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
|
||||||
|
if (code == (unsigned *)-1) { printf("mmap failed\n"); return 1; }
|
||||||
|
{
|
||||||
|
unsigned words[] = {
|
||||||
|
0xe92d4038, 0xe3a00037, /* 0: tick */
|
||||||
|
0xe3500b02, 0xe92d4008, /* 2: shot_dispatch */
|
||||||
|
0xe92d4008, 0xe3a00000, /* 4: ball_end */
|
||||||
|
0xe30817fe, 0xe340108b, /* 6: score_add */
|
||||||
|
};
|
||||||
|
memcpy(code, words, sizeof words);
|
||||||
|
}
|
||||||
|
snprintf(maps_text, sizeof maps_text, "%lx-%lx r-xp 00000000 00:00 0 /games/x/game",
|
||||||
|
(unsigned long)code, (unsigned long)(code + 64));
|
||||||
|
maps_line(maps_text, maps_text + strlen(maps_text));
|
||||||
|
if (!strcmp(what, "select")) {
|
||||||
|
int ok;
|
||||||
|
n = atoi(argv[2]); /* then, per candidate: <path> <game> <good> */
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
write_port(argv[3 + 3 * i], argv[4 + 3 * i], atoi(argv[5 + 3 * i]));
|
||||||
|
files[i] = argv[3 + 3 * i];
|
||||||
|
}
|
||||||
|
ok = port_select(files, n);
|
||||||
|
printf("SELECT %d\n", ok);
|
||||||
|
printf("GAME %s\n", port.game);
|
||||||
|
printf("SITES %d SHOTS %d EVENTS %d\n", port.n_site, port.n_shot, n_event_names);
|
||||||
|
} else { /* reset */
|
||||||
|
printf("RESET game=%s sites=%d shots=%d events=%d can=%u\n",
|
||||||
|
port.game, port.n_site, port.n_shot, n_event_names, can);
|
||||||
|
line("game leftover");
|
||||||
|
line("version 9.9");
|
||||||
|
core_lines();
|
||||||
|
line("shot 0x1 X1");
|
||||||
|
line("shot 0x2 Left slingshot");
|
||||||
|
line("shot 0x4 third");
|
||||||
|
line("event game_start 0x4d");
|
||||||
|
line("event ball_start 0x25");
|
||||||
|
line("event ball_end 0x34");
|
||||||
|
can = 0xffffffffu;
|
||||||
|
port_reset();
|
||||||
|
printf("RESET game=%s sites=%d shots=%d events=%d can=%u\n",
|
||||||
|
port.game, port.n_site, port.n_shot, n_event_names, can);
|
||||||
|
core_lines(); /* clean tables still gate a good port in */
|
||||||
|
printf("RESETGATE %d\n", port_gate());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _select_source():
|
||||||
|
src = _src()
|
||||||
|
maps = _between(src, "#define N_MAPS", "/* ---- rule 2:")
|
||||||
|
gate = "\n".join(_lift(src, s) for s in (
|
||||||
|
"static int words_match(struct site *x)", "static void core_of_port(",
|
||||||
|
"static int site_check(", "static int port_gate(void)\n{"))
|
||||||
|
maps = maps.replace("static void maps_line(const char *s, const char *e)",
|
||||||
|
"static void maps_line(const char *s, const char *e_)")
|
||||||
|
maps = maps.replace(" unsigned long lo = 0, hi = 0;\n const char *q = s, *p;",
|
||||||
|
" const char *e = e_ ? e_ : s + strlen(s);\n"
|
||||||
|
" unsigned long lo = 0, hi = 0;\n const char *q = s, *p;", 1)
|
||||||
|
extra = "\n".join(_lift(src, s) for s in (
|
||||||
|
"static void port_reset(void)",
|
||||||
|
"static int port_select(const char *const *files, int n)"))
|
||||||
|
return (_SELECT_PRELUDE + _port_section(src) + "\n" + maps + "\n" + gate + "\n" + extra
|
||||||
|
+ _SELECT_TAIL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_select_keeps_the_port_that_matches(tmp_path):
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
pytest.skip("the port's site addresses are 32 bits and darwin arm64 maps nothing below "
|
||||||
|
"4 GiB - the Linux leg runs these")
|
||||||
|
a, b = str(tmp_path / "a.port"), str(tmp_path / "b.port")
|
||||||
|
out = _host_run(tmp_path, _select_source(),
|
||||||
|
args=("select", "2", a, "wrong_game", "0", b, "right_game", "1"))
|
||||||
|
assert "SELECT 1" in out, out
|
||||||
|
assert "GAME right_game" in out, out
|
||||||
|
assert "SAY port %s: says wrong_game 1.0 - not this game's port, skipped" % a in out, out
|
||||||
|
assert "SITES 4 SHOTS 2 EVENTS 0" in out, out # event_line is stubbed in this harness
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_select_prefers_the_earlier_candidate(tmp_path):
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
pytest.skip("32-bit site addresses: see the first select test")
|
||||||
|
a, b = str(tmp_path / "a.port"), str(tmp_path / "b.port")
|
||||||
|
out = _host_run(tmp_path, _select_source(),
|
||||||
|
args=("select", "2", a, "a_game", "1", b, "b_game", "1"))
|
||||||
|
assert "SELECT 1" in out and "GAME a_game" in out, out
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_select_refuses_when_no_port_matches(tmp_path):
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
pytest.skip("32-bit site addresses: see the first select test")
|
||||||
|
a = str(tmp_path / "a.port")
|
||||||
|
out = _host_run(tmp_path, _select_source(), args=("select", "1", a, "wrong_game", "0"))
|
||||||
|
assert "SELECT 0" in out, out
|
||||||
|
assert "not this game's port, skipped" in out, out
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_select_resets_the_tables_between_candidates(tmp_path):
|
||||||
|
"""A candidate's lines must not leak into the next one's tables: after a full port is read, a
|
||||||
|
reset leaves every table empty (game, sites, shots, events, can) and still gates a good port."""
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
pytest.skip("32-bit site addresses: see the first select test")
|
||||||
|
out = _host_run(tmp_path, _select_source(), args=("reset",))
|
||||||
|
got = [l for l in out.splitlines() if l.startswith("RESET ")]
|
||||||
|
assert got == ["RESET game= sites=0 shots=0 events=0 can=0",
|
||||||
|
"RESET game= sites=0 shots=0 events=0 can=0"], out
|
||||||
|
assert "RESETGATE 1" in out, out
|
||||||
|
|
||||||
|
|
||||||
|
_SCAN_MAIN = r"""
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
static char buf[512];
|
||||||
|
char cand[8][128];
|
||||||
|
long n, i;
|
||||||
|
char *at = buf;
|
||||||
|
const char *names[] = { "king_kong_le-0.97.port", "readme.txt", ".", "..", "godzilla_le-1.16.port" };
|
||||||
|
unsigned char types[] = { 8, 8, 4, 4, 8 };
|
||||||
|
for (i = 0; i < 5; i++) {
|
||||||
|
unsigned long long ino = (unsigned long long)i + 1;
|
||||||
|
long long off = (long long)i + 1;
|
||||||
|
unsigned short reclen = (unsigned short)(((strlen(names[i]) + 1 + 19) + 7) & ~7L);
|
||||||
|
memcpy(at, &ino, 8);
|
||||||
|
memcpy(at + 8, &off, 8);
|
||||||
|
memcpy(at + 16, &reclen, 2);
|
||||||
|
at[18] = types[i];
|
||||||
|
strcpy(at + 19, names[i]);
|
||||||
|
at += reclen;
|
||||||
|
}
|
||||||
|
n = ports_scan_buf(buf, (long)(at - buf), "/usr/local/padmode/ports", cand, 8, 0);
|
||||||
|
printf("SCAN %ld\n", n);
|
||||||
|
for (i = 0; i < n; i++) printf("CAND %s\n", cand[i]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_source():
|
||||||
|
src = _src()
|
||||||
|
prelude = r"""
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
static void say(const char *fmt, ...) { (void)fmt; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
static long ports_scan_buf(char *buf, long len, const char *dir, char (*cand)[128], long cap, long n);
|
||||||
|
"""
|
||||||
|
helpers = _lift(src, "static int str_eq(")
|
||||||
|
dirent = _between(src, "/* the kernel's getdents64 record", "static long ports_scan(")
|
||||||
|
return prelude + helpers + "\n" + dirent + _SCAN_MAIN
|
||||||
|
|
||||||
|
|
||||||
|
def test_ports_scan_buf_takes_only_port_files(tmp_path):
|
||||||
|
out = _host_run(tmp_path, _scan_source())
|
||||||
|
lines = out.splitlines()
|
||||||
|
assert "SCAN 2" in out, out
|
||||||
|
assert "CAND /usr/local/padmode/ports/king_kong_le-0.97.port" in lines, out
|
||||||
|
assert "CAND /usr/local/padmode/ports/godzilla_le-1.16.port" in lines, out
|
||||||
|
|
||||||
|
|
||||||
|
_SWITCH_PRELUDE = r"""
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
static void port_line(const char *s);
|
||||||
|
static void gl_switch_log(unsigned id, unsigned level, int hit, int playing);
|
||||||
|
static const char *GL_SWITCH_LOG;
|
||||||
|
static void say(const char *fmt, ...) { (void)fmt; }
|
||||||
|
static void gl_rfc3339(char *buf, unsigned cap) { snprintf(buf, cap, "2026-10-13T12:00:00.000Z"); }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 line(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char b[256];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(b, sizeof b, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
port_line(b);
|
||||||
|
}
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
GL_SWITCH_LOG = argv[1];
|
||||||
|
line("game king_kong_le");
|
||||||
|
line("version 0.97");
|
||||||
|
line("switch 34 0x1000000000000000 LOCKDOWN BUTTON");
|
||||||
|
line("switch 36 0x2000000000000000 START BUTTON");
|
||||||
|
line("switch 62 0x4000000000000000 RIGHT FLIPPER BUTTON");
|
||||||
|
line("switch 63 0x8000000000000000 LEFT FLIPPER BUTTON");
|
||||||
|
gl_switch_log(62, 1, 1, 1);
|
||||||
|
gl_switch_log(50, 1, 1, 1); /* not a button the port names: nothing */
|
||||||
|
gl_switch_log(36, 0, 0, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _switch_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("))
|
||||||
|
reader = _between(src, "/* The port is read in PORT_CHUNK pieces",
|
||||||
|
"static struct site *site(const char *name)")
|
||||||
|
lookups = _lift(src, "static struct site *site(const char *name)")
|
||||||
|
fn = _lift(src, "static void gl_switch_log(")
|
||||||
|
return _SWITCH_PRELUDE + helpers + "\n" + reader + "\n" + lookups + "\n" + fn
|
||||||
|
|
||||||
|
|
||||||
|
def test_gl_switch_log_writes_the_ports_buttons(tmp_path):
|
||||||
|
log = tmp_path / "switch.log"
|
||||||
|
out = _host_run(tmp_path, _switch_source(), args=(str(log),))
|
||||||
|
assert out == ""
|
||||||
|
text = log.read_text(encoding="utf-8").splitlines()
|
||||||
|
assert text == [
|
||||||
|
'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
|
||||||
Reference in New Issue
Block a user