Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions framework/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build/
30 changes: 30 additions & 0 deletions framework/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# build/mhfu_framework.prx and build/mhfu_boot.prx: `make -C framework`. Needs docker, uv and make;
# the image has no Python, so the address tables are rendered here first.

IMAGE = pspdev/pspdev:v20260901@sha256:b22811072ea0721d7f6c666a5a279b6def2b0cd95892b819d9570f8ffc4452c0
GEN = build/gen
ADDR = ../packages/mhfu/src/mhfu/addresses
PRXS = build/mhfu_framework.prx build/mhfu_boot.prx

.PHONY: all clean

# psp-prxgen copies EF_MIPS_ABI_EABI32 into the PRX, which old firmware rejects (0x800200D9)
all: $(GEN)/addresses.gen.h $(GEN)/addresses.lua
docker run --rm --platform linux/amd64 --user "$$(id -u):$$(id -g)" -v "$(CURDIR)":/work -w /work \
$(IMAGE) make -f Makefile.psp
uv run python tools/patch_prx_eflags.py $(PRXS)

$(GEN)/addresses.gen.h: $(ADDR).toml $(ADDR).py
@mkdir -p $(@D)
uv run python -m mhfu.addresses c -o $@

$(GEN)/addresses.gen.lua: $(ADDR).toml $(ADDR).py
@mkdir -p $(@D)
uv run python -m mhfu.addresses lua -o $@

# the copy the PRX embeds, without the doc comments (a third of the size)
$(GEN)/addresses.lua: $(GEN)/addresses.gen.lua
grep -v '^[[:space:]]*---' $< > $@

clean:
rm -rf build
70 changes: 70 additions & 0 deletions framework/Makefile.psp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# The PSP side of the build, run in the pspdev image by ./Makefile. Builds build/mhfu_framework.prx,
# then itself again with PRX=boot for build/mhfu_boot.prx (build_prx.mak takes one target per run).

PSPSDK = $(shell psp-config --pspsdk-path)
GEN = build/gen
OBJDIR = build/obj

ifeq ($(PRX),boot)

TARGET = build/mhfu_boot
OBJS = $(OBJDIR)/boot/main.o
INCDIR = include $(GEN)
CFLAGS = -Os -G0 -Wall
# the kernel ModuleMgr: the user one rejects a kernel-thread caller with 0x80020149
LIBS = $(PSPSDK)/lib/prxexports.o -lpspkernel -lpspsdk
GP0 =

else

TARGET = build/mhfu_framework

# Every mod directory compiles, so a disabled one cannot rot; only the manifest's mods link.
MODS := $(shell grep -vE '^[[:space:]]*(#|$$)' mods.manifest)
objs_in = $(patsubst %,$(OBJDIR)/%.o,$(basename $(sort $(wildcard $(addsuffix /*.cpp,$(1)) $(addsuffix /*.S,$(1))))))
CORE_OBJS := $(call objs_in,src/core)
MOD_OBJS := $(foreach m,$(MODS),$(call objs_in,mods/$(m)))
ALL_MODS := $(call objs_in,$(wildcard mods/*))
OBJS = $(CORE_OBJS) $(MOD_OBJS)

INCDIR = include src/core $(GEN)
CFLAGS = -Os -G0 -Wall -fno-strict-aliasing
# no exceptions, RTTI or static guards: the objects need no libstdc++ runtime
CXXFLAGS = -std=gnu++17 -fno-exceptions -fno-rtti -fno-threadsafe-statics
# .incbin in a mod's .S finds the Lua sources and the generated address table
ASFLAGS = -Wa,-Ilua,-I$(GEN)

# prxexports.o: real firmware rejects a PRX without the syslib export (0x800200D9); lua needs libm, libc
LIBS = $(PSPSDK)/lib/prxexports.o -lpspsdk -lpspuser -lpspusb -lpspnet -lpspnet_apctl -llua -lm -lc
# the prebuilt liblua.a uses small data; without GP threshold 0 the link overflows R_MIPS_GPREL16
GP0 = -Wl,-G,0

.PHONY: boot
all: boot $(filter-out $(OBJS),$(ALL_MODS))
boot:
$(MAKE) -f Makefile.psp PRX=boot

endif

include $(PSPSDK)/lib/build_prx.mak

# replaces build_prx.mak's flags, whose own -T linkfile.prx would clash with prx.lds;
# prx.lds is linkfile.prx that also drops .MIPS.abiflags and .reginfo, which old loaders reject
override LDFLAGS := $(addprefix -L,$(LIBDIR)) -Wl,-q,-T$(CURDIR)/prx.lds -nostartfiles -Wl,-zmax-page-size=128 $(GP0)

DEPFLAGS = -MMD -MP
$(OBJDIR)/%.o: %.cpp
@mkdir -p $(@D)
$(COMPILE.cc) $(DEPFLAGS) -o $@ $<
$(OBJDIR)/%.o: %.c
@mkdir -p $(@D)
$(COMPILE.c) $(DEPFLAGS) -o $@ $<
# -pipe keeps the assembler's --MD output to the .incbin files
$(OBJDIR)/%.o: %.S
@mkdir -p $(@D)
$(COMPILE.S) -pipe $(DEPFLAGS) -Wa,--MD,$(@:.o=.inc.d) -o $@ $<

# a deleted .incbin source rebuilds the object instead of stopping make
%.lua: ;

-include $(shell find $(OBJDIR) -name '*.d' 2>/dev/null)
55 changes: 55 additions & 0 deletions framework/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# framework

`mhfu_framework.prx`, the plugin that runs mods inside MHFU (EU, ULES01213): a C++ core, the mods
composed into it at build time, and a Lua host that runs mods from the memory stick.
`mhfu_boot.prx` loads it on a real PSP.

## Build

Needs docker, uv and make.

```bash
make -C framework # both PRXs, compiled in the pinned pspdev image
make -C framework clean
```

| Output | |
|---|---|
| `build/mhfu_framework.prx` | the plugin |
| `build/mhfu_boot.prx` | the real-PSP loader: a kernel plugin that loads the framework from its own directory once the game shows the title or a menu |
| `build/*.elf` | the same with symbols and relocations |
| `build/gen/addresses.gen.h`, `.lua` | the address map, rendered from `packages/mhfu`; Lua reads it as `mhfu.addr` |

`mods.manifest` lists the mods linked into the plugin, one directory under `mods/` per line; `#`
disables one. Every mod directory is compiled either way.

## Memory stick

```
PSP/PLUGINS/mhfu_framework/
mhfu_framework.prx
mhfu_boot.prx real PSP only
plugin.ini PPSSPP only
mods/*.lua Lua mods, loaded at boot and reloaded when they change
framework.log written at run time
```

`plugin.ini` for PPSSPP (it ignores a file without both sections; the addresses are EU only):

```ini
[games]
ULES01213 = true
[options]
type = prx
filename = mhfu_framework.prx
name = MHFU Framework
version = 1
```

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

```
ms0:/PSP/PLUGINS/mhfu_framework/mhfu_boot.prx 1
```

The plugin loads on a cold boot only; after loading a PPSSPP savestate it no longer runs.
115 changes: 115 additions & 0 deletions framework/boot/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* mhfu_boot.prx: the real-PSP entry, a small kernel plugin that loads mhfu_framework.prx from its
* own directory once the game shows the title or a menu. PPSSPP loads the framework directly.
*/
#include <pspkernel.h>
#include <pspsysmem.h>
#include <pspiofilemgr.h>
#include <pspthreadman.h>
#include <pspsdk.h>

#include "addresses.gen.h"
#include "mhfu/world.h"

/* Kernel mode: when plugins load the user partition is full (0x800200D9), the kernel one is not. */
PSP_MODULE_INFO("mhfu_boot", PSP_MODULE_KERNEL, 1, 1);
PSP_MAIN_THREAD_ATTR(0);

#define FRAMEWORK_NAME "mhfu_framework.prx"
#define FALLBACK_PATH "ms0:/PSP/PLUGINS/mhfu_framework/" FRAMEWORK_NAME
/* Load at the title or a menu: the framework installs its code hooks only while the JIT is cold
* there, so a load in-area installs none. */
/* Load anyway this many seconds after the settle if no menu is seen. */
#define LOAD_FALLBACK_S 60
/* The first ms0 I/O waits out the boot's disc read, during which it faults. */
#define BOOT_SETTLE_US (15 * 1000 * 1000)

/* libc-free helpers: this PRX links no newlib */
static unsigned slen(const char *s) { unsigned n = 0; while (s[n]) n++; return n; }
static char *u2d(char *p, unsigned v)
{
char t[12]; int i = 0;
if (!v) t[i++] = '0';
while (v) { t[i++] = (char)('0' + v % 10); v /= 10; }
while (i) *p++ = t[--i];
return p;
}
static char *puts_(char *p, const char *s) { while (*s) *p++ = *s++; return p; }

static void wlog(const char *line, int n)
{
SceUID fd = sceIoOpen("ms0:/PSP/mhfu_boot.txt",
PSP_O_WRONLY | PSP_O_CREAT | PSP_O_APPEND, 0777);
if (fd >= 0) { sceIoWrite(fd, line, n); sceIoClose(fd); }
}
static void log_msg(const char *s) { char b[160]; char *p = puts_(b, s); *p++ = '\n'; wlog(b, (int)(p - b)); }
static void log_kv(const char *s, unsigned v) { char b[160]; char *p = puts_(b, s); p = u2d(p, v); *p++ = '\n'; wlog(b, (int)(p - b)); }
static void log_path(const char *s, const char *path) { char b[320]; char *p = puts_(b, s); p = puts_(p, path); *p++ = '\n'; wlog(b, (int)(p - b)); }

static char g_path[256];

/* The framework's path: this plugin's directory (from argp) plus FRAMEWORK_NAME, as psplink does. */
static void build_path(SceSize args, void *argp)
{
g_path[0] = 0;
if (argp && args > 1) {
const char *a = (const char *)argp;
int i = 0, slash = -1;
while (a[i] && i < 200) { g_path[i] = a[i]; if (a[i] == '/') slash = i; i++; }
g_path[i] = 0;
if (slash >= 0) g_path[slash + 1] = 0; /* dir + trailing slash */
else g_path[0] = 0;
}
if (g_path[0] == 0) {
const char *f = FALLBACK_PATH; int i = 0;
while (f[i]) { g_path[i] = f[i]; i++; } g_path[i] = 0;
return;
}
{ unsigned dl = slen(g_path); const char *f = FRAMEWORK_NAME; int i = 0;
while (f[i] && dl + i < 255) { g_path[dl + i] = f[i]; i++; } g_path[dl + i] = 0; }
}

static int boot_thread(SceSize args, void *argp)
{
build_path(args, argp);
sceKernelDelayThread(BOOT_SETTLE_US);
log_path("[boot] framework=", g_path);

int i;
for (i = 0; i < 600; i++) { /* up to ~10 min, 1 s poll */
unsigned char scr = *(volatile unsigned char *)MHFU_SCREEN_STATE;
int quiet = (scr == MHFU_WORLD_SCREEN_MENU || scr == MHFU_WORLD_SCREEN_TITLE);
if (quiet || i >= LOAD_FALLBACK_S) {
log_kv(quiet ? "[boot] quiet menu load, screen_state="
: "[boot] fallback load, screen_state=", scr);
/* K1 cleared around the load so the path pointer passes the kernel's check */
unsigned int k1 = pspSdkSetK1(0);
SceUID mod = sceKernelLoadModule(g_path, 0, NULL);
if (mod >= 0) {
int st = sceKernelStartModule(mod, args, argp, NULL, NULL);
pspSdkSetK1(k1);
log_kv("[boot] loadmodule OK modid=", (unsigned)mod);
log_kv("[boot] startmodule rc=", (unsigned)st);
log_msg("[boot] framework loaded — done");
return 0;
}
pspSdkSetK1(k1);
log_kv("[boot] loadmodule FAILED rc=", (unsigned)mod);
return 0; /* a load failure does not heal, so no retry */
}
if ((i % 5) == 0) log_kv("[boot] waiting for menu, screen_state=", scr);
sceKernelDelayThread(1000 * 1000);
}
log_msg("[boot] GAVE UP — never reached in-area (screen_state never == 17)");
return 0;
}

int module_start(SceSize args, void *argp)
{
SceUID th = sceKernelCreateThread("mhfu_boot", boot_thread, 0x18, 0x2000, 0, NULL);
if (th >= 0) sceKernelStartThread(th, args, argp);
return 0;
}

int module_stop(SceSize args, void *argp) { (void)args; (void)argp; return 0; }
void _exit(int status) { (void)status; }
Loading
Loading