diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8a66052 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,82 @@ +name: Build + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + container: ghcr.io/ps2homebrew/ps2homebrew:main + steps: + - uses: actions/checkout@v4 + + - name: Fetch full clone + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git fetch --prune --unshallow + + - name: Compile project + id: make + run: | + make + + - name: Upload ELF + uses: actions/upload-artifact@v4 + if: steps.make.outcome == 'success' + with: + name: nhddl + path: | + nhddl-*.elf + !nhddl-*_unc.elf + + - name: Upload YAML examples + if: steps.make.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: examples + path: | + examples/*.yaml + + release: + needs: [build] + runs-on: ubuntu-latest + permissions: write-all + if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Fetch full clone + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git fetch --prune --unshallow + + - name: Get git describe + run: | + echo "GIT_VERSION=$(git describe --always --dirty --tags --exclude nightly)" >> $GITHUB_ENV + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Prepare release archive + run: | + mv nhddl/nhddl-${{ env.GIT_VERSION }}.elf nhddl.elf + zip -r nhddl-${{ env.GIT_VERSION }}.zip nhddl.elf examples + + - uses: "marvinpinto/action-automatic-releases@latest" + if: github.ref == 'refs/heads/main' + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "nightly" + prerelease: true + title: "Nightly build" + files: | + nhddl-${{ env.GIT_VERSION }}.zip + + - uses: "marvinpinto/action-automatic-releases@latest" + if: startsWith(github.ref, 'refs/tags/v') + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + draft: true + files: | + nhddl-${{ env.GIT_VERSION }}.zip \ No newline at end of file diff --git a/.gitignore b/.gitignore index c619c09..806f0d5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ obj/ *.irx *.yaml *.toml +!examples/*.yaml # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux ### Linux ### diff --git a/Makefile b/Makefile index c35cacb..f74074f 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,15 @@ #.SILENT: -EE_BIN = nhddl_unc.elf -EE_BIN_PKD = nhddl.elf +GIT_VERSION := $(shell git describe --always --dirty --tags --exclude nightly) -EE_OBJS = main.o module_init.o common.o iso.o history.o options.o gui.o pad.o launcher.o +ELF_BASE_NAME := nhddl-$(GIT_VERSION) + +EE_BIN = $(ELF_BASE_NAME)_unc.elf +EE_BIN_PKD = $(ELF_BASE_NAME).elf +EE_BIN_DEBUG := $(ELF_BASE_NAME)-debug_unc.elf +EE_BIN_DEBUG_PKD := $(ELF_BASE_NAME)-debug.elf + +EE_OBJS = main.o module_init.o common.o iso.o history.o options.o gui.o pad.o launcher.o iso_cache.o IRX_FILES += sio2man.irx mcman.irx mcserv.irx fileXio.irx iomanX.irx freepad.irx RES_FILES += icon_A.sys icon_C.sys icon_J.sys @@ -18,12 +24,28 @@ EE_OBJS := $(EE_OBJS:%=$(EE_OBJS_DIR)%) EE_INCS := -Iinclude -I$(PS2DEV)/gsKit/include -I$(PS2SDK)/ports/include EE_LDFLAGS := -L$(PS2DEV)/gsKit/lib -L$(PS2SDK)/ports/lib -s -EE_LIBS = -ldebug -lfileXio -lpatches -lelf-loader -lgsKit -ldmaKit -lgskit_toolkit -lpng -lz -ltiff -lpad -EE_CFLAGS := -mno-gpopt -G0 +EE_LIBS = -ldebug -lfileXio -lpatches -lelf-loader -lgskit -ldmakit -lgskit_toolkit -lpng -lz -ltiff -lpad +EE_CFLAGS := -mno-gpopt -G0 -DGIT_VERSION="\"${GIT_VERSION}\"" BIN2C = $(PS2SDK)/bin/bin2c -.PHONY: all clean +NEEDS_REBUILD := 0 +ifeq ($(DEBUG), 1) +# If DEBUG=1, output targets to debug names + EE_BIN = $(EE_BIN_DEBUG) + EE_BIN_PKD = $(EE_BIN_DEBUG_PKD) +# Define DEBUG + EE_CFLAGS += -DDEBUG +# Set rebuild flag + NEEDS_REBUILD = 1 +else ifneq ("$(wildcard $(EE_BIN_DEBUG))","") +# Else, set rebuild flag if EE_BIN_DEBUG exists + NEEDS_REBUILD = 1 +endif + +.PHONY: all clean .FORCE + +.FORCE: all: $(EE_BIN_PKD) @@ -31,7 +53,7 @@ $(EE_BIN_PKD): $(EE_BIN) ps2-packer $< $@ clean: - rm -rf $(EE_BIN) $(EE_BIN_PKD) $(EE_ASM_DIR) $(EE_OBJS_DIR) + rm -rf $(EE_BIN) $(EE_BIN_PKD) $(EE_BIN_DEBUG) $(EE_BIN_DEBUG_PKD) $(EE_ASM_DIR) $(EE_OBJS_DIR) # IRX files %_irx.c: @@ -46,8 +68,14 @@ $(EE_ASM_DIR): $(EE_OBJS_DIR): @mkdir -p $@ +ifeq ($(NEEDS_REBUILD),1) +# If rebuild flag is set, add .FORCE to force full rebuild +$(EE_OBJS_DIR)%.o: $(EE_SRC_DIR)%.c .FORCE | $(EE_OBJS_DIR) + $(EE_CC) $(EE_CFLAGS) $(EE_INCS) -c $< -o $@ +else $(EE_OBJS_DIR)%.o: $(EE_SRC_DIR)%.c | $(EE_OBJS_DIR) $(EE_CC) $(EE_CFLAGS) $(EE_INCS) -c $< -o $@ +endif $(EE_OBJS_DIR)%.o: $(EE_ASM_DIR)%.c | $(EE_OBJS_DIR) $(EE_CC) $(EE_CFLAGS) $(EE_INCS) -c $< -o $@ diff --git a/README.md b/README.md index 62f0526..0953ae8 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,37 @@ # NHDDL — a PS2 exFAT BDM launcher for Neutrino -NHDDL is a memory card-based launcher that scans FAT-formatted BDM devices for ISO files, +NHDDL is a memory card-based launcher that scans _FAT/exFAT-formatted_ BDM devices for ISO files, lists them and boots selected ISO via Neutrino. It displays visual Game ID to trigger per-game settings on the Pixel FX line of products and writes to memory card history file before launching the title, triggering per-title memory cards on SD2PSX and MemCard PRO 2. -## Why it exists - -I have a SCPH-70000 PS2 with internal IDE—microSD mod and RetroGEM installed and I'm tired of dealing with APA-formatted drives. - -## What this is not - -This not an attempt at making a Neutrino-based Open PS2 Loader replacement. +Note that this not an attempt at making a Neutrino-based Open PS2 Loader replacement. It __will not__ boot ISOs from anything other than BDM devices. GSM, PADEMU, IGR and other stuff is out-of-scope of this launcher. ## Usage -Just put the ELF file into Neutrino folder on your memory card and launch it. -By default, NHDDL initializes ATA modules and looks for ISOs on internal HDD. +- Get the [latest Neutrino release](https://github.com/rickgaiser/neutrino/releases) +- Get the [latest `nhddl.elf`](https://github.com/pcm720/nhddl/releases) +- Unpack Neutrino release +- Copy `nhddl.elf` to Neutrino folder next to `neutrino.elf` +- _Additional step if you need USB, MX4SIO or UDPBD_: + Modify `nhddl.yaml` [accordingly](#common-use-cases) and copy it next to `nhddl.elf` +- Copy Neutrino folder to your PS2 memory card. + Any folder (e.g. `APPS`) will do, it doesn't have to be in the root of your memory card. + +Updating `nhddl.elf` is as simple as replacing `nhddl.elf` with the latest version. ### Supported BDM devices NHDDL reuses Neutrino modules for BDM support and requires them to be present in Neutrino `modules` directory. +These files should already be present in Neutrino release ZIP by default. + +By default, NHDDL initializes ATA modules and looks for ISOs on internal FAT/exFAT-formatted HDD, devices other than ATA require additional configuration. +See [this](#launcher-configuration-file) section for details on `nhddl.yml`. #### ATA +This is the default device mode. Make sure that Neutrino `modules` directory contains the following IRX files: - `bdm.irx` - `isofs.irx` @@ -39,6 +46,8 @@ The following files are required for MX4SIO: - `bdmfs_fatfs.irx` - `mx4sio_bd_mini.irx` +`mode: mx4sio` must be present in `nhddl.yaml`. + #### USB The following files are required for USB: - `bdm.irx` @@ -47,6 +56,8 @@ The following files are required for USB: - `usbd_mini.irx` - `usbmass_bd_mini.irx` +`mode: usb` must be present in `nhddl.yaml`. + #### UDPBD The following files are required for UDPBD: - `bdm.irx` @@ -55,6 +66,8 @@ The following files are required for UDPBD: - `dev9_ns.irx` - `smap_udpbd.irx` +`mode: udpbd` must be present in `nhddl.yaml`. + UDPBD module requires PS2 IP address to work. NHDDL attempts to retrieve PS2 IP address from the following sources: - `udpbd_ip` flag in `nhddl.yml` @@ -62,12 +75,19 @@ NHDDL attempts to retrieve PS2 IP address from the following sources: `udpbd_ip` flag takes priority over `IPCONFIG.DAT`. -See [this](#launcher-configuration-file) section for details on `nhddl.yml`. - ### Storing ISO -Just like OPL, NHDDL looks for ISOs in directories named `CD` or `DVD`. -There is no distinction between CD and DVD images, both can be placed in any of those two directories. +ISOs can be stored almost anywhere on the storage device. +Only directories that start with `.`, `$` and the following directories are ignored: + - `nhddl` + - `APPS` + - `ART` + - `CFG` + - `CHT` + - `LNG` + - `THM` + - `VMC` + - `XEBPLUS` ### Displaying cover art @@ -82,85 +102,114 @@ NHDDL uses YAML-like files to load and store its configuration options. ### Launcher configuration file Launcher configuration is read from the `nhddl.yaml` file, which must be located in the same directory as `nhddl.elf`. -This file is completely optional and must be used only to enable 480p or use any device other than ATA. +This file is _completely optional_ and must be used only to enable 480p in NHDDL UI or switch NHDDL mode to something other than `ata`. By default, 480p is disabled and the ATA device is used to look for ISO files. -Example of a valid config file: -```yaml -480p: # if this flag exists and is not disabled, enables 480 output -mode: ata # supported modes: ata (default), mx4sio, udpbd, usb -udpbd_ip: 192.168.1.6 # PS2 IP address for UDPBD mode -``` -To disable a flag, you can add `$` before the argument name or just comment it out with `#`. +To disable a flag, you can just comment it out with `#`. + +See [this file](examples/nhddl.yaml) for an example of a valid `nhddl.yaml` file. ### Configuration files on storage device -NHDDL stores its Neutrino-related config files in `config` directory in the root of BDM device. +NHDDL stores and looks for ISO-related config files in `nhddl` directory in the root of your BDM drive. -#### lastTitle.txt +#### `lastTitle.txt` -To point to the last launched title, NHDDL writes the full ISO path to `lastTitle.txt`. +This file stores the full path of the last launched title and is used to automatically navigate to it each time NHDDL starts up. This file is created automatically. +#### `cache.bin` + +Contains title ID cache for all ISOs located during the previous launch, making building ISO list way faster. +This file is also created automatically. + #### Argument files These files store arbitrary arguments that are passed to Neutrino on title launch. -For a list of valid arguments, see Neutrino README. +Arguments stored in those files __are passed to `neutrino.elf` as-is__. -Example of a valid file: +_For a list of valid arguments, see Neutrino README._ + +Example of a valid argument file: ```yaml # All flags are passed to neutrino as-is for future-proofing, comments are ignored -# Empty values are treated as a simple flag gc: 2 -mc0: mass:/memcard0.bin # file located on HDD -$mc1: mass:/memcard1.bin # disabled argument +mc0: mass:/memcard0.bin # all file paths must always start with mass: +$mc1: mass:/memcard1.bin # this argument is disabled # Arguments that don't have a value +# Empty values are treated as a simple flag dbc: logo: ``` -To disable an argument by default, place a `$` before the argument name. +To be able to parse those arguments and allow you to dynamically enable or disable them in UI, +NHDDL uses a dollar sign (`$`) to mark arguments as enabled or disabled by default. +Only enabled arguments get passed to Neutrino. NHDDL supports two kinds of argument files: #### global.yaml -Neutrino arguments that need to be applied to every ISO by default are stored in `global.yaml` file - -#### Title-specific files +Arguments stored in `nhddl/global.yaml` are applied to every ISO by default. -Neutrino arguments that need to be applied to a specific ISO are loaded from either -`.yaml` or `<anything>.yaml`. +#### ISO-specific files -File that has the same name as ISO has the priority. -NHDDL creates this file automatically when title options are modified and saved in UI. +Arguments stored in `nhddl/<ISO name>.yaml` are applied to every ISO that starts with `<ISO name>`. -#### BDM device file structure example +NHDDL can create this file automatically when title compatibility modes are modified and saved in UI. -`#` marks a comment +#### Example of directory sturcture on BDM device ``` ART/ # cover art, optional | - - SLUS_200.02_COV.jpg - - SLUS_202.28_COV.png - - SLUS_213.86_COV.jpg -config/ + - SLUS_200.02_COV.png +nhddl/ | - lastTitle.txt # created automatically - - global.yaml # optional, applies to all ISOs - - Silent Hill 2.yaml # optional, applies only to ISOs that start with "Silent Hill 2" - - SLUS_213.86.yaml # optional, applies to all ISOs that have a title ID of SLUS_213.86 + - cache.bin # created automatically + - global.yaml # optional argument file, applies to all ISOs + - Silent Hill 2.yaml # optional argument file, applies only to ISOs that start with "Silent Hill 2" CD/ | — Ridge Racer V.iso DVD/ | - Silent Hill 2.iso - - TOTA.iso # SLUS_213.86 - - TOTA UNDUB.iso # SLUS_213.86 + - TimeSplitters.iso ``` +## Common use cases + +### Switching NHDDL to USB mode + +To switch NHDDL to USB mode, you'll need to create `nhddl.yaml` with the following contents: +```yaml +mode: usb +``` +Copy this file to Neutrino directory next to `nhddl.elf`. + +### Switching NHDDL to MX4SIO mode + +To switch NHDDL to MX4SIO mode, you'll need to create `nhddl.yaml` with the following contents: +```yaml +mode: mx4sio +``` +Copy this file to Neutrino directory next to `nhddl.elf`. + +### Switching NHDDL to UDPBD mode + +To switch NHDDL to UDPBD mode, you'll need to create `nhddl.yaml` with the following contents: +```yaml +mode: udpbd +udpbd_ip: <PS2 IP address> +``` + +If you've previously set up the network via uLaunchELF and your memory card +has `SYS-CONF/IPCONFIG.DAT` file, you don't have to add `udpbd_ip`. + +Copy this file to the Neutrino directory next to `nhddl.elf`. + ## UI screenshots <details> diff --git a/examples/nhddl.yaml b/examples/nhddl.yaml new file mode 100644 index 0000000..49768f9 --- /dev/null +++ b/examples/nhddl.yaml @@ -0,0 +1,3 @@ +#480p: # uncomment to enable 480p in NHDDL UI +mode: ata # supported modes: ata (default), mx4sio, udpbd, usb +#udpbd_ip: 192.168.1.6 # PS2 IP address for UDPBD mode (commented out) \ No newline at end of file diff --git a/include/common.h b/include/common.h index a553171..1e4c841 100644 --- a/include/common.h +++ b/include/common.h @@ -20,8 +20,11 @@ typedef struct { // Storage device base path. Initialized in main.c extern const char STORAGE_BASE_PATH[]; +extern const size_t STORAGE_BASE_PATH_LEN; // ELF base path. Initialized in main() during init. extern char ELF_BASE_PATH[PATH_MAX + 1]; +// Path to Neutrino ELF. Initialized in main() during init. +extern char NEUTRINO_ELF_PATH[PATH_MAX+1]; // Options extern LauncherOptions LAUNCHER_OPTIONS; diff --git a/include/gui.h b/include/gui.h index 66b1623..8fd101e 100644 --- a/include/gui.h +++ b/include/gui.h @@ -4,7 +4,7 @@ #include "iso.h" int uiInit(); -int uiLoop(struct TargetList *titles); +int uiLoop(TargetList *titles); void uiCleanup(); #endif \ No newline at end of file diff --git a/include/iso.h b/include/iso.h index f4ff508..7def5d3 100644 --- a/include/iso.h +++ b/include/iso.h @@ -15,23 +15,22 @@ typedef struct Target { } Target; // A linked list of launch candidates -struct TargetList { +typedef struct { int total; // Total number of targets - struct Target *first; // First target - struct Target *last; // Last target -}; + Target *first; // First target + Target *last; // Last target +} TargetList; // Generates a list of launch candidates found in STORAGE_BASE_PATH -struct TargetList *findISO(); +TargetList *findISO(); // Completely frees TargetList. Passed pointer will not be valid after this function executes -void freeTargetList(struct TargetList *result); +void freeTargetList(TargetList *result); // Finds target with given index in the list and returns a pointer to it -struct Target *getTargetByIdx(struct TargetList *targets, int idx); - +Target *getTargetByIdx(TargetList *targets, int idx); // Makes and returns a deep copy of src without prev/next pointers. -struct Target *copyTarget(struct Target *src); +Target *copyTarget(Target *src); #endif \ No newline at end of file diff --git a/include/iso_cache.h b/include/iso_cache.h new file mode 100644 index 0000000..61c6bf0 --- /dev/null +++ b/include/iso_cache.h @@ -0,0 +1,30 @@ +#ifndef _TITLE_CACHE_H_ +#define _TITLE_CACHE_H_ + +#include "iso.h" + +typedef struct { + char titleID[12]; + char *fullPath; +} CacheEntry; + +typedef struct TitleIDCache { + int total; // Total number of elements in cache + int lastMatchedIdx; // Used to skip ahead to the last matched entry when getting title ID from cache + CacheEntry *entries; // Pointer to cache entry array +} TitleIDCache; + +// Saves TargetList into title ID cache +int storeTitleIDCache(TargetList *list); + +// Loads title ID cache from storage into cache +int loadTitleIDCache(TitleIDCache *cache); + +// Returns a pointer to title ID or NULL if path doesn't exist in cache +char *getCachedTitleID(char *fullPath, TitleIDCache *cache); + +// Frees memory used by title ID cache +// All pointers to cache entries (including title IDs) will be invalid +void freeTitleCache(TitleIDCache *cache); + +#endif \ No newline at end of file diff --git a/include/launcher.h b/include/launcher.h index 69c6914..d7f2b67 100644 --- a/include/launcher.h +++ b/include/launcher.h @@ -6,6 +6,6 @@ // Launches target, passing arguments to Neutrino. // Expects arguments to be initialized -void launchTitle(struct Target *target, struct ArgumentList *arguments); +void launchTitle(Target *target, ArgumentList *arguments); #endif \ No newline at end of file diff --git a/include/module_init.h b/include/module_init.h index 77a8849..0efeb9f 100644 --- a/include/module_init.h +++ b/include/module_init.h @@ -1,19 +1,6 @@ #ifndef _HDD_H_ #define _HDD_H_ -// Macros for loading embedded IOP modules -#define IRX_DEFINE(mod) \ - extern unsigned char mod##_irx[] __attribute__((aligned(16))); \ - extern unsigned int size_##mod##_irx - -#define IRX_LOAD(mod) \ - logString("\tloading " #mod "\n"); \ - if (SifExecModuleBuffer(mod##_irx, size_##mod##_irx, 0, NULL, &iopret) < 0) \ - return ret; \ - if (iopret) { \ - return iopret; \ - } - // Initializes Memory Card modules required to load HDD modules and neutrino ELF int init(); diff --git a/include/options.h b/include/options.h index e03dbe0..cdb9253 100644 --- a/include/options.h +++ b/include/options.h @@ -2,6 +2,12 @@ #define _OPTIONS_H_ #include <iso.h> +#include <ps2sdkapi.h> + + +// Location of configuration directory relative to STORAGE_BASE_PATH +extern const char BASE_CONFIG_PATH[]; +extern const size_t BASE_CONFIG_PATH_LEN; // Compatibility modes definitions @@ -31,11 +37,15 @@ typedef struct Argument { } Argument; // A linked list of options from config file -struct ArgumentList { +typedef struct { int total; // Total number of arguments - struct Argument *first; // First target - struct Argument *last; // Last target -}; + Argument *first; // First target + Argument *last; // Last target +} ArgumentList; + +// Writes full path to targetFileName into targetPath. +// If targetFileName is NULL, will return path to config directory +void buildConfigFilePath(char *targetPath, const char *targetFileName); // Sets last launched title path in global config int updateLastLaunchedTitle(char *titlePath); @@ -45,48 +55,48 @@ int getLastLaunchedTitle(char *titlePath); // Generates ArgumentList from global config file. // Will reinitialize result without clearing existing contents. On error, result will contain invalid pointer. -int getGlobalLaunchArguments(struct ArgumentList *result); +int getGlobalLaunchArguments(ArgumentList *result); // Generates ArgumentList from title-specific config file. // Will reinitialize result without clearing existing contents. On error, result will contain invalid pointer. -int getTitleLaunchArguments(struct ArgumentList *result, struct Target *target); +int getTitleLaunchArguments(ArgumentList *result, Target *target); // Saves title launch arguments to title-specific config file. // '$' before the argument name is used as 'disabled' flag. // Empty value means that the argument is empty, but still should be used without the value. -int updateTitleLaunchArguments(struct Target *target, struct ArgumentList *options); +int updateTitleLaunchArguments(Target *target, ArgumentList *options); // Completely frees ArgumentList. Passed pointer will not be valid after this function executes -void freeArgumentList(struct ArgumentList *result); +void freeArgumentList(ArgumentList *result); // Creates new Argument with passed argName and value (without copying) -struct Argument *newArgument(char *argName, char *value); +Argument *newArgument(char *argName, char *value); // Appends arg to the end of target -void appendArgument(struct ArgumentList *target, struct Argument *arg); +void appendArgument(ArgumentList *target, Argument *arg); // Does a deep copy of arg and inserts it into target. // Always places COMPAT_MODES_ARG on the top of the list. -void insertArgumentCopy(struct ArgumentList *target, struct Argument *arg); +void insertArgumentCopy(ArgumentList *target, Argument *arg); // Merges two lists into one, ignoring arguments in the second list that already exist in the first list. // Expects result to be initialized with zeroes. All arguments in resulting list are a deep copy of arguments in source lists. -void mergeArgumentLists(struct ArgumentList *list1, struct ArgumentList *list2); +void mergeArgumentLists(ArgumentList *list1, ArgumentList *list2); // Parses compatibility mode argument value into a bitmask uint8_t parseCompatModes(char *stringValue); // Stores compatibility mode from bitmask into argument value and sets isDisabled flag accordingly. // Target must be at least 6 bytes long, including null terminator -void storeCompatModes(struct Argument *target, uint8_t modes); +void storeCompatModes(Argument *target, uint8_t modes); // Inserts a new compat mode arg into the argument list -void insertCompatModeArg(struct ArgumentList *target, uint8_t modes); +void insertCompatModeArg(ArgumentList *target, uint8_t modes); // Loads both global and title launch arguments, returning pointer to a merged list -struct ArgumentList *loadLaunchArgumentLists(struct Target *target); +ArgumentList *loadLaunchArgumentLists(Target *target); // Parses options file into ArgumentList -int loadArgumentList(struct ArgumentList *options, char *filePath); +int loadArgumentList(ArgumentList *options, char *filePath); #endif \ No newline at end of file diff --git a/src/gui.c b/src/gui.c index 5bff748..996bcd3 100644 --- a/src/gui.c +++ b/src/gui.c @@ -20,11 +20,11 @@ #define COVER_ART_RES_W 140 #define COVER_ART_RES_H 200 -int uiLoop(struct TargetList *titles); -int uiTitleOptionsLoop(struct Target *title); -void drawTitleList(struct TargetList *titles, int selectedTitleIdx, GSTEXTURE *selectedTitleCover); -void drawArgumentList(struct ArgumentList *arguments, uint8_t compatModes, int selectedArgIdx); -void uiLaunchTitle(struct Target *target, struct ArgumentList *arguments); +int uiLoop(TargetList *titles); +int uiTitleOptionsLoop(Target *title); +void drawTitleList(TargetList *titles, int selectedTitleIdx, GSTEXTURE *selectedTitleCover); +void drawArgumentList(ArgumentList *arguments, uint8_t compatModes, int selectedArgIdx); +void uiLaunchTitle(Target *target, ArgumentList *arguments); void drawGameID(const char *game_id); static GSGLOBAL *gsGlobal; @@ -124,7 +124,7 @@ void uiCleanup() { } // Main UI loop. Displays the target list. -int uiLoop(struct TargetList *titles) { +int uiLoop(TargetList *titles) { int res = 0; if ((gsGlobal == NULL) && (res = uiInit(0))) { printf("ERROR: Failed to init UI: %d\n", res); @@ -134,7 +134,7 @@ int uiLoop(struct TargetList *titles) { int isCoverUninitialized = 1; int selectedTitleIdx = 0; int input = 0; - struct Target *curTarget = titles->first; + Target *curTarget = titles->first; // Get last launched title and find it in the target list char *lastTitle = calloc(sizeof(char), PATH_MAX + 1); @@ -179,7 +179,7 @@ int uiLoop(struct TargetList *titles) { input = getInput(-1); if (input & (PAD_CROSS | PAD_CIRCLE)) { // Copy target, free title list and launch - struct Target *target = copyTarget(curTarget); + Target *target = copyTarget(curTarget); freeTargetList(titles); uiLaunchTitle(target, NULL); goto exit; @@ -219,12 +219,12 @@ int uiLoop(struct TargetList *titles) { } // Title options screen handler -int uiTitleOptionsLoop(struct Target *target) { +int uiTitleOptionsLoop(Target *target) { int res = 0; uint8_t modes = 0; // Load arguments from config files - struct ArgumentList *titleArguments = loadLaunchArgumentLists(target); + ArgumentList *titleArguments = loadLaunchArgumentLists(target); // Parse compatibility modes if ((titleArguments->total != 0) && !strcmp(COMPAT_MODES_ARG, titleArguments->first->arg)) { @@ -242,7 +242,7 @@ int uiTitleOptionsLoop(struct Target *target) { // Always start with the second element since the first // is guaranteed to be a compatibility mode flag - struct Argument *curArgument = titleArguments->first->next; + Argument *curArgument = titleArguments->first->next; while (1) { gsKit_clear(gsGlobal, BlackBG); @@ -310,7 +310,7 @@ int uiTitleOptionsLoop(struct Target *target) { } // Draws title list -void drawTitleList(struct TargetList *titles, int selectedTitleIdx, GSTEXTURE *selectedTitleCover) { +void drawTitleList(TargetList *titles, int selectedTitleIdx, GSTEXTURE *selectedTitleCover) { int curPage = selectedTitleIdx / maxTitlesPerPage; // Draw header and footer @@ -327,7 +327,7 @@ void drawTitleList(struct TargetList *titles, int selectedTitleIdx, GSTEXTURE *s "Press \f0090 to launch the title, \f0097 to open launch options\nPress START to exit"); // Draw title list - struct Target *curTitle = titles->first; + Target *curTitle = titles->first; while (curTitle != NULL) { // Do not display titles before the current page if (curTitle->idx < maxTitlesPerPage * curPage) { @@ -370,7 +370,7 @@ void drawTitleList(struct TargetList *titles, int selectedTitleIdx, GSTEXTURE *s } // Draws title arguments -void drawArgumentList(struct ArgumentList *arguments, uint8_t compatModes, int selectedArgIdx) { +void drawArgumentList(ArgumentList *arguments, uint8_t compatModes, int selectedArgIdx) { int startY = 75; int idx = 0; @@ -391,7 +391,7 @@ void drawArgumentList(struct ArgumentList *arguments, uint8_t compatModes, int s // Always start with the second element since the first // is guaranteed to be a compatibility mode flag - struct Argument *argument = arguments->first->next; + Argument *argument = arguments->first->next; int curPage = (selectedArgIdx - CM_NUM_MODES) / MAX_ARGUMENTS; // Advance start Y offset and add some space after compatibility modes startY += (CM_NUM_MODES * 15) + 10; @@ -430,7 +430,7 @@ void drawArgumentList(struct ArgumentList *arguments, uint8_t compatModes, int s } // Displays Game ID and launches the title -void uiLaunchTitle(struct Target *target, struct ArgumentList *arguments) { +void uiLaunchTitle(Target *target, ArgumentList *arguments) { // Initialize arugments if not set if (arguments == NULL) { arguments = loadLaunchArgumentLists(target); diff --git a/src/history.c b/src/history.c index 90e15cc..1b115a4 100644 --- a/src/history.c +++ b/src/history.c @@ -71,7 +71,7 @@ int createSystemDataDir() { int updateHistoryFile(const char *titleID) { // Refuse to write entry if title ID is less than expected if ((titleID == NULL) || (strlen(titleID) < 11)) { - printf("WARN: will not invalid title ID to history files\n"); + printf("WARN: will not write invalid title ID to history files\n"); return 0; } // Detect system directory diff --git a/src/iso.c b/src/iso.c index dd9ddf3..6411991 100644 --- a/src/iso.c +++ b/src/iso.c @@ -1,5 +1,6 @@ #include "iso.h" #include "common.h" +#include "iso_cache.h" #include <errno.h> #include <fcntl.h> #include <kernel.h> @@ -14,11 +15,19 @@ #include <fileXio_rpc.h> #include <io_common.h> -int _findISO(DIR *directory, struct TargetList *result); -void insertIntoList(struct TargetList *result, struct Target *title); +int _findISO(DIR *directory, TargetList *result); +void insertIntoList(TargetList *result, Target *title); char *getTitleID(char *path); +void processTitleID(TargetList *result); -struct TargetList *findISO() { +// Directories to skip when browsing for ISOs +const char *ignoredDirs[] = { + "nhddl", "APPS", "ART", "CFG", "CHT", "LNG", "THM", "VMC", "XEBPLUS", +}; + +// Looks for ISO images in searchDirs. +// Returns NULL if no targets were found or an error occurs +TargetList *findISO() { DIR *directory; // Try to open directory, giving a chance to IOP modules to init for (int i = 0; i < 1000; i++) { @@ -33,7 +42,7 @@ struct TargetList *findISO() { return NULL; } - struct TargetList *result = malloc(sizeof(struct TargetList)); + TargetList *result = malloc(sizeof(TargetList)); result->total = 0; result->first = NULL; result->last = NULL; @@ -44,13 +53,16 @@ struct TargetList *findISO() { return NULL; } closedir(directory); + + processTitleID(result); return result; } // Searches rootpath and adds discovered ISOs to TargetList -int _findISO(DIR *directory, struct TargetList *result) { +int _findISO(DIR *directory, TargetList *result) { if (directory == NULL) return -ENOENT; + // Read directory entries struct dirent *entry; char *fileext; @@ -59,11 +71,22 @@ int _findISO(DIR *directory, struct TargetList *result) { logString("ERROR: Failed to get cwd\n"); return -ENOENT; } + int cwdLen = strlen(titlePath); // Get the length of base path string while ((entry = readdir(directory)) != NULL) { // Check if the entry is a directory using d_type switch (entry->d_type) { case DT_DIR: + // Ignore hidden, special and invalid directories (non-ASCII paths seem to return '?' and cause crashes when used with opendir) + if ((entry->d_name[0] == '.') || (entry->d_name[0] == '$') || (entry->d_name[0] == '?')) + continue; + + for (int i = 0; i < sizeof(ignoredDirs) / sizeof(char *); i++) { + if (!strcmp(ignoredDirs[i], entry->d_name)) { + goto skipDirectory; + } + } + // Open dir and change cwd DIR *d = opendir(entry->d_name); chdir(entry->d_name); @@ -72,6 +95,7 @@ int _findISO(DIR *directory, struct TargetList *result) { // Return back to root directory chdir(".."); closedir(d); + skipDirectory: continue; default: if (entry->d_name[0] == '.') // Ignore .files (most likely macOS doubles) @@ -85,7 +109,7 @@ int _findISO(DIR *directory, struct TargetList *result) { strcat(titlePath, entry->d_name); // Initialize target - struct Target *title = calloc(sizeof(struct Target), 1); + Target *title = calloc(sizeof(Target), 1); title->prev = NULL; title->next = NULL; title->fullPath = strdup(titlePath); @@ -95,9 +119,6 @@ int _findISO(DIR *directory, struct TargetList *result) { title->name = calloc(sizeof(char), nameLength + 1); strncpy(title->name, entry->d_name, nameLength); - // Get title ID - title->id = getTitleID(title->fullPath); - // Increment title counter and update target list result->total++; if (result->first == NULL) { @@ -114,7 +135,7 @@ int _findISO(DIR *directory, struct TargetList *result) { // Set indexes for each title int idx = 0; - struct Target *curTitle = result->first; + Target *curTitle = result->first; while (curTitle != NULL) { curTitle->idx = idx; idx++; @@ -133,9 +154,9 @@ void toUppercase(char *str) { } // Inserts title in the list while keeping the alphabetical order -void inline insertIntoList(struct TargetList *result, struct Target *title) { +void inline insertIntoList(TargetList *result, Target *title) { // Traverse the list in reverse - struct Target *curTitle = result->last; + Target *curTitle = result->last; // Covert title name to uppercase char *curUppercase = strdup(title->name); @@ -183,6 +204,55 @@ void inline insertIntoList(struct TargetList *result, struct Target *title) { free(curUppercase); } +// Fills in title ID for every entry in the list +void processTitleID(TargetList *result) { + if (result->total == 0) + return; + + // Load title cache + TitleIDCache *cache = malloc(sizeof(TitleIDCache)); + int isCacheUpdateNeeded = 0; + if (loadTitleIDCache(cache)) { + logString("Failed to load title ID cache, all ISOs will be rescanned\n"); + free(cache); + cache = NULL; + } else if (cache->total != result->total) { + // Set flag if number of entries is different + isCacheUpdateNeeded = 1; + } + + // For every entry in target list, try to get title ID from cache + // If cache doesn't have title ID for the path, + // get it from ISO + int cacheMisses = 0; + char *titleID = NULL; + Target *curTarget = result->first; + while (curTarget != NULL) { + // Try to get title ID from cache + if (cache != NULL) { + titleID = getCachedTitleID(curTarget->fullPath, cache); + } + + if (titleID != NULL) { + curTarget->id = strdup(titleID); + } else { // Get title ID from ISO + cacheMisses++; + printf("Cache miss for %s\n", curTarget->fullPath); + curTarget->id = getTitleID(curTarget->fullPath); + } + + curTarget = curTarget->next; + } + freeTitleCache(cache); + + if ((cacheMisses > 0) || (isCacheUpdateNeeded)) { + logString("Updating title ID cache\n"); + if (storeTitleIDCache(result)) { + logString("Failed to save title ID cache\n"); + } + } +} + // Loads SYSTEM.CNF from ISO and extracts title ID // This function was copied from Neutrino with minimal changes char *getTitleID(char *path) { @@ -224,8 +294,8 @@ char *getTitleID(char *path) { } // Completely frees Target and returns pointer to a previous argument in the list -struct Target *freeTarget(struct Target *target) { - struct Target *prev = NULL; +Target *freeTarget(Target *target) { + Target *prev = NULL; free(target->fullPath); free(target->name); free(target->id); @@ -237,8 +307,8 @@ struct Target *freeTarget(struct Target *target) { } // Completely frees TargetList. Passed pointer will not be valid after this function executes -void freeTargetList(struct TargetList *result) { - struct Target *target = result->last; +void freeTargetList(TargetList *result) { + Target *target = result->last; while (target != NULL) { target = freeTarget(target); } @@ -249,8 +319,8 @@ void freeTargetList(struct TargetList *result) { } // Finds target with given index in the list and returns a pointer to it -struct Target *getTargetByIdx(struct TargetList *targets, int idx) { - struct Target *current = targets->first; +Target *getTargetByIdx(TargetList *targets, int idx) { + Target *current = targets->first; while (1) { if (current->idx == idx) { return current; @@ -265,8 +335,8 @@ struct Target *getTargetByIdx(struct TargetList *targets, int idx) { } // Makes and returns a deep copy of src without prev/next pointers. -struct Target *copyTarget(struct Target *src) { - struct Target *copy = calloc(sizeof(struct Target), 1); +Target *copyTarget(Target *src) { + Target *copy = calloc(sizeof(Target), 1); copy->idx = src->idx; copy->fullPath = strdup(src->fullPath); diff --git a/src/iso_cache.c b/src/iso_cache.c new file mode 100644 index 0000000..cdeedc6 --- /dev/null +++ b/src/iso_cache.c @@ -0,0 +1,212 @@ +// Implements title ID cache to make bulding target list faster +#include "iso_cache.h" +#include "common.h" +#include "iso.h" +#include "options.h" +#include <malloc.h> +#include <ps2sdkapi.h> +#include <stdio.h> +#include <string.h> + +#define CACHE_MAGIC "NIDC" +#define CACHE_VERSION 1 + +const char titleIDCacheFile[] = "/cache.bin"; +#define MAX_CACHE_PATH_LEN STORAGE_BASE_PATH_LEN + BASE_CONFIG_PATH_LEN + (sizeof(titleIDCacheFile) / sizeof(char)) + +// Structs used to read and write cache file contents +typedef struct { + char titleID[12]; + size_t pathLength; // Includes null-terminator +} CacheEntryHeader; + +typedef struct { + char magic[4]; // Must be always equal to CACHE_MAGIC + uint8_t version; // Cache version + int total; // Total number of elements in cache file +} CacheMetadata; + +// Saves TargetList into title ID cache +int storeTitleIDCache(TargetList *list) { + if (list->total == 0) { + return 0; + } + + char cachePath[MAX_CACHE_PATH_LEN]; + + // Get path to config directory and make sure it exists + buildConfigFilePath(cachePath, NULL); + struct stat st; + if (stat(cachePath, &st) == -1) { + printf("Creating config directory: %s\n", cachePath); + mkdir(cachePath, 0777); + } + + // Open cache file for writing + buildConfigFilePath(cachePath, titleIDCacheFile); + FILE *file = fopen(cachePath, "wb"); + if (file == NULL) { + printf("ERROR: failed to open cache file for writing: %d\n", errno); + return -errno; + } + Target *curTitle = list->first; + + int result; + // Write cache file header + CacheMetadata meta = {.magic = CACHE_MAGIC, .version = CACHE_VERSION, .total = list->total}; + result = fwrite(&meta, sizeof(CacheMetadata), 1, file); + if (!result) { + printf("failed to write metadata: %d\n", errno); + fclose(file); + remove(cachePath); + return result; + } + + // Write each entry + CacheEntryHeader header; + while (curTitle != NULL) { + // Write entry header + memcpy(header.titleID, curTitle->id, sizeof(header.titleID)); + header.titleID[11] = '\0'; + header.pathLength = strlen(curTitle->fullPath) + 1; + result = fwrite(&header, sizeof(CacheEntryHeader), 1, file); + if (!result) { + printf("%s: failed to write header: %d\n", curTitle->name, errno); + fclose(file); + remove(cachePath); + return result; + } + // Write full ISO path + result = fwrite(curTitle->fullPath, header.pathLength, 1, file); + if (!result) { + printf("%s: failed to write full path: %d\n", curTitle->name, errno); + fclose(file); + remove(cachePath); + return result; + } + curTitle = curTitle->next; + } + fclose(file); + return 0; +} + +// Loads title ID cache from storage into cache +int loadTitleIDCache(TitleIDCache *cache) { + cache->total = 0; + cache->lastMatchedIdx = 0; + + // Open cache file for reading + char cachePath[MAX_CACHE_PATH_LEN]; + buildConfigFilePath(cachePath, titleIDCacheFile); + + // For some reason, fopen with non-existing file causes + // all consecutive calls to fopen for the same path to fail + // with errno 12 with trap exception in PCSX2. + // To work around that, check if cache file exists first + if (access(cachePath, F_OK)) { + printf("Cache file doesn't exist\n"); + return -ENOENT; + } + + FILE *file = fopen(cachePath, "rb"); + if (file == NULL) { + printf("ERROR: failed to open cache file: %d\n", -errno); + return -errno; + } + int result; + + // Read cache file header + CacheMetadata meta; + result = fread(&meta, sizeof(CacheMetadata), 1, file); + if (!result) { + printf("ERROR: Failed to read cache metadata\n"); + fclose(file); + return result; + } + + // Make sure header is valid + if (!strcmp(meta.magic, CACHE_MAGIC)) { + printf("ERROR: Cache magic doesn't match, refusing to load\n"); + fclose(file); + return -EINVAL; + } + if (meta.version != CACHE_VERSION) { + printf("ERROR: Unsupported cache version %d\n", meta.version); + fclose(file); + return -EINVAL; + } + + // Allocate memory for cache entries based on total entry count from header metadata + int readIndex = 0; + cache->entries = malloc((sizeof(CacheEntry) * meta.total)); + if (cache->entries == NULL) { + printf("ERROR: Can't allocate enough memory\n"); + fclose(file); + return -ENOMEM; + } + + // Read each entry into cache + CacheEntryHeader header; + char pathBuf[PATH_MAX + 1]; + while (!feof(file)) { + // Read cache entry header + pathBuf[0] = '\0'; + result = fread(&header, sizeof(CacheEntryHeader), 1, file); + if (result != 1) { + if (!feof(file)) + printf("WARN: Read less than expected, title ID cache might be incomplete\n"); + break; + } + // Read ISO path + result = fread(&pathBuf, header.pathLength, 1, file); + if (result != 1) { + printf("WARN: Read less than expected, title ID cache might be incomplete\n"); + break; + } + pathBuf[header.pathLength] = '\0'; + + // Store entry in cache + CacheEntry entry; + memcpy(entry.titleID, header.titleID, sizeof(entry.titleID)); + header.titleID[11] = '\0'; + entry.fullPath = strdup(pathBuf); + cache->entries[readIndex] = entry; + readIndex++; + } + fclose(file); + + // Free unused memory + if (readIndex != meta.total) + cache->entries = realloc(cache->entries, sizeof(CacheEntry) * readIndex); + + cache->total = readIndex; + return 0; +} + +// Returns a pointer to title ID or NULL if fullPath is not found in the cache +char *getCachedTitleID(char *fullPath, TitleIDCache *cache) { + // This code takes advantage of all entries in the title list being sorted alphabetically. + // By starting from the index of the last matched entry, we can skip comparing fullPath with entries + // that have already been matched to a title ID, improving lookup speeds for very large lists. + for (int i = cache->lastMatchedIdx; i < cache->total; i++) { + if (!strcmp(cache->entries[i].fullPath, fullPath)) { + cache->lastMatchedIdx = i; + return cache->entries[i].titleID; + } + } + return NULL; +} + +// Frees memory used by title ID cache +// All pointers to cache entries (including title IDs) will be invalid +void freeTitleCache(TitleIDCache *cache) { + if (cache == NULL) + return; + + for (int i = 0; i < cache->total; i++) { + free(cache->entries[i].fullPath); + } + + free(cache->entries); + free(cache); +} diff --git a/src/launcher.c b/src/launcher.c index 750e1da..08a7fbd 100644 --- a/src/launcher.c +++ b/src/launcher.c @@ -8,7 +8,6 @@ #include <stdlib.h> #include <string.h> -static const char neutrinoELF[] = "neutrino.elf"; static char isoArgument[] = "dvd"; static char bsdArgument[] = "bsd"; @@ -20,8 +19,8 @@ static char bsdArgument[] = "bsd"; // Assembles argument lists into argv for Neutrino. // Expects argv to be initialized with at least (arguments->total) elements. -int assembleArgv(struct ArgumentList *arguments, char **argv) { - struct Argument *curArg = arguments->first; +int assembleArgv(ArgumentList *arguments, char **argv) { + Argument *curArg = arguments->first; int argCount = 0; int argSize = 0; while (curArg != NULL) { @@ -55,7 +54,7 @@ int assembleArgv(struct ArgumentList *arguments, char **argv) { // Launches target, passing arguments to Neutrino. // Expects arguments to be initialized -void launchTitle(struct Target *target, struct ArgumentList *arguments) { +void launchTitle(Target *target, ArgumentList *arguments) { // Append arguments char *bsdValue; switch (LAUNCHER_OPTIONS.mode) { @@ -92,9 +91,5 @@ void launchTitle(struct Target *target, struct ArgumentList *arguments) { } updateHistoryFile(target->id); - char neutrinoPath[PATH_MAX + 1]; - neutrinoPath[0] = '\0'; - strcpy(neutrinoPath, ELF_BASE_PATH); - strcat(neutrinoPath, neutrinoELF); - printf("ERROR: failed to load %s: %d\n", neutrinoELF, LoadELFFromFile(neutrinoPath, argCount, argv)); + printf("ERROR: failed to load %s: %d\n", NEUTRINO_ELF_PATH, LoadELFFromFile(NEUTRINO_ELF_PATH, argCount, argv)); } diff --git a/src/main.c b/src/main.c index 87e1dd0..5b16d9d 100644 --- a/src/main.c +++ b/src/main.c @@ -12,20 +12,31 @@ // Path to ISO storage const char STORAGE_BASE_PATH[] = "mass:"; +const size_t STORAGE_BASE_PATH_LEN = sizeof(STORAGE_BASE_PATH) / sizeof(char); // Path to ELF directory char ELF_BASE_PATH[PATH_MAX + 1]; +// Path to Neutrino ELF +char NEUTRINO_ELF_PATH[PATH_MAX + 1]; // Launcher options LauncherOptions LAUNCHER_OPTIONS; // Options file name relative to ELF_BASE_PATH static const char optionsFile[] = "nhddl.yaml"; // The 'X' in "mcX" will be replaced with memory card number in parseIPConfig static char ipconfigPath[] = "mcX:/SYS-CONF/IPCONFIG.DAT"; +// Neutrino ELF name +static const char neutrinoELF[] = "neutrino.elf"; // Supported options #define OPTION_480P "480p" #define OPTION_MODE "mode" #define OPTION_UDPBD_IP "udpbd_ip" +#ifndef GIT_VERSION +#define GIT_VERSION "v-0.0.0-unknown" +#endif +// Used as ELF_BASE_PATH if DEBUG is defined +#define DEBUG_PATH "mc1:/APPS/neutrino" + void initOptions(char *basePath); int main(int argc, char *argv[]) { @@ -33,14 +44,26 @@ int main(int argc, char *argv[]) { init_scr(); printf("*************\n"); - logString("\n\nNHDDL - a Neutrino launcher by pcm720\n\n"); + logString("\n\nNHDDL %s\nA Neutrino launcher by pcm720\n\n", GIT_VERSION); printf("*************\n"); - // Get base path +// If DEBUG is not defined +#ifndef DEBUG + // Get base path from current working directory if (!getcwd(ELF_BASE_PATH, PATH_MAX + 1)) { logString("ERROR: Failed to get cwd\n"); goto fail; } +#else + // Get base path from hardcoded DEBUG_PATH + strcpy(ELF_BASE_PATH, DEBUG_PATH); +#endif + + if (strncmp("mc", ELF_BASE_PATH, 2) && strncmp("host", ELF_BASE_PATH, 4)) { + logString("ERROR: NHDDL can only be run from the memory card"); + goto fail; + } + // Append '/' to current working directory strcat(ELF_BASE_PATH, "/"); logString("Current working directory is %s\n", ELF_BASE_PATH); @@ -53,17 +76,26 @@ int main(int argc, char *argv[]) { goto fail; } + strcpy(NEUTRINO_ELF_PATH, ELF_BASE_PATH); + strcat(NEUTRINO_ELF_PATH, neutrinoELF); + int neutrinoFd = open(NEUTRINO_ELF_PATH, O_RDONLY); + if (neutrinoFd < 0) { + logString("ERROR: %s doesn't exist\n", NEUTRINO_ELF_PATH); + goto fail; + } + close(neutrinoFd); + initOptions(ELF_BASE_PATH); // Init BDM modules logString("Loading BDM modules...\n"); if ((res = initBDM(ELF_BASE_PATH)) != 0) { - logString("Failed to initialize modules: %d\n", res); + logString("ERROR: Failed to initialize modules: %d\n", res); goto fail; } - logString("\n\nSearching for ISO on %s\n", STORAGE_BASE_PATH); - struct TargetList *titles = findISO(); + logString("\n\nBuilding target list...\n"); + TargetList *titles = findISO(); if (titles == NULL) { logString("No targets found\n"); goto fail; @@ -143,7 +175,7 @@ void initOptions(char *basePath) { snprintf(lineBuffer, sizeof(lineBuffer), "%s/%s", basePath, optionsFile); // Load NHDDL options file into ArgumentList - struct ArgumentList *options = calloc(1, sizeof(struct ArgumentList)); + ArgumentList *options = calloc(1, sizeof(ArgumentList)); if (loadArgumentList(options, lineBuffer)) { logString("Can't load options file, will use defaults\n"); freeArgumentList(options); @@ -151,7 +183,7 @@ void initOptions(char *basePath) { } // Parse the list into Options - struct Argument *arg = options->first; + Argument *arg = options->first; while (arg != NULL) { if (!arg->isDisabled) { if (strcmp(OPTION_480P, arg->arg) == 0) { diff --git a/src/module_init.c b/src/module_init.c index b95a835..3184017 100644 --- a/src/module_init.c +++ b/src/module_init.c @@ -10,6 +10,19 @@ #include "common.h" #include "module_init.h" +// Macros for loading embedded IOP modules +#define IRX_DEFINE(mod) \ + extern unsigned char mod##_irx[] __attribute__((aligned(16))); \ + extern unsigned int size_##mod##_irx + +#define IRX_LOAD(mod) \ + logString("\tloading " #mod "\n"); \ + if (SifExecModuleBuffer(mod##_irx, size_##mod##_irx, 0, NULL, &iopret) < 0) \ + return ret; \ + if (iopret == 1) { \ + return iopret; \ + } + // Embedded IOP modules required for reading from memory card IRX_DEFINE(iomanX); IRX_DEFINE(fileXio); @@ -20,17 +33,21 @@ IRX_DEFINE(freepad); // Initializes basic modules required for reading from memory card int init() { + int ret = 0; + +// If DEBUG is defined, skip IOP reinitialization +#ifndef DEBUG // Reset IOP SifIopReset("", 0); // Initialize the RPC manager SifInitRpc(0); - int ret; // Apply patches required to load modules from EE RAM if ((ret = sbv_patch_enable_lmb())) return ret; if ((ret = sbv_patch_disable_prefix_check())) return ret; +#endif // Load modules int iopret = 0; @@ -47,26 +64,31 @@ int init() { // Loads modules from basePath int initExtraModules(char *basePath, int numModules, const char *modules[]) { // Allocate memory for module paths + int basePathLen = strlen(basePath); char pathBuf[PATH_MAX + 1]; pathBuf[0] = '\0'; strcpy(pathBuf, basePath); - // Load the needed modules - int ret; + // Load modules + int ret, iopret; for (int i = 0; i < numModules; i++) { - strcat(pathBuf, modules[i]); // append module path to base path + strcat(pathBuf, modules[i]); // Append module path to base path logString("\tloading %s\n", pathBuf); - if ((ret = SifLoadModule(pathBuf, 0, NULL)) < 0) { + ret = SifLoadStartModule(pathBuf, 0, NULL, &iopret); + if (ret < 0) { return ret; } - pathBuf[strlen(basePath)] = '\0'; // end bufferred string at basePath for the next strcat in the loop + if (iopret == 1) { + return iopret; + } + pathBuf[basePathLen] = '\0'; // End bufferred string at basePath for the next strcat in the loop } return 0; } +#define MODULE_COUNT(a) sizeof(a) / sizeof(char *) // Base BDM modules -const int BDM_BASE_MODULE_COUNT = 3; const char *bdm_base_modules[] = { // BDM "modules/bdm.irx", @@ -77,21 +99,19 @@ const char *bdm_base_modules[] = { }; // ATA modules -const int ATA_MODULE_COUNT = 2; const char *ata_modules[] = { // DEV9 "modules/dev9_ns.irx", // ATA - "modules/ata_bd.irx"}; + "modules/ata_bd.irx", +}; // MX4SIO modules -const int MX4SIO_MODULE_COUNT = 1; const char *mx4sio_modules[] = { "modules/mx4sio_bd_mini.irx", }; // UDPBD modules -const int UDPBD_MODULE_COUNT = 2; const char *udpbd_modules[] = { // DEV9 "modules/dev9_ns.irx", @@ -101,7 +121,6 @@ const char *udpbd_modules[] = { }; // USB modules -const int USB_MODULE_COUNT = 2; const char *usb_modules[] = { // USBD "modules/usbd_mini.irx", @@ -111,18 +130,18 @@ const char *usb_modules[] = { int initATA(char *basePath) { int res = 0; - if ((res = initExtraModules(basePath, BDM_BASE_MODULE_COUNT, bdm_base_modules))) { + if ((res = initExtraModules(basePath, MODULE_COUNT(bdm_base_modules), bdm_base_modules))) { return res; } - return initExtraModules(basePath, ATA_MODULE_COUNT, ata_modules); + return initExtraModules(basePath, MODULE_COUNT(ata_modules), ata_modules); } int initMX4SIO(char *basePath) { int res = 0; - if ((res = initExtraModules(basePath, BDM_BASE_MODULE_COUNT, bdm_base_modules))) { + if ((res = initExtraModules(basePath, MODULE_COUNT(bdm_base_modules), bdm_base_modules))) { return res; } - return initExtraModules(basePath, MX4SIO_MODULE_COUNT, mx4sio_modules); + return initExtraModules(basePath, MODULE_COUNT(mx4sio_modules), mx4sio_modules); } int initUDPBD(char *basePath, char *hostIPAddr) { @@ -131,37 +150,43 @@ int initUDPBD(char *basePath, char *hostIPAddr) { return -EINVAL; } - int res = 0; - if ((res = initExtraModules(basePath, BDM_BASE_MODULE_COUNT, bdm_base_modules))) { - return res; + int ret = 0; + if ((ret = initExtraModules(basePath, MODULE_COUNT(bdm_base_modules), bdm_base_modules))) { + return ret; } // Treating last module as a special case because it needs an argument to work - if ((res = initExtraModules(basePath, UDPBD_MODULE_COUNT - 1, udpbd_modules))) { - return res; + if ((ret = initExtraModules(basePath, MODULE_COUNT(udpbd_modules) - 1, udpbd_modules))) { + return ret; } // Allocate memory for module path and argument + int iopret; char pathBuf[PATH_MAX + 1]; pathBuf[0] = '\0'; strcpy(pathBuf, basePath); char ipArg[19]; // 15 bytes for IP string + 3 bytes for 'ip=' snprintf(ipArg, sizeof(ipArg), "ip=%s", hostIPAddr); - strcat(pathBuf, udpbd_modules[UDPBD_MODULE_COUNT - 1]); // append module path to base path + // Append module path to base path + strcat(pathBuf, udpbd_modules[MODULE_COUNT(udpbd_modules) - 1]); logString("\tloading %s\n\twith %s\n", pathBuf, ipArg); - if ((res = SifLoadModule(pathBuf, sizeof(ipArg), ipArg)) < 0) { - return res; + ret = SifLoadStartModule(pathBuf, sizeof(ipArg), ipArg, &iopret); + if (ret < 0) { + return ret; + } + if (iopret == 1) { + return iopret; } return 0; } int initUSB(char *basePath) { int res = 0; - if ((res = initExtraModules(basePath, BDM_BASE_MODULE_COUNT, bdm_base_modules))) { + if ((res = initExtraModules(basePath, MODULE_COUNT(bdm_base_modules), bdm_base_modules))) { return res; } - return initExtraModules(basePath, USB_MODULE_COUNT, usb_modules); + return initExtraModules(basePath, MODULE_COUNT(usb_modules), usb_modules); } // Initializes BDM modules depending on launcher mode diff --git a/src/options.c b/src/options.c index 10825c2..c923149 100644 --- a/src/options.c +++ b/src/options.c @@ -8,36 +8,35 @@ #include <stdlib.h> #include <string.h> -// Device + baseConfigPath + lastTitlePath -#define MAX_LAST_LAUNCHED_LENGTH 25 - -int parseOptionsFile(struct ArgumentList *result, FILE *file); -int loadArgumentList(struct ArgumentList *options, char *filePath); -void appendArgument(struct ArgumentList *target, struct Argument *arg); -struct Argument *newArgument(char *argName, char *value); +int parseOptionsFile(ArgumentList *result, FILE *file); +int loadArgumentList(ArgumentList *options, char *filePath); +void appendArgument(ArgumentList *target, Argument *arg); +Argument *newArgument(char *argName, char *value); // Defines all known compatibility modes -const CompatiblityModeMap COMPAT_MODE_MAP[CM_NUM_MODES] = {{CM_DISABLE_BUILTIN_MODES, '0', "Disable built-in compat flags"}, - {CM_IOP_ACCURATE_READS, '1', "IOP: Accurate reads"}, - {CM_IOP_SYNC_READS, '2', "IOP: Sync reads"}, - {CM_EE_UNHOOK_SYSCALLS, '3', "EE : Unhook syscalls"}, - {CM_IOP_EMULATE_DVD_DL, '5', "IOP: Emulate DVD-DL"}}; -const char baseConfigPath[] = "/config"; +const CompatiblityModeMap COMPAT_MODE_MAP[CM_NUM_MODES] = { + {CM_DISABLE_BUILTIN_MODES, '0', "Disable built-in compat flags"}, + {CM_IOP_ACCURATE_READS, '1', "IOP: Accurate reads"}, + {CM_IOP_SYNC_READS, '2', "IOP: Sync reads"}, + {CM_EE_UNHOOK_SYSCALLS, '3', "EE : Unhook syscalls"}, + {CM_IOP_EMULATE_DVD_DL, '5', "IOP: Emulate DVD-DL"}, +}; + +const char BASE_CONFIG_PATH[] = "/nhddl"; +const size_t BASE_CONFIG_PATH_LEN = sizeof(BASE_CONFIG_PATH) / sizeof(char); + const char globalOptionsPath[] = "/global.yaml"; +#define MAX_GLOBAL_OPTS_LEN (STORAGE_BASE_PATH_LEN + BASE_CONFIG_PATH_LEN + (sizeof(globalOptionsPath) / sizeof(char))) + const char lastTitlePath[] = "/lastTitle.txt"; +#define MAX_LAST_TITLE_LEN (STORAGE_BASE_PATH_LEN + BASE_CONFIG_PATH_LEN + (sizeof(lastTitlePath) / sizeof(char))) // Writes full path to targetFileName into targetPath. // If targetFileName is NULL, will return path to config directory -void buildConfigFilePath(char *targetPath, const char *basePath, const char *targetFileName) { - if (basePath[4] == ':') { - strncpy(targetPath, basePath, 5); - targetPath[5] = '\0'; - } else { // Handle numbered devices - strncpy(targetPath, basePath, 6); - targetPath[6] = '\0'; - } - - strcat(targetPath, baseConfigPath); // Append base config path +void buildConfigFilePath(char *targetPath, const char *targetFileName) { + targetPath[0] = '\0'; + strcat(targetPath, STORAGE_BASE_PATH); + strcat(targetPath, BASE_CONFIG_PATH); // Append base config path if (targetFileName != NULL) { // Append / to path if targetFileName doesn't have it already if (targetFileName[0] != '/') @@ -50,9 +49,9 @@ void buildConfigFilePath(char *targetPath, const char *basePath, const char *tar // Gets last launched title path into titlePath int getLastLaunchedTitle(char *titlePath) { printf("Reading last launched title\n"); - char targetPath[MAX_LAST_LAUNCHED_LENGTH]; + char targetPath[MAX_LAST_TITLE_LEN]; targetPath[0] = '\0'; - buildConfigFilePath(targetPath, STORAGE_BASE_PATH, lastTitlePath); + buildConfigFilePath(targetPath, lastTitlePath); // Open last launched title file and read it int fd = open(targetPath, O_RDONLY); @@ -80,9 +79,8 @@ int getLastLaunchedTitle(char *titlePath) { // Writes last launched title path into lastTitle file int updateLastLaunchedTitle(char *titlePath) { printf("Writing last launched title as %s\n", titlePath); - char targetPath[MAX_LAST_LAUNCHED_LENGTH]; - targetPath[0] = '\0'; - buildConfigFilePath(targetPath, titlePath, NULL); + char targetPath[MAX_LAST_TITLE_LEN]; + buildConfigFilePath(targetPath, NULL); // Make sure config directory exists struct stat st; @@ -111,13 +109,12 @@ int updateLastLaunchedTitle(char *titlePath) { } // Generates ArgumentList from global config file -int getGlobalLaunchArguments(struct ArgumentList *result) { - char targetPath[PATH_MAX + 1]; - targetPath[0] = '\0'; - buildConfigFilePath(targetPath, STORAGE_BASE_PATH, globalOptionsPath); +int getGlobalLaunchArguments(ArgumentList *result) { + char targetPath[MAX_GLOBAL_OPTS_LEN]; + buildConfigFilePath(targetPath, globalOptionsPath); int ret = loadArgumentList(result, targetPath); - struct Argument *curArg = result->first; + Argument *curArg = result->first; while (curArg != NULL) { curArg->isGlobal = 1; curArg = curArg->next; @@ -126,45 +123,39 @@ int getGlobalLaunchArguments(struct ArgumentList *result) { } // Generates ArgumentList from global and title-specific config file -int getTitleLaunchArguments(struct ArgumentList *result, struct Target *target) { +int getTitleLaunchArguments(ArgumentList *result, Target *target) { printf("Looking for title-specific config for %s (%s)\n", target->name, target->id); char targetPath[PATH_MAX + 1]; - targetPath[0] = '\0'; - buildConfigFilePath(targetPath, target->fullPath, NULL); + buildConfigFilePath(targetPath, NULL); // Determine actual title options file from config directory contents DIR *directory = opendir(targetPath); if (directory == NULL) { printf("ERROR: Can't open %s\n", targetPath); return -ENOENT; } + targetPath[0] = '\0'; // Find title config in config directory - char configPath[PATH_MAX + 1]; - configPath[0] = '\0'; struct dirent *entry; while ((entry = readdir(directory)) != NULL) { if (entry->d_type != DT_DIR) { + // Find file that starts with ISO name (without the extension) if (!strncmp(entry->d_name, target->name, strlen(target->name))) { - // If file starts with ISO name - // Prefer ISO name config to title ID config - buildConfigFilePath(configPath, targetPath, entry->d_name); + buildConfigFilePath(targetPath, entry->d_name); break; - } else if (!strncmp(entry->d_name, target->id, strlen(target->id))) { - // If file starts with title ID - buildConfigFilePath(configPath, targetPath, entry->d_name); } } } closedir(directory); - if (configPath[0] == '\0') { + if (targetPath[0] == '\0') { printf("Title-specific config not found\n"); return 0; } // Load arguments - printf("Loading title-specific config from %s\n", configPath); - int ret = loadArgumentList(result, configPath); + printf("Loading title-specific config from %s\n", targetPath); + int ret = loadArgumentList(result, targetPath); if (ret) { printf("ERROR: Failed to load argument list: %d\n", ret); } @@ -175,11 +166,10 @@ int getTitleLaunchArguments(struct ArgumentList *result, struct Target *target) // Saves title launch arguments to title-specific config file. // '$' before the argument name is used as 'disabled' flag. // Empty value means that the argument is empty, but still should be used without the value. -int updateTitleLaunchArguments(struct Target *target, struct ArgumentList *options) { +int updateTitleLaunchArguments(Target *target, ArgumentList *options) { // Build file path char lineBuffer[PATH_MAX + 1]; - lineBuffer[0] = '\0'; - buildConfigFilePath(lineBuffer, target->fullPath, target->name); + buildConfigFilePath(lineBuffer, target->name); strcat(lineBuffer, ".yaml"); printf("Saving title-specific config to %s\n", lineBuffer); @@ -194,7 +184,7 @@ int updateTitleLaunchArguments(struct Target *target, struct ArgumentList *optio lineBuffer[0] = '\0'; // reuse buffer int len = 0; int ret = 0; - struct Argument *tArg = options->first; + Argument *tArg = options->first; while (tArg != NULL) { len = 0; // Skip enabled global arguments @@ -218,7 +208,7 @@ int updateTitleLaunchArguments(struct Target *target, struct ArgumentList *optio } // Parses options file into ArgumentList -int loadArgumentList(struct ArgumentList *options, char *filePath) { +int loadArgumentList(ArgumentList *options, char *filePath) { // Open global settings file and read it FILE *file = fopen(filePath, "r"); if (file == NULL) { @@ -242,7 +232,7 @@ int loadArgumentList(struct ArgumentList *options, char *filePath) { } // Parses file into ArgumentList. Result may contain parsed arguments even if an error is returned. -int parseOptionsFile(struct ArgumentList *result, FILE *file) { +int parseOptionsFile(ArgumentList *result, FILE *file) { // Our lines will mostly consist of file paths, which aren't likely to exceed 300 characters due to 255 character limit in exFAT path component char lineBuffer[PATH_MAX + 1]; lineBuffer[0] = '\0'; @@ -321,7 +311,7 @@ int parseOptionsFile(struct ArgumentList *result, FILE *file) { substrIdx--; } - struct Argument *arg = newArgument(argName, NULL); + Argument *arg = newArgument(argName, NULL); arg->isDisabled = isDisabled; // Allocate memory for the argument value @@ -348,8 +338,8 @@ int parseOptionsFile(struct ArgumentList *result, FILE *file) { } // Completely frees Argument and returns pointer to a previous argument in the list -struct Argument *freeArgument(struct Argument *arg) { - struct Argument *prev = NULL; +Argument *freeArgument(Argument *arg) { + Argument *prev = NULL; free(arg->arg); free(arg->value); if (arg->prev != NULL) { @@ -360,8 +350,8 @@ struct Argument *freeArgument(struct Argument *arg) { } // Completely frees ArgumentList. Passed pointer will not be valid after this function executes -void freeArgumentList(struct ArgumentList *result) { - struct Argument *tArg = result->last; +void freeArgumentList(ArgumentList *result) { + Argument *tArg = result->last; while (tArg != NULL) { tArg = freeArgument(tArg); } @@ -372,9 +362,9 @@ void freeArgumentList(struct ArgumentList *result) { } // Makes and returns a deep copy of src without prev/next pointers. -struct Argument *copyArgument(struct Argument *src) { +Argument *copyArgument(Argument *src) { // Do a deep copy for argument and value - struct Argument *copy = calloc(sizeof(struct Argument), 1); + Argument *copy = calloc(sizeof(Argument), 1); copy->isGlobal = src->isGlobal; copy->isDisabled = src->isDisabled; copy->arg = strdup(src->arg); @@ -384,7 +374,7 @@ struct Argument *copyArgument(struct Argument *src) { // Replaces argument and value in dst, freeing arg and value. // Keeps next and prev pointers. -void replaceArgument(struct Argument *dst, struct Argument *src) { +void replaceArgument(Argument *dst, Argument *src) { // Do a deep copy for argument and value free(dst->arg); free(dst->value); @@ -395,8 +385,8 @@ void replaceArgument(struct Argument *dst, struct Argument *src) { } // Creates new Argument with passed argName and value (without copying) -struct Argument *newArgument(char *argName, char *value) { - struct Argument *arg = malloc(sizeof(struct Argument)); +Argument *newArgument(char *argName, char *value) { + Argument *arg = malloc(sizeof(Argument)); arg->arg = argName; arg->value = value; arg->isDisabled = 0; @@ -407,7 +397,7 @@ struct Argument *newArgument(char *argName, char *value) { } // Appends arg to the end of target -void appendArgument(struct ArgumentList *target, struct Argument *arg) { +void appendArgument(ArgumentList *target, Argument *arg) { target->total++; // Always put compatibility mode argument first @@ -430,18 +420,18 @@ void appendArgument(struct ArgumentList *target, struct Argument *arg) { // Does a deep copy of arg and inserts it into target. // Always places COMPAT_MODES_ARG on the top of the list -void appendArgumentCopy(struct ArgumentList *target, struct Argument *arg) { +void appendArgumentCopy(ArgumentList *target, Argument *arg) { // Do a deep copy for argument and value - struct Argument *copy = copyArgument(arg); + Argument *copy = copyArgument(arg); appendArgument(target, copy); } // Merges two lists into one, ignoring arguments in the second list that already exist in the first list. // All arguments merged from the second list are a deep copy of arguments in source lists. // Expects both lists to be initialized. -void mergeArgumentLists(struct ArgumentList *list1, struct ArgumentList *list2) { - struct Argument *curArg1; - struct Argument *curArg2 = list2->first; +void mergeArgumentLists(ArgumentList *list1, ArgumentList *list2) { + Argument *curArg1; + Argument *curArg2 = list2->first; int isDuplicate = 0; // Copy arguments from the second list into result @@ -487,7 +477,7 @@ uint8_t parseCompatModes(char *stringValue) { // Stores compatibility mode from bitmask into argument value and sets isDisabled flag accordingly. // Target must be at least 6 bytes long, including null terminator -void storeCompatModes(struct Argument *target, uint8_t modes) { +void storeCompatModes(Argument *target, uint8_t modes) { int pos = 0; for (int i = 0; i < CM_NUM_MODES; i++) { @@ -506,8 +496,8 @@ void storeCompatModes(struct Argument *target, uint8_t modes) { } // Inserts a new compat mode arg into the argument list -void insertCompatModeArg(struct ArgumentList *target, uint8_t modes) { - struct Argument *newArg = calloc(sizeof(struct Argument), 1); +void insertCompatModeArg(ArgumentList *target, uint8_t modes) { + Argument *newArg = calloc(sizeof(Argument), 1); newArg->arg = strdup(COMPAT_MODES_ARG); newArg->value = calloc(sizeof(char), CM_NUM_MODES + 1); @@ -523,15 +513,15 @@ void insertCompatModeArg(struct ArgumentList *target, uint8_t modes) { } // Loads both global and title launch arguments, returning pointer to a merged list -struct ArgumentList *loadLaunchArgumentLists(struct Target *target) { +ArgumentList *loadLaunchArgumentLists(Target *target) { int res = 0; // Initialize global argument list - struct ArgumentList *globalArguments = calloc(sizeof(struct ArgumentList), 1); + ArgumentList *globalArguments = calloc(sizeof(ArgumentList), 1); if ((res = getGlobalLaunchArguments(globalArguments))) { printf("WARN: Failed to load global launch arguments: %d\n", res); } // Initialize title list and merge global into it - struct ArgumentList *titleArguments = calloc(sizeof(struct ArgumentList), 1); + ArgumentList *titleArguments = calloc(sizeof(ArgumentList), 1); if ((res = getTitleLaunchArguments(titleArguments, target))) { printf("WARN: Failed to load title arguments: %d\n", res); }