diff --git a/.gitignore b/.gitignore index dd518ae..cb68772 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ ipl.rom *.zip /subprojects/libogc2 +/subprojects/libfat !res/qoob_pro_none_upgrade.elf !res/qoob_sx_13c_upgrade.elf diff --git a/README.md b/README.md index ac78819..5af6c18 100644 --- a/README.md +++ b/README.md @@ -11,36 +11,97 @@ Supported targets: ## Usage -gekkoboot will attempt to load DOLs from the following locations in order: -- USB Gecko in Card Slot B -- SD Gecko in Card Slot B -- USB Gecko in Card Slot A -- SD Gecko in Card Slot A -- SD2SP2 +gekkoboot will attempt to load DOLs from the following devices in order: +1. USB Gecko in Card Slot B +2. SD Gecko in Card Slot B +3. USB Gecko in Card Slot A +4. SD Gecko in Card Slot A +5. SD2SP2 -You can use button shortcuts to keep alternate software on quick access. -When loading from an SD card, gekkoboot will look for and load different filenames -depending on what buttons are being held: +For each device, gekkoboot checks for the presence of the following files in order: +1. `gekkoboot.ini` +2. File matching held button (`a.dol`, `x.dol`, etc.) +3. `ipl.dol` - Button Held | File Loaded --------------|-------------- - *None* | `/ipl.dol` - A | `/a.dol` - B | `/b.dol` - X | `/x.dol` - Y | `/y.dol` - Z | `/z.dol` - Start | `/start.dol` +If no file is found, the next device is attempted. After all devices are attempted, the system will reboot into the onboard IPL (original GameCube intro and menu). -CLI files are also supported. +Creating an `gekkoboot.ini` configuration file is the recommended route. -If the selected shortcut file cannot be loaded, gekkoboot will fall back to -`/ipl.dol`. If that cannot be loaded either, the next device will be searched. -If all fails, gekkoboot will reboot to the onboard IPL (original GameCube intro -and menu). +> [!IMPORTANT] +> Be careful not to touch any of the analog controls (sticks and triggers) when powering on as this is when they are calibrated. -Holding D-Pad Left or the reset button will skip gekkoboot functionality and -reboot straight into the onboard IPL. +**Something not working?** See the [troubleshooting section](#troubleshooting). + +### Configuration + +Create a file named `gekkoboot.ini` at the root of your SD card. This file should be formatted using INI syntax, which is basic `NAME=some value`. The following parameters are supported: + +> [!IMPORTANT] +> All values are case sensitive. + + Parameter | Description +------------------|------------- + `{SHORTCUT}` | Shortcut action. + `{SHORTCUT}_ARG` | CLI argument passed to shortcut DOL. + `DEBUG` | Set to `1` to enable debug mode. + +Replace `{SHORTCUT}` with one of the following: `A`, `B`, `X`, `Y`, `Z`, `START`, `DEFAULT`. + +Shortcut action my be one of: + + Value | Description +------------|------------- + A filepath | Path to a DOL to load. All paths are relative to the device root. + `ONBOARD` | Reboot into the onboard IPL (original GameCube intro and menu). + `USBGECKO` | Attempt to load from USB Gecko. + +Holding a button during boot will activate the shortcut with the matching name. If no button is held, `DEFAULT` is used (if unspecified, the `DEFAULT` action is `/ipl.dol`). + +Specify one `{SHORTCUT}_ARG` per CLI argument. You may specify as many as you like. Also consider using a CLI file. + +For example, this configuration would boot straight into Swiss by default, +or GBI if you held B, or the original GameCube intro if you held Z: + +```ini +DEFAULT=swiss.dol +B=gbi.dol +Z=ONBOARD +``` + +This configuration would boot into the original GameCube intro by default, +or Swiss if you held Z, or GBI if you held X or Y: + +```ini +DEFAULT=ONBOARD +Z=swiss.dol +X=gbi.dol +X_ARG=--zoom=2 +Y=gbi.dol +Y_ARG=--zoom=3 +Y_ARG=--vfilter=.5:.5:.0:.5:.0:.5 +``` + +Comments may be included by starting the line with the `#` character. These lines will be ignored. + +See the [Special Features](#special-features) section for additional functionality. + +### Button Files + +> [!IMPORTANT] +> If a config file is found, this behavior does not apply. + +The following buttons can be used as shortcuts to load the associated filenames when a configuration file is not present: + + Button Held | File Loaded +-------------|-------------- + A | `a.dol` + B | `b.dol` + X | `x.dol` + Y | `y.dol` + Z | `z.dol` + Start | `start.dol` + +Holding a button during boot will activate the shortcut. If no button is held, `ipl.dol` is used. For example, this configuration would boot straight into Swiss by default, or GBI if you held B, or the original GameCube intro if you held D-Pad Left: @@ -52,25 +113,27 @@ or Swiss if you held Z, or GBI if you held B: - `/z.dol` - Swiss - `/b.dol` - GBI -**Pro-tip:** You can prevent files from showing in Swiss by marking them as -hidden files on the SD card. +> [!TIP] +> You can prevent files from showing in Swiss by marking them as hidden files on the SD card. -If you hold multiple buttons, the highest in the table takes priority. -Be careful not to touch any of the analog controls (sticks and triggers) when -powering on as this is when they are calibrated. +### Special Features -gekkoboot also acts as a server for @emukidid's [usb-load](https://github.com/emukidid/gc-usb-load), -should you want to use it for development purposes. +CLI files are supported. They will be append after any CLI args defined in the config file. -**Something not working?** See the [troubleshooting section](#troubleshooting). +Holding D-Pad Left or the reset button will skip gekkoboot functionality and +reboot straight into the onboard IPL. +Holding D-Pad Down enables debug mode. You can hold this along with a shortcut button. + +gekkoboot also acts as a server for @emukidid's [usb-load](https://github.com/emukidid/gc-usb-load), +should you want to use it for development purposes. ## Installation Download and extract the [latest release]. -Prepare your SD card by copying DOLs onto the SD card and renaming them -according the table above. +Prepare your SD card by creating a configuration file and/or copying DOLs onto the SD card and renaming them +according to either the [Configuration](#configuration) or [Button Files](#button-files) instructions above. ### PicoBoot @@ -153,10 +216,9 @@ It will be saved as `boot.dol` and can be used in conjunction with the various ## Troubleshooting -gekkoboot displays useful diagnostic messages as it attempts to load the selected DOL. -But it's so fast you may not have time to read or even see them. -If you hold the down direction on the D-Pad, the messages will remain on screen -until you let go. +Enable debug mode by holding d-pad in the down direction. This will allow you to read the diagnostic messages as well as enable more verbose output. Look for warning messages about unrecognized configuration parameters, file read failures, etc. + +If multiple shortcut buttons are held, the highest in the table takes priority. When choosing a shortcut button, beware that some software checks for buttons held at boot to alter certain behaviors. diff --git a/buildtools/qoob_injector.py b/buildtools/qoob_injector.py index 394be55..5d699b8 100755 --- a/buildtools/qoob_injector.py +++ b/buildtools/qoob_injector.py @@ -15,16 +15,18 @@ out = bytearray(f.read()) if bios.endswith(".gcb"): - if len(img) > 128 * 1024: - raise "Qoob Pro BIOS image too big to fit in flasher" + oversize_bytes = len(img) - (128 * 1024) + if oversize_bytes > 0: + raise Exception("Qoob Pro BIOS image too big to fit in flasher. %i bytes too large" % (oversize_bytes)) msg = b"gekkoboot for QOOB Pro\0" msg_offset = 0x1A68 img_offset = 0x1AE0 if bios.endswith(".qbsx"): - if len(img) > 62800: - raise "Qoob SX BIOS image too big to fit in flasher" + oversize_bytes = len(img) - 62800 + if oversize_bytes > 0: + raise Exception("Qoob SX BIOS image too big to fit in flasher. %i bytes too large" % (oversize_bytes)) msg = b"gekkoboot for QOOB SX\0" msg_offset = 7240 diff --git a/devkitPPC.ini b/devkitPPC.ini index aae6b61..19e5d76 100644 --- a/devkitPPC.ini +++ b/devkitPPC.ini @@ -1,6 +1,7 @@ [constants] prefix = 'powerpc-eabi-' -common_args = ['-DGEKKO','-mogc','-mcpu=750','-meabi','-mhard-float'] +platform_args = ['-mogc'] +common_args = ['-DGEKKO','-mcpu=750','-meabi','-mhard-float'] + platform_args link_args = [] [binaries] diff --git a/devkitPPCEmu.ini b/devkitPPCEmu.ini new file mode 100644 index 0000000..d3ebf0c --- /dev/null +++ b/devkitPPCEmu.ini @@ -0,0 +1,2 @@ +[constants] +platform_args = ['-mrvl','-DEMU_BUILD'] diff --git a/meson.build b/meson.build index 3a12cab..4be3aac 100644 --- a/meson.build +++ b/meson.build @@ -8,13 +8,33 @@ project( }, ) +emu_build = get_option('emu_build') + +platform = 'cube' +libogc_libs = ['ogc'] + +if emu_build + platform = 'wii' + libogc_libs += ['bte', 'wiiuse'] +endif + libogc_proj = subproject( 'libogc2', default_options: { - 'libraries': ['ogc'], + 'platform': platform, + 'libraries': libogc_libs, }, ) -libogc_deps = libogc_proj.get_variable('deps') +libogc_deps_dic = libogc_proj.get_variable('deps') +libogc_deps = [] +foreach lib : libogc_libs + libogc_deps += [libogc_deps_dic[lib]] +endforeach + +if emu_build + libfat_proj = subproject('libfat') + libfat_dep = libfat_proj.get_variable('dep') +endif subdir('buildtools') subdir('res') @@ -33,8 +53,6 @@ compressed_exes = {} subdir('stub') -linker_script = meson.current_source_dir() / 'ogc.ld' - git = find_program('git') version_file = vcs_tag( command: [git, 'describe', '--always', '--tags', '--dirty'], @@ -49,51 +67,74 @@ font = custom_target( command: [psf2c, '@INPUT@', '@OUTPUT@', 'console_font_8x16'], ) +gekkoboot_link_args = [] +gekkoboot_link_depends = [] +if emu_build + # Always link the math library because it is used by wiiuse. + # It is never explicitly included so the --as-needed flag would omit it otherwise. + gekkoboot_link_args += ['-lm'] +else + linker_script = meson.current_source_dir() / 'ogc.ld' + gekkoboot_link_args += ['-T' + linker_script] + gekkoboot_link_depends += [linker_script] +endif + gekkoboot = executable( 'gekkoboot', 'source/main.c', - 'source/utils.c', + 'source/filesystem.c', + 'source/shortcut.c', + 'source/cli_args.c', + 'source/config.c', + 'source/types.c', 'source/fatfs/ff.c', 'source/fatfs/ffsystem.c', 'source/fatfs/ffunicode.c', 'source/ffshim.c', + 'source/inih/ini.c', version_file, font, - dependencies: [ - libogc_deps['ogc'], - stub_dep, - ], - link_args: ['-T' + linker_script], - link_depends: [linker_script], + dependencies: ( + libogc_deps + + [stub_dep] + + (emu_build? [libfat_dep] : []) + ), + link_args: gekkoboot_link_args, + link_depends: gekkoboot_link_depends, name_suffix: 'elf', ) -compressed_exes += { - 'gekkoboot': { - 'exe': gekkoboot, - 'link_args': [ - '-Wl,--section-start,.init=0x01300000' - ], - 'dol': true, - }, - 'gekkoboot_sx': { - 'exe': gekkoboot, - 'link_args': [ - # This is the same entry point as the original BIOS, - # but the recovery slot hangs on "starting flashed app..." - # when loading it. A lowmem image works just fine. - #'-Wl,--section-start,.init=0x01500000', - - # Makes the ELF smaller so it actually fits - '-Wl,--nmagic', - ], - }, - 'gekkoboot_lowmem': { - 'exe': gekkoboot, - 'dol': true, - }, -} -subdir('packer') +if emu_build + dols += {'gekkoboot_emu': gekkoboot} +else + compressed_exes += { + 'gekkoboot': { + 'exe': gekkoboot, + 'link_args': [ + '-Wl,--section-start,.init=0x01300000' + ], + 'dol': true, + }, + 'gekkoboot_sx': { + 'exe': gekkoboot, + 'link_args': [ + # This is the same entry point as the original BIOS, + # but the recovery slot hangs on "starting flashed app..." + # when loading it. A lowmem image works just fine. + #'-Wl,--section-start,.init=0x01500000', + + # Makes the ELF smaller so it actually fits + '-Wl,--nmagic', + ], + }, + 'gekkoboot_lowmem': { + 'exe': gekkoboot, + 'dol': true, + }, + } + + subdir('packer') +endif foreach name, exe: dols dol = custom_target( @@ -106,87 +147,89 @@ foreach name, exe: dols set_variable(name + '_dol', dol) endforeach -if full_rom_opt.allowed() - qoob_pro = custom_target( - 'qoob_pro', - input: [gekkoboot_dol, ipl_rom], - output: 'gekkoboot_qoob_pro.gcb', +if not emu_build + if full_rom_opt.allowed() + qoob_pro = custom_target( + 'qoob_pro', + input: [gekkoboot_dol, ipl_rom], + output: 'gekkoboot_qoob_pro.gcb', + command: [dol2ipl, '@OUTPUT@', '@INPUT@'], + build_by_default: true, + install: true, + install_dir: '/qoob_pro', + ) + qoob_pro_updater_tgt = custom_target( + 'qoob_pro_updater', + input: [qoob_pro, qoob_pro_updater], + output: 'qoob_pro_gekkoboot_upgrade.elf', + command: [qoob_injector, '@INPUT@', '@OUTPUT@'], + build_by_default: true, + install: true, + install_dir: '/qoob_pro', + ) + endif + + gekkoboot_sx_stripped = custom_target( + gekkoboot_sx_compressed.name() + '_stripped', + input: gekkoboot_sx_compressed, + output: gekkoboot_sx_compressed.name() + '_stripped.elf', + command: [objcopy, '-S', '@INPUT@', '@OUTPUT@'], + ) + qoob_sx = custom_target( + 'qoob_sx', + input: gekkoboot_sx_stripped, + output: 'gekkoboot_qoob_sx.qbsx', command: [dol2ipl, '@OUTPUT@', '@INPUT@'], - build_by_default: true, - install: true, - install_dir: '/qoob_pro', ) - qoob_pro_updater_tgt = custom_target( - 'qoob_pro_updater', - input: [qoob_pro, qoob_pro_updater], - output: 'qoob_pro_gekkoboot_upgrade.elf', + qoob_sx_updater_tgt = custom_target( + 'qoob_sx_updater', + input: [qoob_sx, qoob_sx_updater], + output: 'qoob_sx_gekkoboot_upgrade.elf', command: [qoob_injector, '@INPUT@', '@OUTPUT@'], build_by_default: true, install: true, - install_dir: '/qoob_pro', + install_dir: '/', ) -endif - -gekkoboot_sx_stripped = custom_target( - gekkoboot_sx_compressed.name() + '_stripped', - input: gekkoboot_sx_compressed, - output: gekkoboot_sx_compressed.name() + '_stripped.elf', - command: [objcopy, '-S', '@INPUT@', '@OUTPUT@'], -) -qoob_sx = custom_target( - 'qoob_sx', - input: gekkoboot_sx_stripped, - output: 'gekkoboot_qoob_sx.qbsx', - command: [dol2ipl, '@OUTPUT@', '@INPUT@'], -) -qoob_sx_updater_tgt = custom_target( - 'qoob_sx_updater', - input: [qoob_sx, qoob_sx_updater], - output: 'qoob_sx_gekkoboot_upgrade.elf', - command: [qoob_injector, '@INPUT@', '@OUTPUT@'], - build_by_default: true, - install: true, - install_dir: '/', -) -viper = custom_target( - 'viper', - input: gekkoboot_dol, - output: 'gekkoboot_viper.vgc', - command: [dol2ipl, '@OUTPUT@', '@INPUT@'], - build_by_default: true, - install: true, - install_dir: '/', -) + viper = custom_target( + 'viper', + input: gekkoboot_dol, + output: 'gekkoboot_viper.vgc', + command: [dol2ipl, '@OUTPUT@', '@INPUT@'], + build_by_default: true, + install: true, + install_dir: '/', + ) -pico = custom_target( - 'pico', - input: gekkoboot_dol, - output: 'gekkoboot_pico.uf2', - command: [dol2ipl, '@OUTPUT@', '@INPUT@'], - build_by_default: true, - install: true, - install_dir: '/', -) + pico = custom_target( + 'pico', + input: gekkoboot_dol, + output: 'gekkoboot_pico.uf2', + command: [dol2ipl, '@OUTPUT@', '@INPUT@'], + build_by_default: true, + install: true, + install_dir: '/', + ) -gci = custom_target( - 'gci', - input: gekkoboot_lowmem_dol, - output: 'gekkoboot_memcard.gci', - command: [dol2gci, '@INPUT@', '@OUTPUT@', 'boot.dol'], - build_by_default: true, - install: true, - install_dir: '/', -) + gci = custom_target( + 'gci', + input: gekkoboot_lowmem_dol, + output: 'gekkoboot_memcard.gci', + command: [dol2gci, '@INPUT@', '@OUTPUT@', 'boot.dol'], + build_by_default: true, + install: true, + install_dir: '/', + ) -apploader = custom_target( - 'apploader', - input: gekkoboot_dol, - output: 'apploader.img', - command: [dol2ipl, '@OUTPUT@', '@INPUT@'], - build_by_default: true, - install: true, - install_dir: '/swiss_igr/swiss/patches', -) + apploader = custom_target( + 'apploader', + input: gekkoboot_dol, + output: 'apploader.img', + command: [dol2ipl, '@OUTPUT@', '@INPUT@'], + build_by_default: true, + install: true, + install_dir: '/swiss_igr/swiss/patches', + ) -install_data('README.md', install_dir: '/') + install_data('README.md', install_dir: '/') +endif diff --git a/meson.options b/meson.options index 03c9208..44417de 100644 --- a/meson.options +++ b/meson.options @@ -1 +1,2 @@ option('full_rom', type: 'feature', description: 'Whether to enable full ROM builds (Qoob Pro support)') +option('emu_build', type: 'boolean', value: false) diff --git a/packer/meson.build b/packer/meson.build index dabdf61..d14db95 100644 --- a/packer/meson.build +++ b/packer/meson.build @@ -19,7 +19,7 @@ packer_stub = static_library( link_with: packer_crt, dependencies: [ # For the headers - libogc_deps['ogc'].partial_dependency( + libogc_deps_dic['ogc'].partial_dependency( compile_args: true, sources: true, ), diff --git a/source/cli_args.c b/source/cli_args.c new file mode 100644 index 0000000..d254c08 --- /dev/null +++ b/source/cli_args.c @@ -0,0 +1,95 @@ +#include "cli_args.h" +#include +#include +#include + +#define MAX_NUM_ARGV 1024 + +// 0 - Failure +// 1 - OK/Empty +int +parse_cli_args(struct __argv *argv, const char **cli_options_strs, int num_cli_options_strs) { + kprintf("Parsing CLI args...\n"); + + argv->argc = 0; + argv->length = 0; + argv->commandLine = NULL; + + const char *args[MAX_NUM_ARGV]; + size_t arg_lengths[MAX_NUM_ARGV]; + int argc = 0; + size_t argv_size = 1; + + for (int oi = 0; oi < num_cli_options_strs; ++oi) { + const char *cli_option_str = cli_options_strs[oi]; + int found_arg_start = false; + size_t arg_start_index = 0; + size_t arg_end_index = 0; + + int eof = false; + for (size_t i = 0; !eof; i++) { + eof = cli_option_str[i] == '\0'; + + // Check if we are at the end of a line. + if (cli_option_str[i] == '\n' || eof) { + // Check if we ever found a start to the arg. + if (found_arg_start) { + // Record the arg. + size_t line_len = (arg_end_index - arg_start_index) + 1; + args[argc] = cli_option_str + arg_start_index; + arg_lengths[argc] = line_len; + argc++; + argv_size += line_len + 1; + + if (argc == MAX_NUM_ARGV) { + kprintf("Reached max of %i args.\n", MAX_NUM_ARGV); + goto loop_break; + } + + // Reset. + found_arg_start = false; + } + } + // Check if we have a non-whitespace character. + else if (cli_option_str[i] != ' ' && cli_option_str[i] != '\t' + && cli_option_str[i] != '\r') { + // Record the start and end of the arg. + if (!found_arg_start) { + found_arg_start = true; + arg_start_index = i; + } + arg_end_index = i; + } + } + } + +loop_break: + + if (argc == 0) { + kprintf("No args found\n"); + return 1; + } + + kprintf("Found %i args. Size is %iB\n", argc, argv_size); + + char *command_line = (char *) malloc(argv_size); + if (!command_line) { + kprintf("Couldn't allocate memory for args\n"); + return 0; + } + + size_t position = 0; + for (int i = 0; i < argc; i++) { + memcpy(command_line + position, args[i], arg_lengths[i]); + position += arg_lengths[i]; + command_line[position] = '\0'; + position += 1; + } + command_line[position] = '\0'; + + argv->argc = argc; + argv->length = argv_size; + argv->commandLine = command_line; + + return 1; +} \ No newline at end of file diff --git a/source/cli_args.h b/source/cli_args.h new file mode 100644 index 0000000..b2360b4 --- /dev/null +++ b/source/cli_args.h @@ -0,0 +1,8 @@ +#ifndef INC_CLI_ARGS_H +#define INC_CLI_ARGS_H +#include + +int +parse_cli_args(struct __argv *argv, const char **cli_options_strs, int num_cli_options_strs); + +#endif diff --git a/source/config.c b/source/config.c new file mode 100644 index 0000000..6110956 --- /dev/null +++ b/source/config.c @@ -0,0 +1,104 @@ +#include "config.h" +#include "filesystem.h" +#include "inih/ini.h" +#include +#include +#include + +const char *default_config_path = "/gekkoboot.ini"; + +void +default_config(CONFIG *config) { + // TODO: Should free strings if this function will ever be used more than once. + config->debug_enabled = false; + + for (int i = 0; i < NUM_SHORTCUTS; ++i) { + config->shortcut_actions[i].type = BOOT_TYPE_NONE; + config->shortcut_actions[i].dol_path = NULL; + config->shortcut_actions[i].dol_cli_options_strs = NULL; + config->shortcut_actions[i].num_dol_cli_options_strs = 0; + } + config->shortcut_actions[0].type = BOOT_TYPE_DOL; + config->shortcut_actions[0].dol_path = shortcuts[0].path; +} + +void +handle_boot_action(const char *value, BOOT_ACTION *action) { + if (strcmp(value, "ONBOARD") == 0) { + action->type = BOOT_TYPE_ONBOARD; + } else if (strcmp(value, "USBGECKO") == 0) { + action->type = BOOT_TYPE_USBGECKO; + } else { + size_t len = strlen(value); + if ((len < 5 || strcmp(value + (len - 4), ".dol") != 0) + && (len < 9 || strcmp(value + (len - 8), ".dol+cli") != 0)) { + kprintf("->> !! Warning: Configured filename does not look like a DOL\n"); + } + action->type = BOOT_TYPE_DOL; + action->dol_path = strdup(value); + } +} + +void +handle_cli_options(const char *value, BOOT_ACTION *action) { + if (action->num_dol_cli_options_strs % 100 == 0) { + action->dol_cli_options_strs = (const char **) realloc( + action->dol_cli_options_strs, + (action->num_dol_cli_options_strs + 100) * sizeof(const char *) + ); + } + action->dol_cli_options_strs[action->num_dol_cli_options_strs++] = strdup(value); +} + +// 0 - Failure +// 1 - OK +int +ini_parse_handler(void *_config, const char *section, const char *name, const char *value) { + CONFIG *config = (CONFIG *) _config; + + if (strcmp(name, "DEBUG") == 0) { + config->debug_enabled = strcmp(value, "1") == 0; + return 1; + } + + for (int i = 0; i < NUM_SHORTCUTS; ++i) { + if (strcmp(name, shortcuts[i].config_name) == 0) { + handle_boot_action(value, &config->shortcut_actions[i]); + return 1; + } + if (strcmp(name, shortcuts[i].config_cli_name) == 0) { + handle_cli_options(value, &config->shortcut_actions[i]); + return 1; + } + } + + kprintf("->> !! Unknown config entry: %s\n", name); + // return 0; + return 1; +} + +// 0 - Failure +// 1 - OK +int +parse_config(CONFIG *config, const char *config_str) { + default_config(config); + return ini_parse_string(config_str, ini_parse_handler, config) == 0; +} + +void +print_config(CONFIG *config) { + kprintf("debug_enabled: %i\n", config->debug_enabled); + for (int i = 0; i < NUM_SHORTCUTS; ++i) { + kprintf("shortcuts[%s].action: %s\n", + shortcuts[i].name, + get_boot_type_name(config->shortcut_actions[i].type)); + if (config->shortcut_actions[i].type == BOOT_TYPE_DOL) { + kprintf("shortcuts[%s].dol_path: %s\n", + shortcuts[i].name, + config->shortcut_actions[i].dol_path); + // kprintf("shortcuts[%s].num_dol_cli_options_strs: %i\n", + // shortcuts[i].name, + // config->shortcut_actions[i].num_dol_cli_options_strs); + } + } +} diff --git a/source/config.h b/source/config.h new file mode 100644 index 0000000..b50122c --- /dev/null +++ b/source/config.h @@ -0,0 +1,17 @@ +#ifndef INC_CONFIG_H +#define INC_CONFIG_H +#include "shortcut.h" +#include "types.h" + +typedef struct { + int debug_enabled; + BOOT_ACTION shortcut_actions[NUM_SHORTCUTS]; +} CONFIG; + +extern const char *default_config_path; +int +parse_config(CONFIG *config, const char *config_str); +void +print_config(CONFIG *config); + +#endif diff --git a/source/filesystem.c b/source/filesystem.c new file mode 100644 index 0000000..bbf93ed --- /dev/null +++ b/source/filesystem.c @@ -0,0 +1,124 @@ +#include "filesystem.h" + +#ifdef EMU_BUILD +#include "filesystem_emu.c" +#else +#include "fatfs/ff.h" +#include "ffshim.h" +#include +#include + +FATFS fs; +FS_RESULT +fs_mount(const DISC_INTERFACE *iface_) { + iface = iface_; + return f_mount(&fs, "", 1); +} + +void +fs_unmount() { + f_unmount(""); + iface->shutdown(); + iface = NULL; +} + +void +fs_get_volume_label(const char *path, char *label) { + FRESULT res = f_getlabel(path, label, NULL); + if (res != FR_OK) { + *label = '\0'; + } +} + +FS_RESULT +_fs_read_file(void **contents_, const char *path, int is_string) { + FIL file; + FRESULT result = f_open(&file, path, FA_READ); + if (result != FR_OK) { + if (result == FR_NO_FILE || result == FR_NO_PATH) { + kprintf("File not found\n"); + return FS_NO_FILE; + } + kprintf("->> !! Failed to open file: %s\n", get_fs_result_message(result)); + return result; + } + + size_t size = f_size(&file); + if (size <= 0) { + kprintf("->> !! File is empty\n"); + return FS_FILE_EMPTY; + } + kprintf("File size: %iB\n", size); + + // Malloc an extra byte if we are reading as a string incase we need to add NUL character. + void *contents = malloc(size + (is_string ? 1 : 0)); + if (!contents) { + kprintf("->> !! Couldn't allocate memory for file\n"); + return FS_NOT_ENOUGH_MEMORY; + } + + kprintf("Reading file...\n"); + UINT _; + result = f_read(&file, contents, size, &_); + if (result != FR_OK) { + kprintf("->> !! Failed to read file: %s\n", get_fs_result_message(result)); + return result; + } + + f_close(&file); + + // Ensure files read as strings end with NUL character. + if (is_string) { + // This is safe because we malloc an extra byte above if reading as string. + ((char *) contents)[size] = '\0'; + } + + *contents_ = contents; + return FS_OK; +} +#endif + +FS_RESULT +fs_read_file(void **contents, const char *path) { + return _fs_read_file(contents, path, false); +} +FS_RESULT +fs_read_file_string(const char **contents, const char *path) { + return _fs_read_file((void **) contents, path, true); +} + +#define NUM_FS_RESULT_MSGS 22 +const char *fs_result_msgs[NUM_FS_RESULT_MSGS] = { + /*FS_OK ( 0)*/ "Succeeded", + /*FS_DISK_ERR ( 1)*/ "A hard error occurred in the low level disk I/O layer", + /*FS_INT_ERR ( 2)*/ "Assertion failed", + /*FS_NOT_READY ( 3)*/ "Device not ready", + /*FS_NO_FILE ( 4)*/ "Could not find the file", + /*FS_NO_PATH ( 5)*/ "Could not find the path", + /*FS_INVALID_NAME ( 6)*/ "The path name format is invalid", + /*FS_DENIED ( 7)*/ "Access denied due to prohibited access or directory full", + /*FS_EXIST ( 8)*/ "Access denied due to prohibited access", + /*FS_INVALID_OBJECT ( 9)*/ "The file/directory object is invalid", + /*FS_WRITE_PROTECTED (10)*/ "The physical drive is write protected", + /*FS_INVALID_DRIVE (11)*/ "The logical drive number is invalid", + /*FS_NOT_ENABLED (12)*/ "The volume has no work area", + /*FS_NO_FILESYSTEM (13)*/ "There is no valid FAT volume", + /*FS_MKFS_ABORTED (14)*/ "The f_mkfs() aborted due to any problem", + /*FS_TIMEOUT (15)*/ + "Could not get a grant to access the volume within defined period", + /*FS_LOCKED (16)*/ + "The operation is rejected according to the file sharing policy", + /*FS_NOT_ENOUGH_CORE (17)*/ "LFN working buffer could not be allocated", + /*FS_TOO_MANY_OPEN_FILES (18)*/ "Number of open files > FF_FS_LOCK", + /*FS_INVALID_PARAMETER (19)*/ "Given parameter is invalid", + /*FS_FILE_EMPTY (20)*/ "File is empty", + /*FS_NOT_ENOUGH_MEMORY (21)*/ "Not enough memory", +}; + +const char * +get_fs_result_message(FS_RESULT result) { + if (result < 0 || result >= NUM_FS_RESULT_MSGS) { + return "Unknown"; + } + return fs_result_msgs[result]; +} diff --git a/source/filesystem.h b/source/filesystem.h new file mode 100644 index 0000000..4ae983e --- /dev/null +++ b/source/filesystem.h @@ -0,0 +1,45 @@ +#ifndef INC_FILESYSTEM_H +#define INC_FILESYSTEM_H +#include + +// See ./fatfs/ff.h:276 +typedef enum { + FS_OK = 0, /* ( 0) Succeeded */ + FS_DISK_ERR, /* ( 1) A hard error occurred in the low level disk I/O layer */ + FS_INT_ERR, /* ( 2) Assertion failed */ + FS_NOT_READY, /* ( 3) The physical drive cannot work */ + FS_NO_FILE, /* ( 4) Could not find the file */ + FS_NO_PATH, /* ( 5) Could not find the path */ + FS_INVALID_NAME, /* ( 6) The path name format is invalid */ + FS_DENIED, /* ( 7) Access denied due to prohibited access or directory full */ + FS_EXIST, /* ( 8) Access denied due to prohibited access */ + FS_INVALID_OBJECT, /* ( 9) The file/directory object is invalid */ + FS_WRITE_PROTECTED, /* (10) The physical drive is write protected */ + FS_INVALID_DRIVE, /* (11) The logical drive number is invalid */ + FS_NOT_ENABLED, /* (12) The volume has no work area */ + FS_NO_FILESYSTEM, /* (13) There is no valid FAT volume */ + FS_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any problem */ + FS_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */ + FS_LOCKED, /* (16) The operation is rejected according to the file sharing policy */ + FS_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */ + FS_TOO_MANY_OPEN_FILES, /* (18) Number of open files > FF_FS_LOCK */ + FS_INVALID_PARAMETER, /* (19) Given parameter is invalid */ + FS_FILE_EMPTY, /* (20) File is empty */ + FS_NOT_ENOUGH_MEMORY, /* (21) Not enough memory to malloc file */ +} FS_RESULT; +// Changes to this enum should also be made to fs_result_msgs in filesystem.c + +FS_RESULT +fs_mount(const DISC_INTERFACE *iface_); +void +fs_unmount(); +void +fs_get_volume_label(const char *path, char *label); +FS_RESULT +fs_read_file(void **contents, const char *path); +FS_RESULT +fs_read_file_string(const char **contents, const char *path); +const char * +get_fs_result_message(FS_RESULT result); + +#endif diff --git a/source/filesystem_emu.c b/source/filesystem_emu.c new file mode 100644 index 0000000..890242d --- /dev/null +++ b/source/filesystem_emu.c @@ -0,0 +1,130 @@ +#include "filesystem.h" +#include +#include +#include +#include +#include +#include + +const DISC_INTERFACE *wiface = NULL; +const char * +get_errno_message(int _errno); + +FS_RESULT +fs_mount(const DISC_INTERFACE *iface_) { + wiface = iface_; + if (!fatMountSimple("sd", wiface)) { + return FS_NOT_READY; + } + return FS_OK; +} + +void +fs_unmount() { + fatUnmount("sd"); + wiface->shutdown(); + wiface = NULL; +} + +void +fs_get_volume_label(const char *path, char *label) { + strcpy(label, ""); +} + +FS_RESULT +_fs_read_file(void **contents_, const char *path, int is_string) { + char full_path[strlen(path) + 5]; + sprintf(full_path, "sd:/%s", path); + + errno = 0; + FILE *file = fopen(full_path, "rb"); + if (!file) { + if (errno == ENOENT) { + kprintf("File not found\n"); + return FS_NO_FILE; + } + kprintf("->> !! Failed to open file: %s\n", get_errno_message(errno)); + return FS_INT_ERR; + } + + fseek(file, 0, SEEK_END); + size_t size = ftell(file); + rewind(file); + + if (size <= 0) { + kprintf("->> !! File is empty\n"); + return FS_FILE_EMPTY; + } + kprintf("File size: %iB\n", size); + + // Malloc an extra byte if we are reading as a string incase we need to add NUL character. + void *contents = malloc(size + (is_string ? 1 : 0)); + if (!contents) { + kprintf("->> !! Couldn't allocate memory for file\n"); + return FS_NOT_ENOUGH_MEMORY; + } + + kprintf("Reading file...\n"); + errno = 0; + if (!fread(contents, size, 1, file)) { + kprintf("->> !! Failed to read file: %s\n", get_errno_message(errno)); + return FS_INT_ERR; + } + + fclose(file); + + // Ensure files read as strings end with NUL character. + if (is_string) { + // This is safe because we malloc an extra byte above if reading as string. + ((char *) contents)[size] = '\0'; + } + + *contents_ = contents; + return FS_OK; +} + +#define NUM_ERRNO_MSGS 35 +const char *errno_msgs[NUM_ERRNO_MSGS] = { + /*OK ( 0)*/ "OK", + /*EPERM ( 1)*/ "Operation not permitted", + /*ENOENT ( 2)*/ "No such file or directory", + /*ESRCH ( 3)*/ "No such process", + /*EINTR ( 4)*/ "Interrupted system call", + /*EIO ( 5)*/ "I/O error", + /*ENXIO ( 6)*/ "No such device or address", + /*E2BIG ( 7)*/ "Argument list too long", + /*ENOEXEC ( 8)*/ "Exec format error", + /*EBADF ( 9)*/ "Bad file number", + /*ECHILD (10)*/ "No child processes", + /*EAGAIN (11)*/ "Try again", + /*ENOMEM (12)*/ "Out of memory", + /*EACCES (13)*/ "Permission denied", + /*EFAULT (14)*/ "Bad address", + /*ENOTBLK (15)*/ "Block device required", + /*EBUSY (16)*/ "Device or resource busy", + /*EEXIST (17)*/ "File exists", + /*EXDEV (18)*/ "Cross-device link", + /*ENODEV (19)*/ "No such device", + /*ENOTDIR (20)*/ "Not a directory", + /*EISDIR (21)*/ "Is a directory", + /*EINVAL (22)*/ "Invalid argument", + /*ENFILE (23)*/ "File table overflow", + /*EMFILE (24)*/ "Too many open files", + /*ENOTTY (25)*/ "Not a typewriter", + /*ETXTBSY (26)*/ "Text file busy", + /*EFBIG (27)*/ "File too large", + /*ENOSPC (28)*/ "No space left on device", + /*ESPIPE (29)*/ "Illegal seek", + /*EROFS (30)*/ "Read-only file system", + /*EMLINK (31)*/ "Too many links", + /*EPIPE (32)*/ "Broken pipe", + /*EDOM (33)*/ "Math argument out of domain of func", + /*ERANGE (34)*/ "Math result not representable", +}; +const char * +get_errno_message(int _errno) { + if (_errno < 0 || _errno >= NUM_ERRNO_MSGS) { + return "Unknown"; + } + return errno_msgs[_errno]; +} \ No newline at end of file diff --git a/source/inih/LICENSE.txt b/source/inih/LICENSE.txt new file mode 100644 index 0000000..cb7ee2d --- /dev/null +++ b/source/inih/LICENSE.txt @@ -0,0 +1,27 @@ + +The "inih" library is distributed under the New BSD license: + +Copyright (c) 2009, Ben Hoyt +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Ben Hoyt nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY BEN HOYT ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BEN HOYT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/inih/README.md b/source/inih/README.md new file mode 100644 index 0000000..b472277 --- /dev/null +++ b/source/inih/README.md @@ -0,0 +1 @@ +https://github.com/benhoyt/inih diff --git a/source/inih/ini.c b/source/inih/ini.c new file mode 100644 index 0000000..509952d --- /dev/null +++ b/source/inih/ini.c @@ -0,0 +1,304 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2020, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#include "ini.h" + +#if !INI_USE_STACK +#if INI_CUSTOM_ALLOCATOR +#include +void* ini_malloc(size_t size); +void ini_free(void* ptr); +void* ini_realloc(void* ptr, size_t size); +#else +#include +#define ini_malloc malloc +#define ini_free free +#define ini_realloc realloc +#endif +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Used by ini_parse_string() to keep track of string parsing state. */ +typedef struct { + const char* ptr; + size_t num_left; +} ini_parse_string_ctx; + +/* Strip whitespace chars off end of given string, in place. Return s. */ +static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to NUL at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +static char* find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Similar to strncpy, but ensures dest (size bytes) is + NUL-terminated, and doesn't pad with NULs. */ +static char* strncpy0(char* dest, const char* src, size_t size) +{ + /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ + size_t i; + for (i = 0; i < size - 1 && src[i]; i++) + dest[i] = src[i]; + dest[i] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; + size_t max_line = INI_MAX_LINE; +#else + char* line; + size_t max_line = INI_INITIAL_ALLOC; +#endif +#if INI_ALLOW_REALLOC && !INI_USE_STACK + char* new_line; + size_t offset; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)ini_malloc(INI_INITIAL_ALLOC); + if (!line) { + return -2; + } +#endif + +#if INI_HANDLER_LINENO +#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) +#else +#define HANDLER(u, s, n, v) handler(u, s, n, v) +#endif + + /* Scan through stream line by line */ + while (reader(line, (int)max_line, stream) != NULL) { +#if INI_ALLOW_REALLOC && !INI_USE_STACK + offset = strlen(line); + while (offset == max_line - 1 && line[offset - 1] != '\n') { + max_line *= 2; + if (max_line > INI_MAX_LINE) + max_line = INI_MAX_LINE; + new_line = ini_realloc(line, max_line); + if (!new_line) { + ini_free(line); + return -2; + } + line = new_line; + if (reader(line + offset, (int)(max_line - offset), stream) == NULL) + break; + if (max_line >= INI_MAX_LINE) + break; + offset += strlen(line + offset); + } +#endif + + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (strchr(INI_START_COMMENT_PREFIXES, *start)) { + /* Start-of-line comment */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(start, NULL); + if (*end) + *end = '\0'; + rstrip(start); +#endif + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!HANDLER(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; +#if INI_CALL_HANDLER_ON_NEW_SECTION + if (!HANDLER(user, section, NULL, NULL) && !error) + error = lineno; +#endif + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = end + 1; +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + value = lskip(value); + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!HANDLER(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ +#if INI_ALLOW_NO_VALUE + *end = '\0'; + name = rstrip(start); + if (!HANDLER(user, section, name, NULL) && !error) + error = lineno; +#else + error = lineno; +#endif + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + ini_free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +/* An ini_reader function to read the next line from a string buffer. This + is the fgets() equivalent used by ini_parse_string(). */ +static char* ini_reader_string(char* str, int num, void* stream) { + ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; + const char* ctx_ptr = ctx->ptr; + size_t ctx_num_left = ctx->num_left; + char* strp = str; + char c; + + if (ctx_num_left == 0 || num < 2) + return NULL; + + while (num > 1 && ctx_num_left != 0) { + c = *ctx_ptr++; + ctx_num_left--; + *strp++ = c; + if (c == '\n') + break; + num--; + } + + *strp = '\0'; + ctx->ptr = ctx_ptr; + ctx->num_left = ctx_num_left; + return str; +} + +/* See documentation in header file. */ +int ini_parse_string(const char* string, ini_handler handler, void* user) { + ini_parse_string_ctx ctx; + + ctx.ptr = string; + ctx.num_left = strlen(string); + return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, + user); +} diff --git a/source/inih/ini.h b/source/inih/ini.h new file mode 100644 index 0000000..01efa6b --- /dev/null +++ b/source/inih/ini.h @@ -0,0 +1,182 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2020, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#ifndef INI_H +#define INI_H + +/* TODO: Move these to preprocessor defines */ +/* https://github.com/benhoyt/inih/blob/master/README.md#compile-time-options */ +#define INI_ALLOW_MULTILINE 0 + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Nonzero if ini_handler callback should accept lineno parameter. */ +#ifndef INI_HANDLER_LINENO +#define INI_HANDLER_LINENO 0 +#endif + +/* Visibility symbols, required for Windows DLLs */ +#ifndef INI_API +#if defined _WIN32 || defined __CYGWIN__ +# ifdef INI_SHARED_LIB +# ifdef INI_SHARED_LIB_BUILDING +# define INI_API __declspec(dllexport) +# else +# define INI_API __declspec(dllimport) +# endif +# else +# define INI_API +# endif +#else +# if defined(__GNUC__) && __GNUC__ >= 4 +# define INI_API __attribute__ ((visibility ("default"))) +# else +# define INI_API +# endif +#endif +#endif + +/* Typedef for prototype of handler function. */ +#if INI_HANDLER_LINENO +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value, + int lineno); +#else +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); +#endif + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +INI_API int ini_parse(const char* filename, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O (see also + ini_parse_string). */ +INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + +/* Same as ini_parse(), but takes a zero-terminated string with the INI data +instead of a file. Useful for parsing INI data from a network socket or +already in memory. */ +INI_API int ini_parse_string(const char* string, ini_handler handler, void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See https://github.com/benhoyt/inih/issues/21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Chars that begin a start-of-line comment. Per Python configparser, allow + both ; and # comments at the start of a line by default. */ +#ifndef INI_START_COMMENT_PREFIXES +#define INI_START_COMMENT_PREFIXES ";#" +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Maximum line length for any line in INI file (stack or heap). Note that + this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +/* Nonzero to allow heap line buffer to grow via realloc(), zero for a + fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is + zero. */ +#ifndef INI_ALLOW_REALLOC +#define INI_ALLOW_REALLOC 0 +#endif + +/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK + is zero. */ +#ifndef INI_INITIAL_ALLOC +#define INI_INITIAL_ALLOC 200 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Nonzero to call the handler at the start of each new section (with + name and value NULL). Default is to only call the handler on + each name=value pair. */ +#ifndef INI_CALL_HANDLER_ON_NEW_SECTION +#define INI_CALL_HANDLER_ON_NEW_SECTION 0 +#endif + +/* Nonzero to allow a name without a value (no '=' or ':' on the line) and + call the handler with value NULL in this case. Default is to treat + no-value lines as an error. */ +#ifndef INI_ALLOW_NO_VALUE +#define INI_ALLOW_NO_VALUE 0 +#endif + +/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory + allocation functions (INI_USE_STACK must also be 0). These functions must + have the same signatures as malloc/free/realloc and behave in a similar + way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ +#ifndef INI_CUSTOM_ALLOCATOR +#define INI_CUSTOM_ALLOCATOR 0 +#endif + + +#ifdef __cplusplus +} +#endif + +#endif /* INI_H */ diff --git a/source/main.c b/source/main.c index 53e782f..15396c9 100644 --- a/source/main.c +++ b/source/main.c @@ -1,186 +1,312 @@ -#include "fatfs/ff.h" -#include "ffshim.h" -#include "utils.h" +#include "cli_args.h" +#include "config.h" +#include "filesystem.h" +#include "shortcut.h" +#include "types.h" #include "version.h" #include +#include #include #include #include -#include #include #include #include #include +#ifndef EMU_BUILD #include "stub.h" +#include +extern u8 __xfb[]; +#else +#include +static void *__xfb = NULL; +#endif -#define VERBOSE_LOGGING 0 - -u8 *dol = NULL; -int dol_argc = 0; -#define MAX_NUM_ARGV 1024 -char *dol_argv[MAX_NUM_ARGV]; +// Global State +// -------------------- +int debug_enabled = false; u16 all_buttons_held; +// -------------------- -char *default_path = "/ipl.dol"; -struct shortcut { - u16 pad_buttons; - char *path; -} shortcuts[] = { - {PAD_BUTTON_A, "/a.dol"}, - {PAD_BUTTON_B, "/b.dol"}, - {PAD_BUTTON_X, "/x.dol"}, - {PAD_BUTTON_Y, "/y.dol"}, - {PAD_TRIGGER_Z, "/z.dol"}, - {PAD_BUTTON_START, "/start.dol"}, - // NOTE: Shouldn't use L, R or Joysticks as analog inputs are calibrated on boot. - // Should also avoid D-Pad as it is used for special functionality. -}; -int num_shortcuts = sizeof(shortcuts) / sizeof(shortcuts[0]); +void +scan_all_buttons_held() { + PAD_ScanPads(); + all_buttons_held = + (PAD_ButtonsHeld(PAD_CHAN0) | PAD_ButtonsHeld(PAD_CHAN1) + | PAD_ButtonsHeld(PAD_CHAN2) | PAD_ButtonsHeld(PAD_CHAN3)); +} void -dol_alloc(int size) { - int mram_size = (SYS_GetArenaHi() - SYS_GetArenaLo()); - kprintf("Memory available: %iB\n", mram_size); +wait_for_confirmation() { + // Wait until the A button or reset button is pressed. + int cur_state = true; + int last_state; + do { + VIDEO_WaitVSync(); + scan_all_buttons_held(); + last_state = cur_state; + cur_state = all_buttons_held & PAD_BUTTON_A; + } while (last_state || !cur_state); +} - kprintf("DOL size is %iB\n", size); +void +delay_exit() { + if (debug_enabled) { + // When debug is enabled, always wait for confirmation before exit. + kprintf("\nDEBUG: Press A to continue...\n"); + wait_for_confirmation(); + } +} - if (size <= 0) { - kprintf("Empty DOL\n"); - return; +// 0 - Failure +// 1 - OK/Does not exist +int +read_dol_file(u8 **dol_file, const char *path) { + *dol_file = NULL; + + kprintf("Trying DOL file: %s\n", path); + FS_RESULT result = fs_read_file((void **) dol_file, path); + if (result == FS_OK) { + kprintf("->> DOL loaded\n"); } + return (result == FS_OK || result == FS_NO_FILE || result == FS_FILE_EMPTY); +} - dol = (u8 *) memalign(32, size); +// 0 - Failure +// 1 - OK/Does not exist +int +read_config_file(const char **config_file, const char *path) { + *config_file = NULL; - if (!dol) { - kprintf("Couldn't allocate memory\n"); + kprintf("Trying config file: %s\n", path); + FS_RESULT result = fs_read_file_string(config_file, path); + if (result == FS_OK) { + kprintf("->> Config loaded\n"); } + return (result == FS_OK || result == FS_NO_FILE || result == FS_FILE_EMPTY); } -void -load_parse_cli(char *path) { - int path_length = strlen(path); - path[path_length - 3] = 'c'; - path[path_length - 2] = 'l'; - path[path_length - 1] = 'i'; - - kprintf("Reading %s\n", path); - FIL file; - FRESULT result = f_open(&file, path, FA_READ); - if (result != FR_OK) { - if (result == FR_NO_FILE) { - kprintf("CLI file not found\n"); - } else { - kprintf("Failed to open CLI file: %s\n", get_fresult_message(result)); - } - return; +// 0 - Failure +// 1 - OK/Does not exist/Skipped +int +read_cli_file(const char **cli_file, const char *dol_path) { + *cli_file = NULL; + + size_t path_len = strlen(dol_path); + if (path_len < 5 || strncmp(dol_path + path_len - 4, ".dol", 4) != 0) { + kprintf("Not reading CLI file: DOL path does not end in \".dol\"\n"); + return 1; } - size_t size = f_size(&file); - kprintf("CLI file size is %iB\n", size); + char path[path_len + 1]; + memcpy(path, dol_path, path_len - 3); + path[path_len - 3] = 'c'; + path[path_len - 2] = 'l'; + path[path_len - 1] = 'i'; + path[path_len] = '\0'; + + kprintf("Trying CLI file: %s\n", path); + FS_RESULT result = fs_read_file_string(cli_file, path); + if (result == FS_OK) { + kprintf("->> CLI file loaded\n"); + } + return (result == FS_OK || result == FS_NO_FILE || result == FS_FILE_EMPTY); +} - if (size <= 0) { - kprintf("Empty CLI file\n"); - return; +// 0 - Device should not be used. +// 1 - Device should be used. +int +load_config(BOOT_PAYLOAD *payload, int shortcut_index) { + // Attempt to read config file from mounted FAT device. + const char *config_file; + if (!read_config_file(&config_file, default_config_path)) { + return 1; + } + if (!config_file) { + return 0; } - char *cli = (char *) malloc(size + 1); + // Config file was found. + // Default to no action in case of failure. + int res = 1; + payload->type = BOOT_TYPE_NONE; - if (!cli) { - kprintf("Couldn't allocate memory for CLI file\n"); - return; + // Parse config file. + CONFIG config; + if (!parse_config(&config, config_file)) { + kprintf("->> !! Failed to parse config file\n"); + return res; } - UINT _; - f_read(&file, cli, size, &_); - f_close(&file); + // Set global state. + if (config.debug_enabled && !debug_enabled) { + kprintf("DEBUG: Debug enabled by config.\n"); + debug_enabled = true; + } - if (cli[size - 1] != '\0') { - cli[size] = '\0'; - size++; + // Print config. + if (debug_enabled) { + kprintf("\nDEBUG: About to print config. Press A to continue...\n"); + wait_for_confirmation(); + kprintf("----------\n"); + print_config(&config); + kprintf("----------\n\n"); + kprintf("DEBUG: Done printing. Press A to continue...\n"); + wait_for_confirmation(); } - // Parse CLI file - // https://github.com/emukidid/swiss-gc/blob/a0fa06d81360ad6d173acd42e4dd5495e268de42/cube/swiss/source/swiss.c#L1236 - dol_argv[dol_argc] = path; - dol_argc++; + // Choose boot action. + if (config.shortcut_actions[shortcut_index].type == BOOT_TYPE_NONE) { + kprintf("\"%s\" shortcut not configured\n", shortcuts[shortcut_index].name); + shortcut_index = 0; + } + kprintf("->> Using \"%s\" shortcut\n", shortcuts[shortcut_index].name); + BOOT_ACTION *action = &config.shortcut_actions[shortcut_index]; + + // Process boot action. + if (action->type == BOOT_TYPE_ONBOARD) { + kprintf("->> Shortcut action: Reboot to onboard IPL\n"); + payload->type = BOOT_TYPE_ONBOARD; + return res; + } + if (action->type == BOOT_TYPE_USBGECKO) { + kprintf("->> Shortcut action: Use USB Gecko\n"); + payload->type = BOOT_TYPE_USBGECKO; + return res; + } + if (action->type != BOOT_TYPE_DOL) { + // Should never happen. + kprintf("->> !! Internal Error: Unexpeted boot type: %i\n", action->type); + return res; + } + + kprintf("->> Shortcut action: Boot DOL\n"); - // First argument is at the beginning of the file - if (cli[0] != '\r' && cli[0] != '\n') { - dol_argv[dol_argc] = cli; - dol_argc++; + // Read DOL file. + u8 *dol_file; + if (!read_dol_file(&dol_file, action->dol_path) || !dol_file) { + kprintf("->> !! Unable to read DOL\n"); + return res; } - // Search for the others after each newline - for (int i = 0; i < size; i++) { - if (cli[i] == '\r' || cli[i] == '\n') { - cli[i] = '\0'; - } else if (cli[i - 1] == '\0') { - dol_argv[dol_argc] = cli + i; - dol_argc++; - if (dol_argc >= MAX_NUM_ARGV) { - kprintf("Reached max of %i args.\n", MAX_NUM_ARGV); - break; - } - } + // Attempt to read CLI file. + const char *cli_file; + if (!read_cli_file(&cli_file, action->dol_path)) { + return res; + } + if (!cli_file) { + kprintf("->> No CLI file\n"); } - kprintf("Found %i CLI args\n", dol_argc); + // Combine CLI options from config and CLI file. + const char **cli_options_strs = NULL; + int num_cli_options_strs = 0; + if (action->num_dol_cli_options_strs > 0) { + cli_options_strs = action->dol_cli_options_strs; + num_cli_options_strs = action->num_dol_cli_options_strs; + } + if (cli_file) { + cli_options_strs = + realloc(cli_options_strs, + (num_cli_options_strs + 1) * sizeof(const char *)); + cli_options_strs[num_cli_options_strs++] = cli_file; + } -#if VERBOSE_LOGGING - for (int i = 0; i < dol_argc; ++i) { - kprintf("arg%i: %s\n", i, dol_argv[i]); + // Parse CLI options. + if (num_cli_options_strs > 0) { + int parse_res = + parse_cli_args(&payload->argv, cli_options_strs, num_cli_options_strs); + if (cli_file) { + free((void *) cli_file); + } + if (!parse_res) { + return res; + } } -#endif + + // Return DOL boot payload. + payload->type = BOOT_TYPE_DOL; + payload->dol_file = dol_file; + return res; } +// 0 - Device should not be used. +// 1 - Device should be used. int -load_fat(const char *slot_name, const DISC_INTERFACE *iface_, char **paths, int num_paths) { - int res = 0; +load_shortcut_files(BOOT_PAYLOAD *payload, int shortcut_index) { + // Attempt to read shortcut paths from from mounted FAT device. + u8 *dol_file = NULL; + const char *dol_path = shortcuts[shortcut_index].path; + if (!read_dol_file(&dol_file, dol_path)) { + return 1; + } + if (!dol_file && shortcut_index != 0) { + shortcut_index = 0; + dol_path = shortcuts[shortcut_index].path; + if (!read_dol_file(&dol_file, dol_path)) { + return 1; + } + } + if (!dol_file) { + return 0; + } - kprintf("Trying %s\n", slot_name); + kprintf("Will boot DOL\n"); - FATFS fs; - iface = iface_; - FRESULT mount_result = f_mount(&fs, "", 1); - if (mount_result != FR_OK) { - kprintf("Couldn't mount %s: %s\n", slot_name, get_fresult_message(mount_result)); - goto end; + // Attempt to read CLI file. + const char *cli_file; + if (!read_cli_file(&cli_file, dol_path)) { + return 1; + } + if (!cli_file) { + kprintf("->> No CLI file\n"); } - char name[256]; - f_getlabel(slot_name, name, NULL); - kprintf("Mounted %s as %s\n", name, slot_name); - - for (int i = 0; i < num_paths; ++i) { - char *path = paths[i]; - kprintf("Reading %s\n", path); - FIL file; - FRESULT open_result = f_open(&file, path, FA_READ); - if (open_result != FR_OK) { - kprintf("Failed to open file: %s\n", get_fresult_message(open_result)); - continue; + // Parse CLI file. + if (cli_file) { + int res = parse_cli_args(&payload->argv, &cli_file, 1); + free((void *) cli_file); + if (!res) { + return 1; } + } - size_t size = f_size(&file); - dol_alloc(size); - if (!dol) { - continue; - } - UINT _; - f_read(&file, dol, size, &_); - f_close(&file); + payload->type = BOOT_TYPE_DOL; + payload->dol_file = dol_file; + return 1; +} + +// 0 - Device should not be used. +// 1 - Device should be used. +int +load_fat( + BOOT_PAYLOAD *payload, + const char *device_name, + const DISC_INTERFACE *iface, + int shortcut_index +) { + int res = 0; - // Attempt to load and parse CLI file - load_parse_cli(path); + kprintf("Trying %s\n", device_name); - res = 1; - break; + // Mount device. + FS_RESULT result = fs_mount(iface); + if (result != FS_OK) { + kprintf("Couldn't mount %s: %s\n", device_name, get_fs_result_message(result)); + goto end; } - kprintf("Unmounting %s\n", slot_name); - iface->shutdown(); - iface = NULL; + char volume_label[256]; + fs_get_volume_label(device_name, volume_label); + kprintf("Mounted \"%s\" volume from %s\n", volume_label, device_name); + + // Attempt to load config or shortcut files. + res = (load_config(payload, shortcut_index) || load_shortcut_files(payload, shortcut_index) + ); + + kprintf("Unmounting %s\n", device_name); + fs_unmount(); end: return res; @@ -203,27 +329,18 @@ convert_int(unsigned int in) { #define GC_READY 0x88 #define GC_OK 0x89 +// 0 - Device should not be used. +// 1 - Device should be used. int -load_usb(char slot) { - kprintf("Trying USB Gecko in slot %c\n", slot); - - int channel, res = 1; - - switch (slot) { - case 'B': - channel = 1; - break; +load_usb(BOOT_PAYLOAD *payload, char slot) { + int res = 0; + int channel = slot == 'B' ? 1 : 0; - case 'A': - default: - channel = 0; - break; - } + kprintf("Trying USB Gecko in slot %c\n", slot); if (!usb_isgeckoalive(channel)) { kprintf("Not present\n"); - res = 0; - goto end; + return res; } usb_flush(channel); @@ -241,13 +358,14 @@ load_usb(char slot) { current_time = gettime(); if (diff_sec(start_time, current_time) >= 5) { kprintf("PC did not respond in time\n"); - res = 0; - goto end; + return res; } usb_recvbuffer_safe_ex(channel, &data, 1, 10); // 10 retries } + res = 1; + if (data == PC_READY) { kprintf("Respond with OK\n"); // Sometimes the PC can fail to receive the byte, this helps @@ -261,15 +379,20 @@ load_usb(char slot) { usb_recvbuffer_safe(channel, &size, 4); size = convert_int(size); - dol_alloc(size); - unsigned char *pointer = dol; + if (size <= 0) { + kprintf("DOL is empty\n"); + return res; + } + kprintf("DOL size is %iB\n", size); - if (!dol) { - res = 0; - goto end; + u8 *dol_file = (u8 *) malloc(size); + if (!dol_file) { + kprintf("Couldn't allocate memory for DOL file\n"); + return res; } kprintf("Receiving file...\n"); + unsigned char *pointer = dol_file; while (size > 0xF7D8) { usb_recvbuffer_safe(channel, (void *) pointer, 0xF7D8); size -= 0xF7D8; @@ -279,33 +402,19 @@ load_usb(char slot) { usb_recvbuffer_safe(channel, (void *) pointer, size); } -end: + payload->type = BOOT_TYPE_DOL; + payload->dol_file = dol_file; return res; } -extern u8 __xfb[]; - -void -delay_exit() { - // Wait while the d-pad down direction or reset button is held. - if (all_buttons_held & PAD_BUTTON_DOWN) { - kprintf("(release d-pad down to continue)\n"); - } - if (SYS_ResetButtonDown()) { - kprintf("(release reset button to continue)\n"); - } - - while (all_buttons_held & PAD_BUTTON_DOWN || SYS_ResetButtonDown()) { - VIDEO_WaitVSync(); - PAD_ScanPads(); - all_buttons_held = - (PAD_ButtonsHeld(PAD_CHAN0) | PAD_ButtonsHeld(PAD_CHAN1) - | PAD_ButtonsHeld(PAD_CHAN2) | PAD_ButtonsHeld(PAD_CHAN3)); - } -} - +#ifdef EMU_BUILD +int +main_capture(int _argc, char **_argv) { +#else int -main() { +main(int _argc, char **_argv) { +#endif +#ifndef EMU_BUILD // GCVideo takes a while to boot up. // If VIDEO_GetPreferredMode is called before it's done, // it will not see the "component cable", and default to interlaced mode, @@ -319,11 +428,15 @@ main() { } } } +#endif VIDEO_Init(); PAD_Init(); GXRModeObj *rmode = VIDEO_GetPreferredMode(NULL); VIDEO_Configure(rmode); +#ifdef EMU_BUILD + __xfb = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); +#endif VIDEO_SetNextFramebuffer(__xfb); VIDEO_SetBlack(FALSE); VIDEO_Flush(); @@ -334,6 +447,9 @@ main() { ); kprintf("\n\ngekkoboot %s\n", version); +#ifdef EMU_BUILD + kprintf("EMU_BUILD\n"); +#endif // Disable Qoob u32 val = 6 << 24; @@ -346,106 +462,177 @@ main() { EXI_Sync(EXI_CHANNEL_0); EXI_Deselect(EXI_CHANNEL_0); EXI_Unlock(EXI_CHANNEL_0); + // Since we've disabled the Qoob, we wil reboot to the Nintendo IPL + + // Check argv flags. + int should_ask = false; + for (int i = 0; i < _argc; i++) { + if (strcmp(_argv[i], "--debug") == 0) { + kprintf("DEBUG: Debug enabled by flag.\n"); + debug_enabled = true; + } else if (strcmp(_argv[i], "--ask") == 0) { + should_ask = true; + } + } - PAD_ScanPads(); +#ifdef EMU_BUILD + kprintf("DEBUG: Debug enabled by EMU_BUILD.\n"); + debug_enabled = true; + should_ask = true; +#endif + if (should_ask) { + kprintf("DEBUG: Press button...\n"); + do { + VIDEO_WaitVSync(); + scan_all_buttons_held(); + } while (all_buttons_held == 0 || all_buttons_held == PAD_BUTTON_DOWN); + } else { + scan_all_buttons_held(); + } - all_buttons_held = - (PAD_ButtonsHeld(PAD_CHAN0) | PAD_ButtonsHeld(PAD_CHAN1) - | PAD_ButtonsHeld(PAD_CHAN2) | PAD_ButtonsHeld(PAD_CHAN3)); + // Check if d-pad down direction or reset button is held. + if (all_buttons_held & PAD_BUTTON_DOWN || SYS_ResetButtonDown()) { + kprintf("DEBUG: Debug enabled.\n"); + debug_enabled = true; + } if (all_buttons_held & PAD_BUTTON_LEFT || SYS_ResetButtonDown()) { - // Since we've disabled the Qoob, we wil reboot to the Nintendo IPL - kprintf("Skipped. Rebooting into original IPL...\n"); + kprintf("Skip enabled. Rebooting into original IPL...\n\n"); delay_exit(); return 0; } - char *paths[2]; - int num_paths = 0; + int mram_size = SYS_GetArenaHi() - SYS_GetArenaLo(); + kprintf("Memory available: %iB\n", mram_size); - for (int i = 0; i < num_shortcuts; i++) { + // Detect selected shortcut. + int shortcut_index = 0; + for (int i = 1; i < NUM_SHORTCUTS; i++) { if (all_buttons_held & shortcuts[i].pad_buttons) { - paths[num_paths++] = shortcuts[i].path; + kprintf("->> \"%s\" shortcut selected\n", shortcuts[i].name); + shortcut_index = i; break; } } - - paths[num_paths++] = default_path; - - if (load_usb('B')) { - goto load; + if (shortcut_index == 0) { + kprintf("->> Using default shortcut\n"); } - if (load_fat("sdb", &__io_gcsdb, paths, num_paths)) { - goto load; + // Init payload. + BOOT_PAYLOAD payload; + payload.type = BOOT_TYPE_NONE; + payload.dol_file = NULL; + payload.argv.argc = 0; + payload.argv.length = 0; + payload.argv.commandLine = NULL; + payload.argv.argvMagic = ARGV_MAGIC; + +#ifndef EMU_BUILD + // Attempt to load from each device. + int res = + (load_fat(&payload, "SD Gecko in slot B", &__io_gcsdb, shortcut_index) + || load_fat(&payload, "SD Gecko in slot A", &__io_gcsda, shortcut_index) + || load_fat(&payload, "SD2SP2", &__io_gcsd2, shortcut_index)); + + if (!res || payload.type == BOOT_TYPE_USBGECKO) { + payload.type = BOOT_TYPE_NONE; + res = (load_usb(&payload, 'B') || load_usb(&payload, 'A') || res); } +#else + int res = load_fat(&payload, "Wii SD", &__io_wiisd, shortcut_index); +#endif - if (load_usb('A')) { - goto load; + if (!res) { + // If we reach here, we did not find a device with any shortcut files. + kprintf("\nNo shortcuts found\n"); + kprintf("Press A to reboot into onboard IPL...\n\n"); + wait_for_confirmation(); + return 0; } - if (load_fat("sda", &__io_gcsda, paths, num_paths)) { - goto load; + if (payload.type == BOOT_TYPE_NONE) { + // If we reach here, we found a device with shortcut files but failed to load any + // shortcut. + kprintf("\nUnable to load shortcut\n"); + kprintf("Press A to reboot into onboard IPL...\n\n"); + wait_for_confirmation(); + return 0; } - if (load_fat("sd2", &__io_gcsd2, paths, num_paths)) { - goto load; + if (payload.type == BOOT_TYPE_ONBOARD) { + kprintf("Rebooting into onboard IPL...\n\n"); + delay_exit(); + return 0; } -load: - if (!dol) { - kprintf("No DOL found! Halting."); - while (true) { - VIDEO_WaitVSync(); - } + if (payload.type != BOOT_TYPE_DOL) { + // Should never happen. + kprintf("\n->> !! Internal Error: Unexpected boot type: %i\n", payload.type); + kprintf("Press A to reboot into onboard IPL...\n\n"); + wait_for_confirmation(); + return 0; } - struct __argv dolargs; - dolargs.commandLine = (char *) NULL; - dolargs.length = 0; - - // https://github.com/emukidid/swiss-gc/blob/f5319aab248287c847cb9468325ebcf54c993fb1/cube/swiss/source/aram/sidestep.c#L350 - if (dol_argc) { - dolargs.argvMagic = ARGV_MAGIC; - dolargs.argc = dol_argc; - dolargs.length = 1; - - for (int i = 0; i < dol_argc; i++) { - size_t arg_length = strlen(dol_argv[i]) + 1; - dolargs.length += arg_length; - } - - kprintf("CLI argv size is %iB\n", dolargs.length); - dolargs.commandLine = (char *) malloc(dolargs.length); - - if (!dolargs.commandLine) { - kprintf("Couldn't allocate memory for CLI argv\n"); - dolargs.length = 0; - } else { - unsigned int position = 0; - for (int i = 0; i < dol_argc; i++) { - size_t arg_length = strlen(dol_argv[i]) + 1; - memcpy(dolargs.commandLine + position, dol_argv[i], arg_length); - position += arg_length; + // Print DOL args. + if (debug_enabled) { + if (payload.argv.length > 0) { + kprintf("\nDEBUG: About to print CLI args. Press A to continue...\n"); + wait_for_confirmation(); + kprintf("----------\n"); + size_t position = 0; + for (int i = 0; i < payload.argv.argc; ++i) { + kprintf("arg%i: %s\n", i, payload.argv.commandLine + position); + position += strlen(payload.argv.commandLine + position) + 1; } - dolargs.commandLine[dolargs.length - 1] = '\0'; - DCStoreRange(dolargs.commandLine, dolargs.length); + kprintf("----------\n\n"); + } else { + kprintf("DEBUG: No CLI args\n"); } } + // Prepare DOL argv. + if (payload.argv.length > 0) { + DCStoreRange(payload.argv.commandLine, payload.argv.length); + } + + kprintf("Booting DOL...\n"); + +#ifndef EMU_BUILD + // Load stub. memcpy((void *) STUB_ADDR, stub, (size_t) stub_size); DCStoreRange((void *) STUB_ADDR, (u32) stub_size); delay_exit(); + if (debug_enabled) { + kprintf("DEBUG: Loading DOL...\n"); + } + + // Boot DOL. SYS_ResetSystem(SYS_SHUTDOWN, 0, FALSE); SYS_SwitchFiber( - (intptr_t) dol, + (intptr_t) payload.dol_file, 0, - (intptr_t) dolargs.commandLine, - dolargs.length, + (intptr_t) payload.argv.commandLine, + payload.argv.length, STUB_ADDR, STUB_STACK ); + + // Will never reach here. + return 0; +#else + kprintf("EMU_BUILD: Success!\n"); return 0; +#endif +} + +#ifdef EMU_BUILD +int +main(int _argc, char **_argv) { + int res = main_capture(_argc, _argv); + kprintf("EMU_BUILD: Exited with %i. Press A to exit...\n", res); + wait_for_confirmation(); + return res; } +#endif diff --git a/source/shortcut.c b/source/shortcut.c new file mode 100644 index 0000000..9d1b35c --- /dev/null +++ b/source/shortcut.c @@ -0,0 +1,14 @@ +#include "shortcut.h" +#include + +SHORTCUT shortcuts[NUM_SHORTCUTS] = { + {"default", 0, "DEFAULT", "DEFAULT_ARG", "/ipl.dol"}, + {"A", PAD_BUTTON_A, "A", "A_ARG", "/a.dol"}, + {"B", PAD_BUTTON_B, "B", "B_ARG", "/b.dol"}, + {"X", PAD_BUTTON_X, "X", "X_ARG", "/x.dol"}, + {"Y", PAD_BUTTON_Y, "Y", "Y_ARG", "/y.dol"}, + {"Z", PAD_TRIGGER_Z, "Z", "Z_ARG", "/z.dol"}, + {"START", PAD_BUTTON_START, "START", "START_ARG", "/start.dol"}, + // NOTE: Shouldn't use L, R or Joysticks as analog inputs are calibrated on boot. + // Should also avoid D-Pad as it is used for special functionality. +}; diff --git a/source/shortcut.h b/source/shortcut.h new file mode 100644 index 0000000..f6f03b0 --- /dev/null +++ b/source/shortcut.h @@ -0,0 +1,17 @@ +#ifndef INC_SHORTCUT_H +#define INC_SHORTCUT_H +#include + +#define NUM_SHORTCUTS 7 + +typedef struct { + const char *const name; + const u16 pad_buttons; + const char *const config_name; + const char *const config_cli_name; + const char *const path; +} SHORTCUT; + +extern SHORTCUT shortcuts[NUM_SHORTCUTS]; + +#endif diff --git a/source/types.c b/source/types.c new file mode 100644 index 0000000..8e7ca93 --- /dev/null +++ b/source/types.c @@ -0,0 +1,17 @@ +#include "types.h" + +#define NUM_BOOT_TYPE_NAMES 4 +const char *boot_type_names[NUM_BOOT_TYPE_NAMES] = { + /*BOOT_TYPE_NONE (0)*/ "NONE", + /*BOOT_TYPE_DOL (1)*/ "DOL", + /*BOOT_TYPE_ONBOARD (2)*/ "ONBOARD", + /*BOOT_TYPE_USBGECKO (3)*/ "USBGECKO", +}; + +const char * +get_boot_type_name(BOOT_TYPE type) { + if (type < 0 || type >= NUM_BOOT_TYPE_NAMES) { + return "Unknown"; + } + return boot_type_names[type]; +} diff --git a/source/types.h b/source/types.h new file mode 100644 index 0000000..89a6e6f --- /dev/null +++ b/source/types.h @@ -0,0 +1,29 @@ +#ifndef INC_TYPES_H +#define INC_TYPES_H +#include + +typedef enum { + BOOT_TYPE_NONE = 0, + BOOT_TYPE_DOL, + BOOT_TYPE_ONBOARD, + BOOT_TYPE_USBGECKO, + // Changes to this enum should also be made to boot_type_names in types.c +} BOOT_TYPE; + +const char * +get_boot_type_name(BOOT_TYPE type); + +typedef struct { + BOOT_TYPE type; + const char *dol_path; + const char **dol_cli_options_strs; + int num_dol_cli_options_strs; +} BOOT_ACTION; + +typedef struct { + BOOT_TYPE type; + u8 *dol_file; + struct __argv argv; +} BOOT_PAYLOAD; + +#endif diff --git a/source/utils.c b/source/utils.c deleted file mode 100644 index c52e4c7..0000000 --- a/source/utils.c +++ /dev/null @@ -1,36 +0,0 @@ -#include "fatfs/ff.h" - -// See ./fatfs/ff.h:276 -char *fresult_msgs[] = { - /*FR_OK ( 0)*/ "Succeeded", - /*FR_DISK_ERR ( 1)*/ "A hard error occurred in the low level disk I/O layer", - /*FR_INT_ERR ( 2)*/ "Assertion failed", - /*FR_NOT_READY ( 3)*/ "Device not ready", - /*FR_NO_FILE ( 4)*/ "Could not find the file", - /*FR_NO_PATH ( 5)*/ "Could not find the path", - /*FR_INVALID_NAME ( 6)*/ "The path name format is invalid", - /*FR_DENIED ( 7)*/ "Access denied due to prohibited access or directory full", - /*FR_EXIST ( 8)*/ "Access denied due to prohibited access", - /*FR_INVALID_OBJECT ( 9)*/ "The file/directory object is invalid", - /*FR_WRITE_PROTECTED (10)*/ "The physical drive is write protected", - /*FR_INVALID_DRIVE (11)*/ "The logical drive number is invalid", - /*FR_NOT_ENABLED (12)*/ "The volume has no work area", - /*FR_NO_FILESYSTEM (13)*/ "There is no valid FAT volume", - /*FR_MKFS_ABORTED (14)*/ "The f_mkfs() aborted due to any problem", - /*FR_TIMEOUT (15)*/ - "Could not get a grant to access the volume within defined period", - /*FR_LOCKED (16)*/ - "The operation is rejected according to the file sharing policy", - /*FR_NOT_ENOUGH_CORE (17)*/ "LFN working buffer could not be allocated", - /*FR_TOO_MANY_OPEN_FILES (18)*/ "Number of open files > FF_FS_LOCK", - /*FR_INVALID_PARAMETER (19)*/ "Given parameter is invalid", -}; -int num_fresult_msgs = sizeof(fresult_msgs) / sizeof(fresult_msgs[0]); - -char * -get_fresult_message(FRESULT result) { - if (result < 0 || result >= num_fresult_msgs) { - return "Unknown"; - } - return fresult_msgs[result]; -} diff --git a/source/utils.h b/source/utils.h deleted file mode 100644 index 9fda394..0000000 --- a/source/utils.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef INC_UTILS_H -#define INC_UTILS_H - -#include "fatfs/ff.h" - -extern char * -get_fresult_message(FRESULT result); - -#endif diff --git a/stub/meson.build b/stub/meson.build index a6b6abd..e0de71f 100644 --- a/stub/meson.build +++ b/stub/meson.build @@ -16,7 +16,7 @@ stub_exe = executable( link_depends: stub_linker_script, dependencies: [ # For the headers - libogc_deps['ogc'].partial_dependency( + libogc_deps_dic['ogc'].partial_dependency( compile_args: true, sources: true, ), diff --git a/subprojects/libfat.wrap b/subprojects/libfat.wrap new file mode 100644 index 0000000..bcfe006 --- /dev/null +++ b/subprojects/libfat.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url = https://github.com/extremscorner/libfat.git +# TODO: redolution/libogc2 is behind extremscorner/libogc2 which introduced a breaking change +# which libfat relies on. Namely, the introduction of SYS_IsDMAAddress: +# https://github.com/devkitPro/libfat/commit/6e782e688f89e05b0f5750e0c5eeef1dda492a37 +revision = 5700bbf2945cbb7ceda0bac9516a32cb7cc6e474 +patch_directory=libfat \ No newline at end of file diff --git a/subprojects/packagefiles/libfat/configure b/subprojects/packagefiles/libfat/configure new file mode 100755 index 0000000..dea870f --- /dev/null +++ b/subprojects/packagefiles/libfat/configure @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +src=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") + +if [[ "$(readlink -f "$PWD")" == "$src" ]]; then + printf '%s\n' 'This script must be run out of tree!' + exit 1 +fi + +tmp=$(mktemp -t Makefile.XXXXXXXXXX) +trap 'rm -f "$tmp"' EXIT + +function write() { + printf '%s\n' "$*" >> "$tmp" +} + +write '# Automatically generated file' + +for arg in "$@"; do + val="${arg#*=}" + case $arg in + --prefix=*) + write INSTALL_PREFIX := "$val" + ;; + --incdir=*) + write INCDEST := "$val" + ;; + --libdir=*) + write LIBDEST := "$val" + ;; + *) + printf '%s\n' "$arg: Invalid argument" + exit 1 + esac +done + +write SRCDIR := "$src" +write 'default:' +write ' $(MAKE) -C $(SRCDIR) wii-release' +write 'install:' +write ' @mkdir -p $(DESTDIR)$(INSTALL_PREFIX)' +write ' @mkdir -p $(DESTDIR)$(INSTALL_PREFIX)/$(INCDEST)' +write ' @mkdir -p $(DESTDIR)$(INSTALL_PREFIX)/$(LIBDEST)' +write ' @cp -frv $(SRCDIR)/include/* -t $(DESTDIR)$(INSTALL_PREFIX)/$(INCDEST)' +write ' @cp -frv $(SRCDIR)/libogc2/lib/* -t $(DESTDIR)$(INSTALL_PREFIX)/$(LIBDEST)' +write ' @cp -frv $(SRCDIR)/libfat_license.txt $(DESTDIR)$(INSTALL_PREFIX)' + +cp "$tmp" Makefile diff --git a/subprojects/packagefiles/libfat/meson.build b/subprojects/packagefiles/libfat/meson.build new file mode 100644 index 0000000..ace2e70 --- /dev/null +++ b/subprojects/packagefiles/libfat/meson.build @@ -0,0 +1,19 @@ +project( + 'libfat', + ['c'], + meson_version: '>=1.1', +) + +external_project = import('unstable-external_project') + +p = external_project.add_project( + 'configure', + configure_options: [ + '--prefix=@PREFIX@', + '--libdir=@LIBDIR@', + '--incdir=@INCLUDEDIR@', + ], + cross_configure_options: [], +) + +dep = p.dependency(':wii/libfat.a')