Files
stern-event-agent/pad_stock.h
T
mattlqxandClaude Code abc881f913 feat: add passive GAMELOG event logger for Spike 2 King Kong LE 0.97
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>
2026-10-12 13:57:12 -04:00

193 lines
14 KiB
C

/* pad_stock.h - rewrite the SHOT LOGIC of one of the game's OWN rules in C (item 161).
*
* Implemented in pad_mode_runtime.c ("STOCK RULES IN C") on item 160's per-rule wrap of the shot
* handler's vtable slot. Every address, offset, slot number and id below comes from the PORT
* (`site` / `data` / `value` lines; MODE_SDK.md, "Rewriting a stock rule's shot logic"); nothing in
* this header is an address, and a call whose port line is missing does nothing and says so once.
*
* WHAT A STOCK RULE IS. The game's own modes (a battle, a multiball) are compiled C++ objects, one
* per rule, each with a vtable (MODE_SDK.md, "The game's own modes"). Three of its virtuals matter
* here: START (v[8]), STOP(reason) (v[11]) and the SHOT HANDLER (v[42] on Premium 1.16, v[41] on
* Pro 1.15; the port's `stock_slot_*` values say which). The handler is called with every shot the
* game dispatches while the rule is active: (this, _, shot lo, shot hi, [sp] factor). It reads and
* writes the rule's own fields (its lit mask, its counters) and calls the engine (awards, shows,
* events, the stop). Rewriting a rule's shot logic = registering a C function that runs INSTEAD
* of that virtual, and calling the same engine pieces through the port.
*
* WHAT IT IS NOT. The rule's START, its timer, its display layers, its lamps and its ending are
* still the game's own. A rewrite changes what a shot DOES; the battle still starts from the
* select screen, still times out, and still ends with the game's own screens.
*
* THE RULES (pad_mode.h's still apply: no blocking, no malloc, static state):
* - A handler runs INSIDE the game's dispatch, on the game thread, with the rule object live.
* Return quickly; never start a mode of ours from it.
* - Reach the rule's fields only through the accessors here; every one reads its offset from
* the port, and returns 0 / does nothing when the port has no line for it.
* - `pm_stock_call_original` is the ONLY way to run the game's handler from yours; the wrap
* never re-enters your handler for that call, and no counts-as row is applied to it.
* - With no record registered for a rule, and no counts-as row, nothing is wrapped: the game
* runs stock (the wrap is installed only for what a card asks for).
*
* WHAT IS PROVEN. See MODE_SDK.md's section for the emulator runs. Item 158 proved (emulator,
* Premium 1.16) the facts it rests on: the slot numbers, the field and counter offsets, that a
* wrapped v[42] sees every shot with its factor, that caward_add / show_start / game_event are what
* the handlers call, and that the final blow ends the battle with reason 1 from inside the handler.
*/
#ifndef PAD_STOCK_H
#define PAD_STOCK_H
#include "pad_mode.h"
/* ---- a stock rule, and the player it acts for -------------------------------------------- */
struct pm_stock_rule; /* opaque: the runtime's handle on one rule object */
/* The rule with this id, or 0 when the port cannot reach the manager (`site stock_rule_get`,
* `data stock_mode_manager`, a `rule` line), the manager has not built it yet, or its object is not
* the port's. Godzilla: 4 tank attack, 12 Ebirah (`value ebirah_rule`, `value tank_rule`). */
struct pm_stock_rule *pm_stock_rule(unsigned id);
unsigned pm_stock_rule_id(const struct pm_stock_rule *r);
/* The player the game's own handler would act for: the current player (1-4), as the handlers
* read it (the same byte pm_player() reads). 0 when no game. */
unsigned pm_stock_player(void);
/* ---- registering a handler ------------------------------------------------------------------
* Return PM_STOCK_DONE when your code handled the shot (the game's handler is NOT run for it),
* PM_STOCK_PASS to let the game's handler run as if you were not there (after item 160's counts-as
* rows, when the card has any). `factor` is the dispatch's stack word (1 from a switch; the
* game's own spot helpers pass their own). `shot` is the mask as dispatched: test bits, never
* compare the whole mask (a Big loop arrives as 0x1000000001; every switch also sends 0x1). */
#define PM_STOCK_PASS 0
#define PM_STOCK_DONE 1
typedef int (*pm_stock_shot_handler)(struct pm_stock_rule *rule, uint64_t shot, unsigned factor);
struct pm_stock_handler {
unsigned rule_id; /* the rule this replaces */
pm_stock_shot_handler shot; /* runs instead of the game's handler */
void (*started)(struct pm_stock_rule *rule); /* optional: after the game's START ran */
void (*stopped)(struct pm_stock_rule *rule, unsigned reason);/* optional: before the game's STOP runs (1 = won) */
const char *name; /* shown in the log */
const struct pm_mode *mode; /* optional: the mode the log lines and the lamp
holds made from the callbacks belong to */
};
/* The full form, and the one-liner: PM_STOCK_HANDLER(12, my_ebirah_shot). Both put the record in
* the `pm_stock` section the runtime walks from its tick (twice a second, until the manager has
* built the rule), like PM_REGISTER. One record per rule; a second is refused and said. */
#define PM_STOCK_RULE(h) \
static const struct pm_stock_handler *const pm__stock_##h \
__attribute__((used, section("pm_stock"))) = &(h)
#define PM_STOCK_HANDLER(id, fn) \
static const struct pm_stock_handler pm__stock_rec_##fn = { (id), (fn), 0, 0, #fn, 0 }; \
PM_STOCK_RULE(pm__stock_rec_##fn)
/* The game's own handler for this rule, with these arguments, as it was before the wrap; what it
* returns (r0:r1) is passed back. From inside a handler or a started/stopped callback of the rule. */
uint64_t pm_stock_call_original(struct pm_stock_rule *rule, uint64_t shot, unsigned factor);
/* ---- the rule's state, through the port ------------------------------------------------------
* The lit-shot mask the rule keeps per player: u64 at obj + `value stock_field` + 8 * player.
* What the game's own v[25] reports as lit for most rules (tank keeps its own, see below), and
* what the game's handler gates on (Ebirah: a shot with no lit bit does nothing). */
uint64_t pm_stock_field(struct pm_stock_rule *rule, unsigned player);
int pm_stock_field_set(struct pm_stock_rule *rule, unsigned player, uint64_t mask);
/* The rule's own answers: lit shots (v[stock_slot_lit]), active (v[stock_slot_active]; -1 unknown),
* running for a player (the byte at obj + `value stock_running_at` + player; -1 unknown). */
uint64_t pm_stock_lit(struct pm_stock_rule *rule);
int pm_stock_active(struct pm_stock_rule *rule);
int pm_stock_running(struct pm_stock_rule *rule, unsigned player);
/* A word of the rule's own, at an offset the port names: `at` is the result of pm_stock_at(),
* -1 when the port has no such line (every call below then fails with 0). A per-player word
* is at obj + at + 4 * player (the game's counters index from 1); a plain word at obj + at. */
long pm_stock_at(const char *value_name); /* pm_port_value(name, -1), said once when missing */
int pm_stock_pw_get(struct pm_stock_rule *rule, long at, unsigned player, int *out);
int pm_stock_pw_set(struct pm_stock_rule *rule, long at, unsigned player, int value);
int pm_stock_w_get(struct pm_stock_rule *rule, long at, int *out);
int pm_stock_w_set(struct pm_stock_rule *rule, long at, int value);
int pm_stock_b_get(struct pm_stock_rule *rule, long at, int *out); /* a byte */
/* ---- the engine pieces the handlers call --------------------------------------------------
* An AWARD, the way the stock handler pays one: caward_add(the rule's own award object at
* obj + `value stock_award_at`, 0, value, index 0). Returns what the engine says it added.
* pm_stock_award_to pays through ANOTHER rule's award object (Ebirah under King of the Monsters
* pays through rule 20's). pm_stock_build is caward_build on the rule's award (tank pays half a
* kill into it). Sites: caward_add, caward_build. */
uint64_t pm_stock_award(struct pm_stock_rule *rule, uint64_t value);
uint64_t pm_stock_award_to(unsigned rule_id, uint64_t value);
uint64_t pm_stock_build(struct pm_stock_rule *rule, uint64_t value);
/* A SHOW (a light show / display cue by id, `site show_start`): returns the show's record, which
* the stock handlers then fill (a colour, a lamp id, a count; the offsets are the show's, not
* the rule's), or 0. Ebirah shows 347 and 359 on a spin, 148 on a stage; tank shows 144 on the
* destroyed tank's insert. */
void *pm_stock_show(unsigned id);
/* A GAME EVENT (`site game_event`: the rule-to-rule broadcast the handlers post; Ebirah 19 on the
* final blow, tank 51 on a Maser hit) with the value the handlers pass (the award total). The
* trailing arguments the handlers pass (a word, then a u64) are sent as 0. */
int pm_stock_event(unsigned id, uint64_t value);
/* A display EVENT record (`site event_post_replacing`), what the stage screens and the stop
* displays post; returns the record or 0. pm_stock_event_cancel removes one by id (`site event_cancel`). */
void *pm_stock_display_event(unsigned id, unsigned handler, unsigned flags);
int pm_stock_event_cancel(unsigned id);
/* A callout or a sound: the ordinary pad_mode.h calls (pm_callout, pm_sound). */
/* STOP the rule the way its own code does: v[stock_slot_stop](reason), 1 = completed (won), 0 =
* not. pm_stock_start runs its START (for a test; the game's own way is the select screen). Both
* go through the vtable, so a probe's wrap of those slots logs them. */
int pm_stock_stop(struct pm_stock_rule *rule, int won);
int pm_stock_start(struct pm_stock_rule *rule);
/* ---- Godzilla: battle vs Ebirah (rule `value ebirah_rule`) -----------------------------------
* Its own state (item 158): three per-player spin counters (`value ebirah_spin_left_at`,
* `ebirah_spin_top_at`, `ebirah_spin_shield_at`; the ctor's defaults 15 / 40 / 15), a stage index
* (`ebirah_stage_index_at`, which 5M / 10M / 15M award is next), a per-player stage count
* (`ebirah_stage_count_at`), and the KOTM byte (`ebirah_kotm_at`: awards go to King of the
* Monsters while it runs). Its handler tests RAW shot bits (`value ebirah_spin_left_bit` 0x200,
* `ebirah_spin_top_bit` 0x2000, `ebirah_spin_shield_bit` 0x20000) and, once every counter is 0,
* the FINAL shot (`ebirah_final_shot` 0x40, the Pop bumper, or bit 42) with the field set to
* `ebirah_final_mask_lo/hi`. */
#define PM_EBIRAH_LEFT 0
#define PM_EBIRAH_TOP 1
#define PM_EBIRAH_SHIELD 2
int pm_ebirah_spins(struct pm_stock_rule *rule, int which, unsigned player); /* -1 = no line */
int pm_ebirah_spins_set(struct pm_stock_rule *rule, int which, unsigned player, int n);
uint64_t pm_ebirah_spin_bit(int which); /* 0 = no line */
/* A STAGE AWARD, exactly as the game gives one: sets that spinner's counter to 1, lights its bit,
* and runs the game's handler with the spinner's own bit. The game then pays the last spin, gives
* the 5M / 10M / 15M award in completion order, shows its award screen, posts its reminder
* event, and, when every counter is 0, writes the final mask itself. Returns 1 when it ran. */
int pm_ebirah_stage_award(struct pm_stock_rule *rule, int which);
/* The FINAL BLOW, exactly as the game gives one: sets the field to the final mask and runs the
* game's handler with the final shot. The game pays 25,000,000, the finishing bonus, shows the
* final-blow screen, posts its events and STOPS the battle as WON (v[11](1)): the battle's own
* ending plays. Returns 1 when it ran. Ebirah's port lines; another rule has no final blow. */
int pm_stock_final_blow(struct pm_stock_rule *rule);
/* The stage awards the game's own vector holds (`data ebirah_stage_awards`, `value
* ebirah_stage_award_count`): 5,000,000 / 10,000,000 / 15,000,000 on Godzilla; 0 = no line. */
uint64_t pm_ebirah_stage_value(unsigned index);
/* ---- Godzilla: tank attack multiball (rule `value tank_rule`) ---------------------------------
* Tank keeps no lit mask: its shots are TANK RECORDS (a vector at obj + `value tank_records_at`
* of `tank_record_size`-byte records: active byte at +`tank_record_active_at`, position mask at
* +`tank_record_pos_at`, previous position at +`tank_record_prev_at`, destination at
* +`tank_record_dest_at`, value at +`tank_record_value_at`), walking the PATH (`data tank_path`,
* `value tank_path_entries` 16-byte entries {u64 mask, u16 lamp, u16 id, u32}). Counters:
* destroyed (`tank_destroyed_at`), level per player (`tank_level_at`), the tank value per
* player (`tank_value_at`, u64), the Maser multiplier (`tank_multiplier_at`). Nothing of the
* tank's has run under a C handler yet (desk). */
struct pm_tank_record { int active; uint64_t pos, prev, dest, value; };
int pm_tank_records(struct pm_stock_rule *rule, struct pm_tank_record *out, int max); /* how many were filled (at most max) */
int pm_tank_destroyed(struct pm_stock_rule *rule); /* -1 = no line */
int pm_tank_level(struct pm_stock_rule *rule, unsigned player);
int pm_tank_maser_phase(struct pm_stock_rule *rule); /* the game's own test (`site tank_maser_phase`) */
/* The game's own pieces (sites tank_destroy, tank_seed_wave, tank_advance): destroy record
* `index` with credit (the destroyed count, the value step, a respawn), seed a new wave (level +
* 1, three tanks), advance every tank one step and spawn. Each returns 1 when the site is there
* and it ran. pm_tank_path_index is the first path entry that ANDs the shot (from the port's
* data), -1 when none. */
int pm_tank_destroy(struct pm_stock_rule *rule, int index, int credit);
int pm_tank_seed_wave(struct pm_stock_rule *rule);
int pm_tank_advance(struct pm_stock_rule *rule);
int pm_tank_path_index(struct pm_stock_rule *rule, uint64_t shot);
int pm_tank_path_entry(unsigned index, uint64_t *mask, unsigned *lamp); /* from the port's data */
#endif