From 68138a78d8e185f1c45453f0ed9470b225897b5a Mon Sep 17 00:00:00 2001 From: Victor Mateus Date: Sun, 5 Jul 2026 11:14:06 -0300 Subject: [PATCH 1/2] Add support for booting ELFs from HDD (PFS) boot paths Boot paths like "hdd0:__common:pfs:/APPS/OPNPS2LD.ELF" previously hung on a red screen: the bootstrap loads ELFs with SifLoadElf and fio, and neither can interpret that path format nor read from pfs, which is an iomanX-only device invisible to rom0:LOADFILE/ioman. startgameExecute now recognizes hdd0:partition:pfs: paths, validates them with getMountInfo, remounts pfs0: on the target partition and checks the ELF exists, showing a proper error menu on failure instead of a red screen. The bootstrap gained LoadElfFromPFS, which loads the ELF manually through the fileXio RPC (still resident since the IOP is not reset on handoff), then performs the usual IOP reset and ExecPS2 with the original hdd0: path as argv[0], matching the uLaunchELF convention. Requires the HDD build and starting Cheat Device from the HDD, since the dev9/atad/ps2hdd/ps2fs modules are only loaded in that case; other configurations now show an explanatory error for hdd0: paths. Co-Authored-By: Claude Fable 5 --- README.md | 2 + bootstrap/Makefile | 2 + bootstrap/main.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++ src/startgame.c | 84 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70ce3bc..8746f82 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ using Cheat Device. ## Features * Easy to navigate menu system similar to CodeBreaker * Supports booting retail discs and loader ELFs +* HDD build: boot ELFs stored on PFS partitions with boot paths like + `hdd0:__common:pfs:/APPS/OPNPS2LD.ELF` (requires starting Cheat Device from the HDD) * Fast loading of large cheat lists * Save manager for backing up and restoring game saves to/from a flash drive * Powered by ps2rd's powerful cheat engine diff --git a/bootstrap/Makefile b/bootstrap/Makefile index 9b38d80..d4671c2 100644 --- a/bootstrap/Makefile +++ b/bootstrap/Makefile @@ -6,6 +6,8 @@ EE_OBJS = main.o #EE_LDFLAGS = -s -Ttext 0x000F3000 #EE_LIBS += -lkernel -lpatches -lerl +EE_LIBS += -lfileXio + EE_CFLAGS := -mips3 -ffreestanding -fno-builtin -G0 \ -fshort-double -mlong64 -mhard-float -mno-abicalls -O2 -EL -Wall \ $(EE_INCS) $(EE_CFLAGS) diff --git a/bootstrap/main.c b/bootstrap/main.c index 3cb5cd9..b0d1b43 100644 --- a/bootstrap/main.c +++ b/bootstrap/main.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -68,6 +69,94 @@ typedef struct { u32 align; } elf_pheader_t; +/* Loads an ELF from a PFS partition that Cheat Device left mounted on + * "pfs0:" before executing the bootstrap. SifLoadElf and fio can't see + * iomanX devices like pfs, so the ELF is loaded manually through the + * fileXio RPC, whose IOP module is still resident since the IOP was not + * reset. elfpath is in the "hdd0:partition:pfs:/path" form and is passed + * as-is to the loaded ELF as argv[0]. Only returns on failure, leaving + * the current stage's debug color on screen. */ +static void LoadElfFromPFS(char *elfpath) +{ + elf_header_t boot_header; + elf_pheader_t boot_pheader; + char pfspath[256]; + char *file; + char *args[1]; + int i, fd; + + GS_BGCOLOUR = red; /* RED: Opening elf */ + + file = strstr(elfpath, ":pfs:"); + if (file == NULL) + return; + + strcpy(pfspath, "pfs0:"); + strncat(pfspath, file + 5, sizeof(pfspath) - sizeof("pfs0:")); + + SifInitRpc(0); + if (fileXioInit() < 0) + return; + + fd = fileXioOpen(pfspath, O_RDONLY, 0); + if (fd < 0) + return; + + GS_BGCOLOUR = blue; /* BLUE: Installing elf header */ + if (fileXioRead(fd, (void *)&boot_header, sizeof(elf_header_t)) != sizeof(elf_header_t)) { + fileXioClose(fd); + return; + } + + GS_BGCOLOUR = white; + /* check ELF magic */ + if ((*(u32*)boot_header.ident) != ELF_MAGIC) { + fileXioClose(fd); + return; + } + + GS_BGCOLOUR = green; /* GREEN: Reading elf */ + /* copy loadable program segments to RAM */ + for (i = 0; i < boot_header.phnum; i++) { + fileXioLseek(fd, boot_header.phoff + (i * sizeof(elf_pheader_t)), SEEK_SET); + fileXioRead(fd, (void *)&boot_pheader, sizeof(elf_pheader_t)); + + if (boot_pheader.type != ELF_PT_LOAD) + continue; + + fileXioLseek(fd, boot_pheader.offset, SEEK_SET); + fileXioRead(fd, boot_pheader.vaddr, boot_pheader.filesz); + + if (boot_pheader.memsz > boot_pheader.filesz) + memset(boot_pheader.vaddr + boot_pheader.filesz, 0, + boot_pheader.memsz - boot_pheader.filesz); + } + + fileXioClose(fd); + fileXioUmount("pfs0:"); + + GS_BGCOLOUR = yellow; /* YELLOW: ExecPS2 */ + + /* IOP reboot routine from ps2rd */ + while (!SifIopReset("rom0:UDNL rom0:EELOADCNF", 0)) + ; + while (!SifIopSync()) + ; + + /* exit services */ + fioExit(); + SifExitIopHeap(); + SifLoadFileExit(); + SifExitRpc(); + SifExitCmd(); + + FlushCache(0); + FlushCache(2); + + args[0] = elfpath; + ExecPS2((u32*)boot_header.entry, 0, 1, args); +} + /* From NetCheat */ void MyLoadElf(char *elfpath) { @@ -88,6 +177,14 @@ void MyLoadElf(char *elfpath) /* Clear scratchpad memory */ memset((void*)0x70000000, 0, 16 * 1024); + /* ELFs on the HDD live on PFS partitions, which SifLoadElf and fio + can't read. Load them through fileXio instead. */ + if (elfpath[0] == 'h' && elfpath[1] == 'd' && elfpath[2] == 'd') + { + LoadElfFromPFS(elfpath); /* only returns on failure */ + SleepThread(); + } + /* HACK do not reset IOP when launching ELF from mass */ if (!(elfpath[0] == 'm' && elfpath[1] == 'a' && elfpath[2] == 's' && elfpath[3] == 's')) diff --git a/src/startgame.c b/src/startgame.c index 0b302a0..22d7582 100644 --- a/src/startgame.c +++ b/src/startgame.c @@ -15,6 +15,15 @@ #include "util.h" #include "objectpool.h" +#ifdef HDD +#include + +extern int booting_from_hdd; +extern int HDD_USABLE; +extern char MountPoint[40]; +int getMountInfo(char *path, char *mountString, char *mountPoint, char *newCWD); +#endif + typedef struct { u8 ident[16]; // struct definition for ELF object header u16 type; @@ -86,10 +95,66 @@ static void* loadBootstrap() return (void *)eh->entry; } +#ifdef HDD +// Prepares booting an ELF from an "hdd0:partition:pfs:/path" style boot path: +// remounts pfs0: on the target partition and checks that the ELF exists, so +// failures show an error menu here instead of hanging in the bootstrap. +// On failure, pfs0: is restored to the boot partition and 0 is returned. +static int setupHddBootPath(const char *path) +{ + char msg[192]; + char pathCopy[100]; + char partition[64]; + char pfsPath[128]; + const char *file; + int ret; + + if(!booting_from_hdd || !HDD_USABLE) + { + displayError("HDD boot paths can only be used when\nCheat Device was started from the HDD."); + return 0; + } + + file = strstr(path, ":pfs:"); + strncpy(pathCopy, path, sizeof(pathCopy) - 1); + pathCopy[sizeof(pathCopy) - 1] = '\0'; + + if(!file || !getMountInfo(pathCopy, NULL, partition, NULL)) + { + displayError("Invalid HDD boot path!\nExpected format:\nhdd0:partition:pfs:/path/to/boot.elf"); + return 0; + } + + fileXioUmount("pfs0:"); + ret = fileXioMount("pfs0:", partition, FIO_MT_RDONLY); + if(ret < 0) + { + snprintf(msg, sizeof(msg), "Error: failed to mount partition\n\"%s\" (%d)", partition, ret); + fileXioMount("pfs0:", MountPoint, FIO_MT_RDWR); + displayError(msg); + return 0; + } + + snprintf(pfsPath, sizeof(pfsPath), "pfs0:%s", file + 5); + ret = fileXioOpen(pfsPath, O_RDONLY, 0); + if(ret < 0) + { + snprintf(msg, sizeof(msg), "Error: couldn't open \"%s\"\non partition \"%s\" (%d)", file + 5, partition, ret); + fileXioUmount("pfs0:"); + fileXioMount("pfs0:", MountPoint, FIO_MT_RDWR); + displayError(msg); + return 0; + } + fileXioClose(ret); + + return 1; +} +#endif + void startgameExecute(const char *path) { static char boot2[100]; - + if(strcmp(path, "==Disc==") == 0) { if(!discPrompt()) @@ -143,11 +208,26 @@ void startgameExecute(const char *path) } else { - strncpy(boot2, path, 100); + strncpy(boot2, path, sizeof(boot2) - 1); + boot2[sizeof(boot2) - 1] = '\0'; } + // Databases and settings must be saved before an HDD boot path gets a + // chance to remount pfs0: on another partition. cheatsSaveDatabase(); settingsSave(NULL, 0); + + if(strncmp(boot2, "hdd0:", 5) == 0) + { +#ifdef HDD + if(!setupHddBootPath(boot2)) + return; +#else + displayError("HDD boot paths are only supported by\nthe HDD build of Cheat Device."); + return; +#endif + } + cheatsInstallCodesForEngine(); killMenus(); killCheats(); From 09c8bc4cdb264bb5167dae869bfd9ccda20e6850 Mon Sep 17 00:00:00 2001 From: Victor Mateus Date: Mon, 6 Jul 2026 12:31:24 -0300 Subject: [PATCH 2/2] Fix HDD boot detection when launched from OPL's APPS tab OPL launches apps through ps2sdk's elf-loader, whose loader concatenates the partition and file path into argv[0], producing e.g. "hdd0:+OPL:pfs0:APPS/CheatDevice-HDD.ELF". CheatDevice only recognized the uLaunchELF-style ":pfs:" marker, so booting_from_hdd stayed 0, the dev9/atad/ps2hdd/ps2fs modules were never loaded, and main() still tried to mount the partition, failing with a misleading 'failed to mount partition "hdd0:+OPL" err:-19' (ENODEV, since the pfs device was never registered). The new pfsPathGetFilePart() helper accepts both ":pfs:" and ":pfsN:" markers and is now used for HDD boot detection, for Start Game HDD boot paths, and by the bootstrap's PFS loader, so OPL-style paths work everywhere. Additionally: - main() no longer parses the boot path or mounts pfs0: when not booted from HDD, removing the spurious boot path/mount errors when the HDD build is started from mc/mass. - loadModules() now reports DEV9.IRX load failures and bad HDD status with a nonzero return so the real error is displayed instead of a misleading mount failure later. - util.c declared 'extern char* error' for main.c's 'char error[255]', so any error sprintf in util.c would have written through a garbage pointer; it is now declared with the correct array type. Co-Authored-By: Claude Fable 5 --- bootstrap/main.c | 12 ++++++++++-- src/main.c | 11 ++++++++--- src/startgame.c | 6 +++--- src/util.c | 28 +++++++++++++++++++++++++++- src/util.h | 6 ++++++ 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/bootstrap/main.c b/bootstrap/main.c index b0d1b43..6dd2f9b 100644 --- a/bootstrap/main.c +++ b/bootstrap/main.c @@ -87,12 +87,20 @@ static void LoadElfFromPFS(char *elfpath) GS_BGCOLOUR = red; /* RED: Opening elf */ - file = strstr(elfpath, ":pfs:"); + /* Accept both the uLaunchELF-style ":pfs:" marker and the OPL/ + elf-loader-style ":pfs0:" one. */ + file = strstr(elfpath, ":pfs"); if (file == NULL) return; + file += 4; + while (*file >= '0' && *file <= '9') + file++; + if (*file != ':') + return; + file++; strcpy(pfspath, "pfs0:"); - strncat(pfspath, file + 5, sizeof(pfspath) - sizeof("pfs0:")); + strncat(pfspath, file, sizeof(pfspath) - sizeof("pfs0:")); SifInitRpc(0); if (fileXioInit() < 0) diff --git a/src/main.c b/src/main.c index 15a8f1f..567a09b 100644 --- a/src/main.c +++ b/src/main.c @@ -36,15 +36,19 @@ int main(int argc, char *argv[]) #endif #ifdef HDD DPRINTF("Checking if booting from HDD\n"); - if (argc > 0) booting_from_hdd = (strstr(argv[0], "hdd0:")!=NULL)&&(strstr(argv[0], ":pfs:")!=NULL); + // uLaunchELF-style launchers pass "hdd0:partition:pfs:/path/file.elf" + // while OPL (through ps2sdk's elf-loader) passes + // "hdd0:partition:pfs0:path/file.elf", so accept both pfs markers. + if (argc > 0) booting_from_hdd = (strstr(argv[0], "hdd0:")!=NULL)&&(pfsPathGetFilePart(argv[0])!=NULL); DPRINTF("Booting from hdd:%d\n", booting_from_hdd); + if (booting_from_hdd) { char* BUF = NULL; BUF = strdup(argv[0]); //use strdup, otherwise, path will become `hdd0:` if (BUF==NULL) { DPRINTF("Could not strdup()\n"); } if (getMountInfo(BUF, NULL, MountPoint, pfspath)) { - + DPRINTF("MountPoint '%s'\npfspath '%s'\n", MountPoint, pfspath); char *pos = strrchr(pfspath, '/'); if (pos != NULL) { @@ -61,12 +65,13 @@ int main(int argc, char *argv[]) chdir(pfspath); } else displayError("Error processing HDD boot path (2)"); } else displayError("Error processing HDD boot path (1)"); + } #endif ret = loadModules(booting_from_hdd); if (ret != 0) displayError(error); #ifdef HDD - if (ret == 0) { + if (ret == 0 && booting_from_hdd) { int mtret=0; if ((mtret=fileXioMount("pfs0:", MountPoint, FIO_MT_RDWR)) < 0) { sprintf(error, "Error: failed to mount partition \"%s\"!\nerr:%d (0x%x)", MountPoint, mtret, mtret); diff --git a/src/startgame.c b/src/startgame.c index 22d7582..31ebf0d 100644 --- a/src/startgame.c +++ b/src/startgame.c @@ -115,7 +115,7 @@ static int setupHddBootPath(const char *path) return 0; } - file = strstr(path, ":pfs:"); + file = pfsPathGetFilePart(path); strncpy(pathCopy, path, sizeof(pathCopy) - 1); pathCopy[sizeof(pathCopy) - 1] = '\0'; @@ -135,11 +135,11 @@ static int setupHddBootPath(const char *path) return 0; } - snprintf(pfsPath, sizeof(pfsPath), "pfs0:%s", file + 5); + snprintf(pfsPath, sizeof(pfsPath), "pfs0:%s", file); ret = fileXioOpen(pfsPath, O_RDONLY, 0); if(ret < 0) { - snprintf(msg, sizeof(msg), "Error: couldn't open \"%s\"\non partition \"%s\" (%d)", file + 5, partition, ret); + snprintf(msg, sizeof(msg), "Error: couldn't open \"%s\"\non partition \"%s\" (%d)", file, partition, ret); fileXioUmount("pfs0:"); fileXioMount("pfs0:", MountPoint, FIO_MT_RDWR); displayError(msg); diff --git a/src/util.c b/src/util.c index 535ee8c..dc33ddf 100644 --- a/src/util.c +++ b/src/util.c @@ -8,6 +8,7 @@ #include "cheats.h" #include "settings.h" #include +#include #include #include #include @@ -18,7 +19,7 @@ #include #include -extern char* error; +extern char error[255]; #define EXTERN_BIN2O(_name_) extern u8 _name_##_start[]; extern int _name_##_size; #define LOAD_IRX_BUF(_irx_, ARGC, ARGV, RET) SifExecModuleBuffer(_irx_##_start, _irx_##_size, ARGC, ARGV, RET) #define LOAD_IRX_BUF_NARG(_irx_, RET) LOAD_IRX_BUF(_irx_, 0, NULL, RET) @@ -105,9 +106,28 @@ void poweroffCallback(void *arg) } #endif +const char *pfsPathGetFilePart(const char *path) +{ + const char *p = strstr(path, ":pfs"); + if(!p) + return NULL; + + p += strlen(":pfs"); + while(*p >= '0' && *p <= '9') + p++; // Skip the mount point number, if any + + if(*p != ':') + return NULL; + + return p + 1; +} + int loadModules(int booting_from_hdd) { int ID, RET, HDDSTAT, filexio_loaded=0, dev9_loaded=0, mmceman_loaded=0; +#ifdef DEV9 + int dev9_id=0, dev9_ret=0; +#endif DPRINTF("\n ** Loading main modules **\n"); /* IOP reset routine taken from ps2rd */ @@ -151,6 +171,8 @@ int loadModules(int booting_from_hdd) if (booting_from_hdd) { ID = LOAD_IRX_BUF_NARG(_ps2dev9_irx, &RET); dev9_loaded = IRX_LOAD_SUCCESS(); + dev9_id = ID; + dev9_ret = RET; } #endif @@ -240,7 +262,11 @@ int loadModules(int booting_from_hdd) } } else { sprintf(error, "HDD Init Error:\nHDD Status: %d (%s)", HDDSTAT, HDDerr(HDDSTAT)); + return -6; } + } else { + sprintf(error, "HDD Init error\n%s: ID:%d, RET_%d!", "DEV9.IRX", dev9_id, dev9_ret); + return -7; } } #endif diff --git a/src/util.h b/src/util.h index 9a141cf..067842d 100644 --- a/src/util.h +++ b/src/util.h @@ -58,6 +58,12 @@ int getNumLines(const char *str); void replaceIllegalChars(const char *str, char* valid, char replacement); // Remove trailing whitespace from str. char* rtrim(char *str); +// Get the file path portion following the ":pfs:" or ":pfsN:" marker in an +// "hdd0:partition:pfs:path" style boot path, or NULL if there is no marker. +// uLaunchELF-style launchers use ":pfs:" while OPL (through ps2sdk's +// elf-loader) produces ":pfs0:". +const char *pfsPathGetFilePart(const char *path); + // Get file extension from filename. Returns null if filename doesn't have an extension. const char* getFileExtension(const char *filename); // Get file path without extension.