Skip to content

Commit da4e9b1

Browse files
sp00ktoberclaude
andauthored
feat(framework): one event system, one wrapper builder, live-path fixes (#20)
* feat(framework): one event system, one wrapper builder, live-path fixes The behaviour half of sp00ktober/MHFU-ModKit#65: an event registry with owners, every hook that calls C through one wrapper builder, em_vhook and the Brute joint fix in core, the overlay relocator on mhfu.reloc's rules, and the fixes found on the way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(framework): the marshal test's semaphores outlive its threads, so pytest exits on Linux; CI jobs get time limits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 489e4c8 commit da4e9b1

71 files changed

Lines changed: 4425 additions & 2724 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ on:
88
permissions:
99
contents: read
1010

11+
# every job has a time limit: a hung one otherwise runs for six hours on the minutes budget
1112
jobs:
1213
lint:
1314
runs-on: ubuntu-latest
15+
timeout-minutes: 10
1416
steps:
1517
- uses: actions/checkout@v7
1618
- uses: astral-sh/setup-uv@v10.2.0
@@ -28,6 +30,7 @@ jobs:
2830
matrix:
2931
python: ["3.11", "3.14"]
3032
runs-on: ubuntu-latest
33+
timeout-minutes: 10
3134
steps:
3235
- uses: actions/checkout@v7
3336
- uses: astral-sh/setup-uv@v10.2.0
@@ -41,6 +44,7 @@ jobs:
4144
api:
4245
if: github.event_name == 'pull_request'
4346
runs-on: ubuntu-latest
47+
timeout-minutes: 5
4448
steps:
4549
- uses: actions/checkout@v7
4650
with:

‎framework/Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ PRXS = build/mhfu_framework.prx build/mhfu_boot.prx
1111
# psp-prxgen copies EF_MIPS_ABI_EABI32 into the PRX, which old firmware rejects (0x800200D9)
1212
all: $(GEN)/addresses.gen.h $(GEN)/addresses.lua
1313
docker run --rm --platform linux/amd64 --user "$$(id -u):$$(id -g)" -v "$(CURDIR)":/work -w /work \
14-
$(IMAGE) make -f Makefile.psp
14+
$(IMAGE) make -f Makefile.psp $(if $(findstring k,$(firstword -$(MAKEFLAGS))),-k)
1515
uv run python tools/patch_prx_eflags.py $(PRXS)
1616

1717
$(GEN)/addresses.gen.h: $(ADDR).toml $(ADDR).py

‎framework/README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ PSP/PLUGINS/mhfu_framework/
3434
framework.log written at run time
3535
```
3636

37-
`plugin.ini` for PPSSPP (it ignores a file without both sections; the addresses are EU only):
37+
`plugin.ini` for PPSSPP (it ignores a file without both sections; the addresses are EU only).
38+
`memory = 64` gives the emulated PSP the extra RAM that model injection, entity clones, relocated
39+
overlays and the debug shell's bridge live in:
3840

3941
```ini
4042
[games]
@@ -44,6 +46,7 @@ type = prx
4446
filename = mhfu_framework.prx
4547
name = MHFU Framework
4648
version = 1
49+
memory = 64
4750
```
4851

4952
On a real PSP under PRO CFW, `seplugins/game.txt` names `mhfu_boot.prx` instead:

‎framework/boot/main.c‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ static int boot_thread(SceSize args, void *argp)
7575
sceKernelDelayThread(BOOT_SETTLE_US);
7676
log_path("[boot] framework=", g_path);
7777

78-
int i;
79-
for (i = 0; i < 600; i++) { /* up to ~10 min, 1 s poll */
78+
for (int i = 0;; i++) { /* 1 s poll; the fallback load ends the wait */
8079
unsigned char scr = *(volatile unsigned char *)MHFU_SCREEN_STATE;
8180
int quiet = (scr == MHFU_WORLD_SCREEN_MENU || scr == MHFU_WORLD_SCREEN_TITLE);
8281
if (quiet || i >= LOAD_FALLBACK_S) {
@@ -100,8 +99,6 @@ static int boot_thread(SceSize args, void *argp)
10099
if ((i % 5) == 0) log_kv("[boot] waiting for menu, screen_state=", scr);
101100
sceKernelDelayThread(1000 * 1000);
102101
}
103-
log_msg("[boot] GAVE UP — never reached in-area (screen_state never == 17)");
104-
return 0;
105102
}
106103

107104
int module_start(SceSize args, void *argp)
@@ -112,4 +109,5 @@ int module_start(SceSize args, void *argp)
112109
}
113110

114111
int module_stop(SceSize args, void *argp) { (void)args; (void)argp; return 0; }
115-
void _exit(int status) { (void)status; }
112+
/* declared noreturn; this plugin never exits */
113+
void _exit(int status) { (void)status; for (;;) sceKernelDelayThread(1000000); }

‎framework/include/mhfu/ai.h‎

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#define MHFU_AI_H
1010

1111
#include <stdint.h>
12-
#include "hooks.h"
12+
#include "events.h"
1313

1414
#ifdef __cplusplus
1515
extern "C" {
@@ -117,36 +117,44 @@ typedef void (*mhfu_bigmonster_spawn_cb_t)(const mhfu_bigmonster_spawn_ctx_t *ct
117117
typedef void (*mhfu_bigmonster_death_cb_t)(const mhfu_bigmonster_death_ctx_t *ctx);
118118
typedef void (*mhfu_bigmonster_damaged_cb_t)(const mhfu_bigmonster_damaged_ctx_t *ctx);
119119

120-
/* --- registration: higher priority runs first, ties in registration order -------------- */
121-
122-
mhfu_hook_rc_t mhfu_on_ai_overlay_loaded(mhfu_ai_overlay_loaded_cb_t cb, int priority);
123-
mhfu_hook_rc_t mhfu_off_ai_overlay_loaded(mhfu_ai_overlay_loaded_cb_t cb);
124-
125-
mhfu_hook_rc_t mhfu_on_bigmonster_slot_picked(
126-
mhfu_bigmonster_slot_picked_cb_t cb, int priority);
127-
mhfu_hook_rc_t mhfu_on_bigmonster_action_input(
128-
mhfu_bigmonster_action_input_cb_t cb, int priority);
129-
mhfu_hook_rc_t mhfu_on_bigmonster_action_decided(
130-
mhfu_bigmonster_action_decided_cb_t cb, int priority);
131-
mhfu_hook_rc_t mhfu_on_bigmonster_ai_step(
132-
mhfu_bigmonster_ai_step_cb_t cb, int priority);
133-
mhfu_hook_rc_t mhfu_on_bigmonster_action(
134-
mhfu_bigmonster_action_cb_t cb, int priority);
135-
mhfu_hook_rc_t mhfu_on_bigmonster_spawn(
136-
mhfu_bigmonster_spawn_cb_t cb, int priority);
137-
mhfu_hook_rc_t mhfu_on_bigmonster_death(
138-
mhfu_bigmonster_death_cb_t cb, int priority);
139-
mhfu_hook_rc_t mhfu_on_bigmonster_damaged(
140-
mhfu_bigmonster_damaged_cb_t cb, int priority);
141-
142-
mhfu_hook_rc_t mhfu_off_bigmonster_slot_picked (mhfu_bigmonster_slot_picked_cb_t cb);
143-
mhfu_hook_rc_t mhfu_off_bigmonster_action_input (mhfu_bigmonster_action_input_cb_t cb);
144-
mhfu_hook_rc_t mhfu_off_bigmonster_action_decided(mhfu_bigmonster_action_decided_cb_t cb);
145-
mhfu_hook_rc_t mhfu_off_bigmonster_ai_step (mhfu_bigmonster_ai_step_cb_t cb);
146-
mhfu_hook_rc_t mhfu_off_bigmonster_action (mhfu_bigmonster_action_cb_t cb);
147-
mhfu_hook_rc_t mhfu_off_bigmonster_spawn (mhfu_bigmonster_spawn_cb_t cb);
148-
mhfu_hook_rc_t mhfu_off_bigmonster_death (mhfu_bigmonster_death_cb_t cb);
149-
mhfu_hook_rc_t mhfu_off_bigmonster_damaged (mhfu_bigmonster_damaged_cb_t cb);
120+
/* --- registration (events.h): higher priority runs first, ties in registration order --- */
121+
122+
static inline mhfu_hook_rc_t mhfu_on_ai_overlay_loaded(
123+
mhfu_ai_overlay_loaded_cb_t cb, int priority, const char *owner) {
124+
return mhfu_event_on(MHFU_EVENT_AI_OVERLAY_LOADED, (mhfu_event_fn_t)cb, priority, owner);
125+
}
126+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_slot_picked(
127+
mhfu_bigmonster_slot_picked_cb_t cb, int priority, const char *owner) {
128+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_SLOT_PICKED, (mhfu_event_fn_t)cb, priority, owner);
129+
}
130+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_action_input(
131+
mhfu_bigmonster_action_input_cb_t cb, int priority, const char *owner) {
132+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_ACTION_INPUT, (mhfu_event_fn_t)cb, priority, owner);
133+
}
134+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_action_decided(
135+
mhfu_bigmonster_action_decided_cb_t cb, int priority, const char *owner) {
136+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_ACTION_DECIDED, (mhfu_event_fn_t)cb, priority, owner);
137+
}
138+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_ai_step(
139+
mhfu_bigmonster_ai_step_cb_t cb, int priority, const char *owner) {
140+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_AI_STEP, (mhfu_event_fn_t)cb, priority, owner);
141+
}
142+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_action(
143+
mhfu_bigmonster_action_cb_t cb, int priority, const char *owner) {
144+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_ACTION, (mhfu_event_fn_t)cb, priority, owner);
145+
}
146+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_spawn(
147+
mhfu_bigmonster_spawn_cb_t cb, int priority, const char *owner) {
148+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_SPAWN, (mhfu_event_fn_t)cb, priority, owner);
149+
}
150+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_death(
151+
mhfu_bigmonster_death_cb_t cb, int priority, const char *owner) {
152+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_DEATH, (mhfu_event_fn_t)cb, priority, owner);
153+
}
154+
static inline mhfu_hook_rc_t mhfu_on_bigmonster_damaged(
155+
mhfu_bigmonster_damaged_cb_t cb, int priority, const char *owner) {
156+
return mhfu_event_on(MHFU_EVENT_BIGMONSTER_DAMAGED, (mhfu_event_fn_t)cb, priority, owner);
157+
}
150158

151159
/* What the picker last returned for vt8_input on this monster type (a pointer for Tigrex;
152160
* every pick is cached, 96 per type), or 0 before the engine has picked it once. */

‎framework/include/mhfu/bigmon_overlay.h‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* Load a second big-monster AI overlay (em*.ovl) at a fresh address: the engine loads every
2-
* em overlay to one slot (MHFU_EM_OVERLAY_SLOT), so a second family has nowhere else to go. */
2+
* em overlay to one load address, so a second family has nowhere else to go. */
33
#ifndef MHFU_BIGMON_OVERLAY_H
44
#define MHFU_BIGMON_OVERLAY_H
55

@@ -11,17 +11,16 @@ extern "C" {
1111

1212
typedef struct {
1313
int uid; /* partition block, or -1 (extra RAM, volatile RAM, native slot) */
14-
uint32_t region_base; /* relocated footprint base, MHFU_EM_OVERLAY_SLOT + delta */
1514
int32_t delta; /* 64 KB aligned */
16-
uint32_t new_load; /* relocated load address (file offset 0) */
17-
uint32_t img_size; /* .ovl file size */
18-
uint32_t foot_size; /* footprint including the BSS below and above the image */
15+
uint32_t new_load; /* relocated load address: the footprint starts here */
16+
uint32_t img_size; /* header, text and data */
17+
uint32_t foot_size; /* img_size plus bss */
1918
uint32_t si_start; /* relocated static-initializer list [si_start, si_end) */
2019
uint32_t si_end;
2120
} mhfu_ovl_region_t;
2221

2322
/* Read the overlay at path, place it (the native slot if free, else a partition block, the
24-
* extra-RAM window or volatile RAM), relocate it and zero its BSS. Idempotent per session.
23+
* extra-RAM window or volatile RAM), relocate it and zero its bss. Idempotent per session.
2524
* Returns 0, or negative. Does not wire it to the engine. */
2625
int mhfu_ovl_load_relocated(const char *path, mhfu_ovl_region_t *out);
2726

‎framework/include/mhfu/em_vhook.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* The em_vhook mod's surface: it wraps two slots of the spawned big monster's species vtable,
1+
/* em_vhook: the core wraps two slots of the spawned big monster's species vtable,
22
* MONSTER_VTABLE.ENTER_ACTION (provisions a behaviour pair) and .AI_STEP (every frame).
33
*
44
* substitution the engine's (main, id) is rewritten before enter-action runs, so our

‎framework/include/mhfu/entity.h‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/* Entity access: the registry (MHFU_ENTITY_REGISTRY; slot 0 is never the player, 0 = empty)
2-
* and typed fields. Field offsets are the generated MHFU_ENTITY_* macros. */
1+
/* Entity access: the registry (MHFU_ENTITY_REGISTRY, MHFU_ENTITY_REGISTRY_COUNT slots; slot 0 is
2+
* never the player, 0 = empty) and typed fields, whose offsets are the MHFU_ENTITY_* macros. */
33
#ifndef MHFU_ENTITY_H
44
#define MHFU_ENTITY_H
55

@@ -11,8 +11,6 @@
1111
extern "C" {
1212
#endif
1313

14-
#define MHFU_REGISTRY_SLOTS 21
15-
1614
uint32_t mhfu_entity_at(int slot); /* the pointer in slot, or 0 */
1715
int mhfu_entity_slot_of(uint32_t ent); /* ent's slot, or -1 */
1816
int mhfu_entity_is_alive(uint32_t ent); /* still in the registry */

‎framework/include/mhfu/events.h‎

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
1-
/* Fan-out events: the framework installs the anchors and poll threads, and every callback
2-
* registered for an event runs, higher priority first (mhfu_hook_event in hooks.h). */
1+
/* Events: one registry for every event the framework raises. A callback belongs to an owner,
2+
* the mod id, and the framework drops an owner's callbacks when that mod shuts down. Higher
3+
* priority runs first, ties in registration order. An event's hook is installed for its
4+
* first subscriber; a code patch then lands at the next title or menu screen. */
35
#ifndef MHFU_EVENTS_H
46
#define MHFU_EVENTS_H
57

68
#include <stdint.h>
79
#include "types.h"
10+
#include "hooks.h"
811

912
#ifdef __cplusplus
1013
extern "C" {
1114
#endif
1215

1316
typedef enum {
14-
MHFU_EVENT_QUEST_BEGINNING = 0, /* the player commits to a quest */
15-
MHFU_EVENT_QUEST_ENTERED = 1, /* area index becomes 98, the snowy-mountains camp */
16-
MHFU_EVENT_MAP_SECTION_ENTERED = 2, /* the area index changed */
17-
MHFU_EVENT_MONSTER_SPAWNED = 3, /* a new entity in registry slot 1 or above */
18-
MHFU_EVENT_QUEST_TARGETS_BUILDING = 4, /* edit the quest's monster list here (quest.h) */
17+
MHFU_EVENT_QUEST_BEGINNING, /* the player commits to a quest */
18+
MHFU_EVENT_QUEST_ENTERED, /* the first area after QUEST_BEGINNING */
19+
MHFU_EVENT_MAP_SECTION_ENTERED, /* the area index changed */
20+
MHFU_EVENT_MONSTER_SPAWNED, /* a new entity in registry slot 1 or above */
21+
MHFU_EVENT_QUEST_TARGETS_BUILDING, /* edit the quest's monster list here (quest.h) */
22+
/* big monsters, ai.h */
23+
MHFU_EVENT_AI_OVERLAY_LOADED,
24+
MHFU_EVENT_BIGMONSTER_SLOT_PICKED,
25+
MHFU_EVENT_BIGMONSTER_ACTION_INPUT,
26+
MHFU_EVENT_BIGMONSTER_ACTION_DECIDED,
27+
MHFU_EVENT_BIGMONSTER_AI_STEP,
28+
MHFU_EVENT_BIGMONSTER_ACTION,
29+
MHFU_EVENT_BIGMONSTER_SPAWN,
30+
MHFU_EVENT_BIGMONSTER_DEATH,
31+
MHFU_EVENT_BIGMONSTER_DAMAGED,
1932

2033
MHFU_EVENT_COUNT_
2134
} mhfu_event_id_t;
2235

23-
/* Quest events: the registers at the anchor instruction. */
36+
/* A callback of any event, stored untyped; the event calls it through its own type. */
37+
typedef void (*mhfu_event_fn_t)(void);
38+
39+
/* BADARG on a bad id, callback or owner; NOSPACE when the event is full; CONFLICT when its
40+
* hook cannot be installed. The same owner and callback again is OK and adds nothing. The
41+
* owner string is kept, not copied: a mod id or another literal. */
42+
mhfu_hook_rc_t mhfu_event_on(mhfu_event_id_t id, mhfu_event_fn_t cb, int priority,
43+
const char *owner);
44+
45+
/* Drop every callback owner registered; the framework calls it on mod shutdown. */
46+
void mhfu_event_release(const char *owner);
47+
48+
/* QUEST_BEGINNING and QUEST_ENTERED: the registers at the anchor instruction. */
2449
typedef struct {
2550
mhfu_event_id_t event_id;
2651
uint32_t cell_value;
@@ -42,45 +67,44 @@ typedef struct {
4267

4368
typedef struct {
4469
mhfu_event_id_t event_id;
45-
int slot; /* registry slot, 1..20 */
70+
int slot; /* registry slot, 1..MHFU_ENTITY_REGISTRY_COUNT-1 */
4671
uint32_t entity_ptr;
4772
uint8_t monster_type; /* ENTITY.SPECIES */
4873
uint8_t entity_id; /* ENTITY.ID */
4974
uint16_t hp; /* ENTITY.HP */
5075
float size_scale; /* ENTITY.SIZE_SCALE */
5176
} mhfu_monster_spawn_ctx_t;
5277

53-
/* Context of MHFU_EVENT_QUEST_TARGETS_BUILDING. */
5478
typedef struct {
5579
mhfu_event_id_t event_id;
5680
mhfu_quest_t quest;
5781
} mhfu_quest_ctx_t;
5882

59-
/* The registry stores this; the typed aliases below check signatures at the caller. */
60-
typedef void (*mhfu_event_cb_t)(const void *ctx);
83+
typedef void (*mhfu_quest_cb_t) (const mhfu_event_ctx_t *ctx);
84+
typedef void (*mhfu_map_section_cb_t) (const mhfu_map_section_ctx_t *ctx);
85+
typedef void (*mhfu_monster_spawn_cb_t) (const mhfu_monster_spawn_ctx_t *ctx);
86+
typedef void (*mhfu_quest_targets_cb_t) (const mhfu_quest_ctx_t *ctx);
6187

62-
typedef void (*mhfu_quest_cb_t) (const mhfu_event_ctx_t *ctx);
63-
typedef void (*mhfu_map_section_cb_t) (const mhfu_map_section_ctx_t *ctx);
64-
typedef void (*mhfu_monster_spawn_cb_t)(const mhfu_monster_spawn_ctx_t *ctx);
65-
66-
/* Priority 0. Returns 0, or negative on a bad id or a full event. */
67-
int mhfu_event_register(mhfu_event_id_t event_id, mhfu_event_cb_t cb);
68-
69-
static inline int mhfu_on_quest_beginning(mhfu_quest_cb_t cb) {
70-
return mhfu_event_register(MHFU_EVENT_QUEST_BEGINNING,
71-
(mhfu_event_cb_t)(void *)cb);
88+
static inline mhfu_hook_rc_t mhfu_on_quest_beginning(mhfu_quest_cb_t cb, int priority,
89+
const char *owner) {
90+
return mhfu_event_on(MHFU_EVENT_QUEST_BEGINNING, (mhfu_event_fn_t)cb, priority, owner);
91+
}
92+
static inline mhfu_hook_rc_t mhfu_on_quest_entered(mhfu_quest_cb_t cb, int priority,
93+
const char *owner) {
94+
return mhfu_event_on(MHFU_EVENT_QUEST_ENTERED, (mhfu_event_fn_t)cb, priority, owner);
7295
}
73-
static inline int mhfu_on_quest_entered(mhfu_quest_cb_t cb) {
74-
return mhfu_event_register(MHFU_EVENT_QUEST_ENTERED,
75-
(mhfu_event_cb_t)(void *)cb);
96+
static inline mhfu_hook_rc_t mhfu_on_map_section_entered(mhfu_map_section_cb_t cb,
97+
int priority, const char *owner) {
98+
return mhfu_event_on(MHFU_EVENT_MAP_SECTION_ENTERED, (mhfu_event_fn_t)cb, priority, owner);
7699
}
77-
static inline int mhfu_on_map_section_entered(mhfu_map_section_cb_t cb) {
78-
return mhfu_event_register(MHFU_EVENT_MAP_SECTION_ENTERED,
79-
(mhfu_event_cb_t)(void *)cb);
100+
static inline mhfu_hook_rc_t mhfu_on_monster_spawned(mhfu_monster_spawn_cb_t cb, int priority,
101+
const char *owner) {
102+
return mhfu_event_on(MHFU_EVENT_MONSTER_SPAWNED, (mhfu_event_fn_t)cb, priority, owner);
80103
}
81-
static inline int mhfu_on_monster_spawned(mhfu_monster_spawn_cb_t cb) {
82-
return mhfu_event_register(MHFU_EVENT_MONSTER_SPAWNED,
83-
(mhfu_event_cb_t)(void *)cb);
104+
static inline mhfu_hook_rc_t mhfu_on_quest_targets_building(mhfu_quest_targets_cb_t cb,
105+
int priority, const char *owner) {
106+
return mhfu_event_on(MHFU_EVENT_QUEST_TARGETS_BUILDING, (mhfu_event_fn_t)cb, priority,
107+
owner);
84108
}
85109

86110
#ifdef __cplusplus

0 commit comments

Comments
 (0)