diff --git a/Makefile b/Makefile index 8b86fbd2..ca21342c 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ SOURCES := source \ source/Channels \ source/BoxCover \ source/GameCube \ + source/riivolution \ source/cheats \ source/homebrewboot \ source/themes \ diff --git a/source/prompts/GameWindow.cpp b/source/prompts/GameWindow.cpp index ade76e96..88bc5be9 100644 --- a/source/prompts/GameWindow.cpp +++ b/source/prompts/GameWindow.cpp @@ -840,12 +840,46 @@ void GameWindow::BootGame(struct discHdr *header) } } + /* Standalone Friivolution route: Friivolution owns mod preparation and + * launching; GX only hands over the game/mod selection. Normal launches + * below are untouched when the per-game toggle is off. */ + if ((header->type == TYPE_GAME_WII_IMG || header->type == TYPE_GAME_WII_DISC) && + game_cfg->FriivMode == ON) + { + if (gameNandEmuMode != EMUNAND_OFF) + { + ShowError(tr("EmuNAND saves do not apply under Friivolution. Turn EmuNAND off for this game first.")); + return; + } + if (game_cfg->loadalternatedol != ALT_DOL_DEFAULT) + { + if (!WindowPrompt(tr("Friivolution:"), tr("Alternate DOL selection does not apply under Friivolution. Continue?"), tr("Continue"), tr("Cancel"))) + return; + } + if (ocarinaChoice) + { + if (!WindowPrompt(tr("Friivolution:"), tr("GX cheat codes do not apply under Friivolution. Continue without them?"), tr("Continue"), tr("Cancel"))) + return; + } + GameStatistics.SetPlayCount(header->id, GameStatistics.GetPlayCount(header->id)+1); + GameStatistics.Save(); + + int friivRet = GameBooter::BootFriivolution(header); + + if (friivRet == -1) + { + GameStatistics.SetPlayCount(header->id, GameStatistics.GetPlayCount(header->id)-1); + GameStatistics.Save(); + } + return; + } + GameStatistics.SetPlayCount(header->id, GameStatistics.GetPlayCount(header->id)+1); GameStatistics.Save(); // Just calling that shuts down everything and starts game int ret = GameBooter::BootGame(header, ocarinaChoice); - + // If the launch is canceled, reduce playCount if(ret == -1) { diff --git a/source/riivolution/FriivAdapter.cpp b/source/riivolution/FriivAdapter.cpp new file mode 100644 index 00000000..72543a5d --- /dev/null +++ b/source/riivolution/FriivAdapter.cpp @@ -0,0 +1,268 @@ +/**************************************************************************** + * Friivolution launch adapter for USB Loader GX. See FriivAdapter.h. + * Core only: no GX/Wii headers, host-testable. + ****************************************************************************/ +#include +#include +#include +#include +#include "FriivAdapter.h" + +static bool FriivHasNul(const char *s, size_t max) +{ + for (size_t i = 0; i < max; i++) + { + if (!s[i]) + return true; + } + return false; +} + +static bool FriivHasDotDot(const char *path) +{ + for (const char *p = path; *p; p++) + { + if (p[0] == '.' && p[1] == '.' && + (p == path || p[-1] == '/') && + (p[2] == '/' || p[2] == 0)) + return true; + } + return false; +} + +static bool FriivEndsWithCI(const char *s, const char *suffix) +{ + size_t sl = strlen(s), xl = strlen(suffix); + if (sl < xl) + return false; + const char *tail = s + sl - xl; + for (size_t i = 0; i < xl; i++) + { + if (tolower((unsigned char)tail[i]) != suffix[i]) + return false; + } + return true; +} + +/* True for companion parts that must never be used as the entry point. */ +static bool FriivIsCompanionPart(const char *rel) +{ + /* ".wbf1".."wbf9" (GX split.c names parts by patching the last char). */ + size_t n = strlen(rel); + if (n >= 5 && rel[n - 5] == '.' && + (rel[n - 4] == 'w' || rel[n - 4] == 'W') && + (rel[n - 3] == 'b' || rel[n - 3] == 'B') && + (rel[n - 2] == 'f' || rel[n - 2] == 'F') && + rel[n - 1] >= '1' && rel[n - 1] <= '9') + return true; + /* ".iso1".."iso9". */ + if (FriivEndsWithCI(rel, ".iso1") || FriivEndsWithCI(rel, ".iso2") || + FriivEndsWithCI(rel, ".iso3") || FriivEndsWithCI(rel, ".iso4") || + FriivEndsWithCI(rel, ".iso5") || FriivEndsWithCI(rel, ".iso6") || + FriivEndsWithCI(rel, ".iso7") || FriivEndsWithCI(rel, ".iso8") || + FriivEndsWithCI(rel, ".iso9")) + return true; + /* ".partN.iso" with N > 0. */ + if (n >= 11 && rel[n - 4] == '.' && + (rel[n - 3] == 'i' || rel[n - 3] == 'I')) + { + const char *dot = NULL; + for (const char *p = rel + n - 5; p > rel; p--) + { + if (*p == '.') + { + dot = p; + break; + } + if (*p == '/') + break; + } + if (dot && !strncmp(dot, ".part", 5) && dot[5] >= '1' && dot[5] <= '9') + return true; + } + return false; +} + +FriivBuildResult FriivTranslateGamePath(const char *fatPath, bool isDisc, + char *relOut, size_t relLen, bool *usbOut) +{ + if (!relOut || !relLen || !usbOut) + return FRIIV_ERR_ARGS; + relOut[0] = 0; + *usbOut = false; + + if (isDisc) + { + if (fatPath && fatPath[0]) + return FRIIV_ERR_ARGS; + return FRIIV_OK; /* empty path = disc in the drive */ + } + + if (!fatPath || !fatPath[0]) + return FRIIV_ERR_NO_PATH; + + /* Split "usb1:/wbfs/..." into device prefix and device-relative path. */ + const char *colon = strchr(fatPath, ':'); + if (!colon || colon == fatPath) + return FRIIV_ERR_BAD_PATH; + size_t devLen = (size_t)(colon - fatPath); + const char *rel = colon + 1; + + bool usb; + if (devLen == 2 && !strncmp(fatPath, "sd", 2)) + usb = false; + else if (devLen == 4 && !strncmp(fatPath, "usb1", 4)) + usb = true; + else + return FRIIV_ERR_BAD_DEVICE; + + if (!rel[0] || rel[0] != '/') + return FRIIV_ERR_BAD_PATH; + if (strlen(rel) >= FRIIV_CFG_PATH_MAX) + return FRIIV_ERR_PATH_TOO_LONG; + if (strchr(rel, '\\') || FriivHasDotDot(rel)) + return FRIIV_ERR_BAD_PATH; + if (FriivIsCompanionPart(rel)) + return FRIIV_ERR_BAD_PATH; + if (!FriivEndsWithCI(rel, ".wbfs") && !FriivEndsWithCI(rel, ".iso") && + !FriivEndsWithCI(rel, ".part0.iso")) + return FRIIV_ERR_BAD_PATH; + + if (strlen(rel) + 1 > relLen) + return FRIIV_ERR_PATH_TOO_LONG; + strcpy(relOut, rel); + *usbOut = usb; + return FRIIV_OK; +} + +FriivBuildResult FriivBuildConfig(const char *relPath, bool usb, + const void *gameId4, const char *modXml, + FRIIV_CFG *out) +{ + if (!relPath || !out) + return FRIIV_ERR_ARGS; + if (!modXml) + modXml = ""; + + if (relPath[0]) + { + if (relPath[0] != '/' || !FriivHasNul(relPath, FRIIV_CFG_PATH_MAX)) + return FRIIV_ERR_BAD_PATH; + if (strlen(relPath) >= FRIIV_CFG_PATH_MAX) + return FRIIV_ERR_PATH_TOO_LONG; + if (strchr(relPath, '\\') || FriivHasDotDot(relPath)) + return FRIIV_ERR_BAD_PATH; + } + if (modXml[0]) + { + if (!FriivHasNul(modXml, FRIIV_CFG_XML_MAX)) + return FRIIV_ERR_BAD_MOD; + if (strlen(modXml) >= FRIIV_CFG_XML_MAX) + return FRIIV_ERR_BAD_MOD; + if (strchr(modXml, '/') || strchr(modXml, '\\') || + !strcmp(modXml, ".") || !strcmp(modXml, "..")) + return FRIIV_ERR_BAD_MOD; + } + + memset(out, 0, sizeof(*out)); + out->Magic = FRIIV_CFG_MAGIC; + out->Version = FRIIV_CFG_VERSION; + out->Flags = FRIIV_CFG_AUTO_BOOT | (usb ? FRIIV_CFG_GAME_USB : 0); + if (gameId4) + memcpy(&out->GameID, gameId4, 4); + strcpy(out->GamePath, relPath); + strcpy(out->ModXml, modXml); + /* Reserved stays zeroed. */ + return FRIIV_OK; +} + +int FriivScanModDir(const char *dirPath, char list[][FRIIV_MOD_NAME_MAX], + int maxList, int *count) +{ + if (!dirPath || !list || maxList <= 0 || !count) + return -1; + DIR *d = opendir(dirPath); + if (!d) + return -1; + + int added = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL) + { + const char *name = ent->d_name; + if (!strcmp(name, ".") || !strcmp(name, "..")) + continue; + if (!FriivEndsWithCI(name, ".xml")) + continue; + if (strlen(name) >= FRIIV_MOD_NAME_MAX) + continue; + bool dup = false; + for (int i = 0; i < *count; i++) + { + if (!strcmp(list[i], name)) + { + dup = true; + break; + } + } + if (dup || *count >= maxList) + continue; + /* Sorted insert for a stable chooser. */ + int pos = *count; + while (pos > 0 && strcmp(list[pos - 1], name) > 0) + { + strcpy(list[pos], list[pos - 1]); + pos--; + } + strcpy(list[pos], name); + (*count)++; + added++; + } + closedir(d); + return added; +} + +int FriivCollectModNames(char list[][FRIIV_MOD_NAME_MAX], int maxList) +{ + if (!list || maxList <= 0) + return 0; + int count = 0; + char dir[32]; + const char *devs[] = { "sd", "usb1", "usb2", "usb3", "usb4", + "usb5", "usb6", "usb7", "usb8" }; + for (size_t i = 0; i < sizeof(devs) / sizeof(devs[0]); i++) + { + snprintf(dir, sizeof(dir), "%s:/riivolution", devs[i]); + FriivScanModDir(dir, list, maxList, &count); + if (count >= maxList) + break; + } + return count; +} + +const char *FriivResultText(FriivBuildResult r) +{ + switch (r) + { + case FRIIV_OK: + return "OK"; + case FRIIV_ERR_ARGS: + return "Internal error building the Friivolution request."; + case FRIIV_ERR_TYPE: + return "Only Wii backups and Wii discs can launch via Friivolution."; + case FRIIV_ERR_NO_PATH: + return "This game has no file path (WBFS partitions are not supported; use FAT32)."; + case FRIIV_ERR_BAD_PATH: + return "The game path cannot be handed to Friivolution (must be a first-part .wbfs/.iso on SD or USB)."; + case FRIIV_ERR_PATH_TOO_LONG: + return "The game path is too long for the Friivolution request."; + case FRIIV_ERR_CISO: + return "Compressed ISO (.ciso) is not supported by Friivolution; use plain ISO or WBFS."; + case FRIIV_ERR_BAD_DEVICE: + return "The game must be on the SD card or the first USB device for Friivolution."; + case FRIIV_ERR_BAD_MOD: + return "The selected mod filename is invalid (bare \"name.xml\", max 63 chars)."; + default: + return "Unknown Friivolution error."; + } +} diff --git a/source/riivolution/FriivAdapter.h b/source/riivolution/FriivAdapter.h new file mode 100644 index 00000000..ca7e528b --- /dev/null +++ b/source/riivolution/FriivAdapter.h @@ -0,0 +1,97 @@ +/**************************************************************************** + * Friivolution launch adapter for USB Loader GX. + * + * This module translates a GX-selected Wii backup into the standalone + * Friivolution boot contract (FRIIV_CFG, see FriivConfig.h) and hands it + * over with the same binary-argument mechanism GX uses for Nintendont + * (AddBootArgument + BootHomebrewFromMem). + * + * Deliberately OUT of scope here: Riivolution XML parsing, patch planning, + * option editing, table rebuilds, runtime serving. Friivolution owns all of + * that; GX only supplies the game/mod selection and launches the app. + * + * The core (Translate/Build/ResultText/ScanModDir) uses no GX or Wii + * headers so it can be unit-tested on the host. Thin GX glue + * (settings, prompts, homebrew launch) lives in GameBooter::BootFriivolution. + ****************************************************************************/ +#ifndef FRIIV_ADAPTER_H_ +#define FRIIV_ADAPTER_H_ + +#include +#include "FriivConfig.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Maximum mod filenames collected for the in-GX chooser. */ +#define FRIIV_MOD_MAX 64 +/* Matches FRIIV_CFG_XML_MAX; bare "name.xml", no directory. */ +#define FRIIV_MOD_NAME_MAX 64 + +typedef enum FriivBuildResult +{ + FRIIV_OK = 0, + FRIIV_ERR_ARGS, /* null pointer / zero length passed in */ + FRIIV_ERR_TYPE, /* not a Wii backup or disc GX can hand over */ + FRIIV_ERR_NO_PATH, /* backup has no FAT path (e.g. WBFS partition) */ + FRIIV_ERR_BAD_PATH, /* path breaks contract rules (relative, escape, ...) */ + FRIIV_ERR_PATH_TOO_LONG, + FRIIV_ERR_CISO, /* compressed ISO: Friivolution reads plain ISO/WBFS */ + FRIIV_ERR_BAD_DEVICE,/* not SD or the first USB device */ + FRIIV_ERR_BAD_MOD /* mod name breaks contract rules */ +} FriivBuildResult; + +/* + * Translate a GX FAT path ("usb1:/wbfs/.../SB4E01.wbfs", "sd:/...") into the + * device-relative representation Friivolution documents ("/wbfs/..."). + * + * isDisc selects the physical disc in the drive instead; fatPath must then + * be NULL or empty and relOut comes back empty with usbOut false. + * + * Split images: pass the first part (.wbfs / .iso / .part0.iso), which is + * what GX lists. A non-first part (.wbf1, .iso1, .part1.iso, ...) is refused + * rather than booting the wrong data. + * + * Only SD and the first USB device are accepted. v1 names no partition, so + * anything else is refused with FRIIV_ERR_BAD_DEVICE instead of being + * silently resolved to the wrong volume. + */ +FriivBuildResult FriivTranslateGamePath(const char *fatPath, bool isDisc, + char *relOut, size_t relLen, bool *usbOut); + +/* + * Fill a FRIIV_CFG for handoff. relPath is "" (disc) or a translated "/..." + * path; usb sets FRIIV_CFG_GAME_USB. gameId4 is the 4-byte disc ID prefix or + * NULL for no game check. modXml is "" (every matching mod, as by hand) or a + * bare filename limited to one XML in /riivolution/. + */ +FriivBuildResult FriivBuildConfig(const char *relPath, bool usb, + const void *gameId4, const char *modXml, + FRIIV_CFG *out); + +/* + * Collect bare "*.xml" basenames from one ":/riivolution" directory + * into list (deduplicated, sorted, NUL-terminated). Returns entries added + * from this directory, or -1 when the directory cannot be opened (not an + * error: the device may simply be absent). *count is updated. + */ +int FriivScanModDir(const char *dirPath, char list[][FRIIV_MOD_NAME_MAX], + int maxList, int *count); + +/* + * Collect every mod XML basename from the /riivolution folders GX can see + * (SD plus all USB devices), for the in-GX mod chooser. No XML is parsed + * here; Friivolution owns matching names to games. Returns total entries. + */ +int FriivCollectModNames(char list[][FRIIV_MOD_NAME_MAX], int maxList); + +/* Short human-readable reason for a result, for prompts and logs. */ +const char *FriivResultText(FriivBuildResult r); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/source/riivolution/FriivConfig.h b/source/riivolution/FriivConfig.h new file mode 100644 index 00000000..9fdff7dd --- /dev/null +++ b/source/riivolution/FriivConfig.h @@ -0,0 +1,92 @@ +/* Vendored from Friivolution for USB Loader GX. + * + * Upstream: https://git.gay/december/friivolution + * File: launcher/include/FriivConfig.h at 69c8216d2f170e18d0855c49bc36294ebfe157b8 + * License: same as Friivolution (see below and upstream COPYING/README). + * The upstream header says: "Copy this file into your loader. It has no + * dependencies." Only this provenance banner was added; the contract below + * is byte-identical to upstream. If upstream bumps FRIIV_CFG_VERSION, this + * copy must be re-vendored and the adapter's static asserts will fail the + * build until the new layout is handled. + */ + +/* + * FriivConfig.h - boot configuration handed to Friivolution by another loader. + * + * Copy this file into your loader. It has no dependencies. + * + * Friivolution can be told to boot a game without showing its own menus, + * the same way Nintendont does (see Nintendont's CommonConfig.h and USB Loader + * GX's GameBooter.cpp / NIN_Config.h). Delivery is via arguments: pass the + * raw FRIIV_CFG bytes as the second argument when launching Friivolution's + * boot.dol/boot.elf. argv[0] is the path of the dol. The struct can contain + * NUL bytes. + * + * Detecting support: read from the meta.xml next to boot.elf; + * 1.01 and later understand this argument. You can basically just check + * if it's not 1.00, as that's the only build without this. Older builds + * (and Riivolution itself) ignore arguments and simply show their own + * menus. + * + * While starting up, Friivolution shows "Autoboot: hold B" and drops to its + * normal picker if B is held on any controller during that time. + * + * Mod options: Friivolution loads every XML in /riivolution/ whose game ID + * matches, and each option's choice comes from /riivolution/config/XXXX.xml + * (XXXX = first 4 characters of the game ID), which Friivolution's own menu + * writes. A loader that wants to set choices itself should write that file, + * in Riivolution's format: + * + * + * + * To limit a boot to a single mod, name its XML in ModXml instead. + */ +#ifndef __FRIIV_CONFIG_H__ +#define __FRIIV_CONFIG_H__ + +#define FRIIV_CFG_MAGIC 0x46524956u /* 'FRIV' */ +#define FRIIV_CFG_VERSION 1u +#define FRIIV_CFG_PATH_MAX 255 +#define FRIIV_CFG_XML_MAX 64 + +typedef struct FRIIV_CFG { + unsigned int Magic; /* FRIIV_CFG_MAGIC */ + unsigned int Version; /* FRIIV_CFG_VERSION */ + unsigned int Flags; /* FRIIV_CFG_* bits below */ + unsigned int GameID; /* first 4 bytes of the disc header ('SB4E'); 0 = don't check */ + + /* Path of the image below the device root, e.g. + * "/wbfs/Super Mario Galaxy 2 [SB4E01]/SB4E01.wbfs" + * "/riivolution/backups/game.iso" + * For split images give the first part (.wbfs / .iso / .part0.iso). + * Empty = use the disc in the drive. + * Device: SD, or USB when FRIIV_CFG_GAME_USB is set. */ + char GamePath[FRIIV_CFG_PATH_MAX]; + + /* Optional. File name (no directory) of one mod XML in /riivolution/, + * e.g. "smg2-mod.xml". Only that mod is loaded for this boot; its + * option choices still come from /riivolution/config/XXXX.xml. + * Empty = every mod XML for the game, as when started by hand. */ + char ModXml[FRIIV_CFG_XML_MAX]; + + unsigned char Reserved[49]; /* zero; pads the struct to 384 bytes */ +} FRIIV_CFG; + +enum friivconfigbits +{ + /* Skip the menus and boot GamePath. Without this bit the argument is + * ignored, exactly like Nintendont. You may think this is useless, + * considering you have to explicitly pass a second argument. + * ...yeah, probably. Better safe than sorry, though. */ + FRIIV_CFG_AUTO_BOOT = (1u << 0), + /* GamePath is on the USB drive instead of the SD card. */ + FRIIV_CFG_GAME_USB = (1u << 1), + /* Boot the image with no Riivolution patches at all (vanilla). Mod XMLs + * are not read; ModXml is ignored. This is also probably useless LOL + * but it might be good if you are making your own loader and want to rely + * on us I guess? */ + FRIIV_CFG_NO_PATCHES = (1u << 2) +}; + +#endif diff --git a/source/riivolution/hosttest/run.sh b/source/riivolution/hosttest/run.sh new file mode 100644 index 00000000..e2049cff --- /dev/null +++ b/source/riivolution/hosttest/run.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# GX -> Friivolution contract test. Runs on the host with the normal C++ +# compiler; no Wii toolchain needed. +# +# Usage: sh run.sh [FRIIV_SRC] +# FRIIV_SRC defaults to ../../../../friivolution (this workspace layout). +# When the Friivolution tree is absent the consumer half is skipped and the +# script reports that platform boundary instead of failing. +set -eu +cd "$(dirname "$0")" +FRIIV_SRC="${1:-../../../../friivolution}" + +HAVE_CONSUMER=0 +if [ -f "$FRIIV_SRC/launcher/source/bootcfg.cpp" ] && \ + [ -f "$FRIIV_SRC/launcher/include/FriivConfig.h" ] && \ + [ -f "$FRIIV_SRC/launcher/include/bootcfg.h" ]; then + HAVE_CONSUMER=1 + mkdir -p .tmp-contract + sed -n '/^\/\/ --- bootcfg (host-tested/,/^\/\/ --- end bootcfg/p' \ + "$FRIIV_SRC/launcher/source/bootcfg.cpp" | sed '$d' > .tmp-contract/bootcfg-body.inc + mkdir -p .tmp-contract/launcher-include + cp "$FRIIV_SRC/launcher/include/FriivConfig.h" .tmp-contract/launcher-include/ + cp "$FRIIV_SRC/launcher/include/bootcfg.h" .tmp-contract/launcher-include/ +fi + +FLAGS="-std=gnu++98 -g -O1 -fno-strict-aliasing -Wall -Wextra" +if [ "$HAVE_CONSUMER" = 1 ]; then + FLAGS="$FLAGS -DHAVE_FRIIV_CONSUMER -I.tmp-contract -I.tmp-contract/launcher-include" + echo "contract: testing adapter builder against real Friivolution consumer ($FRIIV_SRC)" +else + echo "contract: Friivolution tree not found at $FRIIV_SRC; adapter-only checks (consumer boundary NOT exercised)" +fi + +g++ $FLAGS test_friiv_contract.cpp ../FriivAdapter.cpp -o /tmp/friiv-contract +/tmp/friiv-contract +rm -rf .tmp-contract diff --git a/source/riivolution/hosttest/test_friiv_contract.cpp b/source/riivolution/hosttest/test_friiv_contract.cpp new file mode 100644 index 00000000..b45e594b --- /dev/null +++ b/source/riivolution/hosttest/test_friiv_contract.cpp @@ -0,0 +1,115 @@ +// GX -> Friivolution contract test: the REAL adapter builder against the +// REAL Friivolution consumer (bootcfg-body.inc, extracted from the +// Friivolution tree the same way its own run-usb2.sh does). No independent +// models: adapter output bytes go straight into BootCfg_ValidateSized. +// +// Build & run: sh run.sh [FRIIV_SRC] +// FRIIV_SRC = Friivolution checkout (default: ../../../../friivolution). +// Without it only adapter-local structural checks run and the script +// reports the platform boundary instead of failing. +#include +#include +#include +#include "../FriivAdapter.h" + +#ifdef HAVE_FRIIV_CONSUMER +#include "../launcher-include/FriivConfig.h" +#include "../launcher-include/bootcfg.h" +#include "bootcfg-body.inc" + +static void ExpectAccepted(const FRIIV_CFG *built) +{ + FRIIV_CFG out; + // Exact-size and oversized reads accept; short reads refuse. + assert(BootCfg_ValidateSized(built, sizeof(*built), &out)); + assert(BootCfg_ValidateSized(built, sizeof(*built) + 16, &out)); + assert(!BootCfg_ValidateSized(built, sizeof(*built) - 1, &out)); + assert(!BootCfg_ValidateSized(built, 0, &out)); + assert(BootCfg_Validate(built, &out)); + assert(out.Magic == FRIIV_CFG_MAGIC && out.Version == FRIIV_CFG_VERSION); +} +#else +struct FRIIV_CFG; +static void ExpectAccepted(const FRIIV_CFG *) {} +#endif + +int main() +{ + // Both sides compile against a 384-byte v1 layout. + assert(sizeof(FRIIV_CFG) == 384); + + char rel[512]; + bool usb = false; + FRIIV_CFG cfg; + + // Normal SD WBFS backup. + assert(FriivTranslateGamePath("sd:/wbfs/SMG2 [SB4E01]/SB4E01.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_OK); + assert(!usb && !strcmp(rel, "/wbfs/SMG2 [SB4E01]/SB4E01.wbfs")); + { + char id[4]; memcpy(id, "SB4E", 4); + assert(FriivBuildConfig(rel, usb, id, "", &cfg) == FRIIV_OK); + assert(cfg.Flags == FRIIV_CFG_AUTO_BOOT); + ExpectAccepted(&cfg); + } + + // USB backup sets the USB source bit; spaces and brackets survive. + assert(FriivTranslateGamePath("usb1:/riivolution/backups/game.iso", false, rel, sizeof(rel), &usb) == FRIIV_OK); + assert(usb && !strcmp(rel, "/riivolution/backups/game.iso")); + assert(FriivBuildConfig(rel, usb, NULL, "smg2-mod.xml", &cfg) == FRIIV_OK); + assert(cfg.Flags == (FRIIV_CFG_AUTO_BOOT | FRIIV_CFG_GAME_USB)); + assert(cfg.GameID == 0 && !strcmp(cfg.ModXml, "smg2-mod.xml")); + ExpectAccepted(&cfg); + + // Physical disc: empty path, no USB bit. + assert(FriivTranslateGamePath(NULL, true, rel, sizeof(rel), &usb) == FRIIV_OK); + assert(!usb && !rel[0]); + assert(FriivBuildConfig(rel, usb, NULL, "", &cfg) == FRIIV_OK); + ExpectAccepted(&cfg); + + // Split first parts pass; companion parts are refused, never misbooted. + assert(FriivTranslateGamePath("usb1:/wbfs/g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_OK); + assert(FriivTranslateGamePath("usb1:/wbfs/g.wbf1", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("usb1:/wbfs/g.wbf2", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("sd:/wbfs/g.iso", false, rel, sizeof(rel), &usb) == FRIIV_OK); + assert(FriivTranslateGamePath("sd:/wbfs/g.iso1", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("sd:/wbfs/g.part0.iso", false, rel, sizeof(rel), &usb) == FRIIV_OK); + assert(FriivTranslateGamePath("sd:/wbfs/g.part1.iso", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("sd:/wbfs/g.ciso", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + + // Devices: SD and first USB only; everything else is an explicit refusal. + assert(FriivTranslateGamePath("usb2:/wbfs/g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_DEVICE); + assert(FriivTranslateGamePath("usb8:/wbfs/g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_DEVICE); + assert(FriivTranslateGamePath("/wbfs/g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("wbfs/g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath(NULL, false, rel, sizeof(rel), &usb) == FRIIV_ERR_NO_PATH); + + // Malformed and hostile paths are refused, never truncated into boot. + assert(FriivTranslateGamePath("usb1:wbfs/g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("usb1:/wbfs/../g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("usb1:/..", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + assert(FriivTranslateGamePath("usb1:/wbfs\\g.wbfs", false, rel, sizeof(rel), &usb) == FRIIV_ERR_BAD_PATH); + { + char longp[600]; + strcpy(longp, "usb1:/wbfs/"); + memset(longp + 11, 'a', sizeof(longp) - 20); + strcpy(longp + sizeof(longp) - 9, ".wbfs"); + assert(FriivTranslateGamePath(longp, false, rel, sizeof(rel), &usb) == FRIIV_ERR_PATH_TOO_LONG); + } + + // Mod names: bare filenames only (incl. a real fixture name; matching on + // target is case-insensitive in ParseXMLs, the bytes pass through here). + assert(FriivBuildConfig("/wbfs/g.wbfs", false, NULL, "", &cfg) == FRIIV_OK); + assert(FriivBuildConfig("/wbfs/g.wbfs", false, NULL, "mod.xml", &cfg) == FRIIV_OK); + assert(FriivBuildConfig("/wbfs/Spectral [SMN01]/SMN01.wbfs", false, NULL, "Spectral_USA.xml", &cfg) == FRIIV_OK); + ExpectAccepted(&cfg); + assert(FriivBuildConfig("/wbfs/g.wbfs", false, NULL, "../mod.xml", &cfg) == FRIIV_ERR_BAD_MOD); + assert(FriivBuildConfig("/wbfs/g.wbfs", false, NULL, "sub/mod.xml", &cfg) == FRIIV_ERR_BAD_MOD); + assert(FriivBuildConfig("/wbfs/g.wbfs", false, NULL, "..", &cfg) == FRIIV_ERR_BAD_MOD); + assert(FriivBuildConfig("wbfs/g.wbfs", false, NULL, "", &cfg) == FRIIV_ERR_BAD_PATH); + + // Result texts are always presentable. + assert(FriivResultText(FRIIV_OK) && FriivResultText(FRIIV_ERR_BAD_DEVICE)); + + puts("friiv-contract: ok"); + return 0; +} diff --git a/source/settings/CGameSettings.cpp b/source/settings/CGameSettings.cpp index 342037a6..94944b7c 100644 --- a/source/settings/CGameSettings.cpp +++ b/source/settings/CGameSettings.cpp @@ -252,6 +252,8 @@ bool CGameSettings::Save() fprintf(f, "DEVODiscDelay:%d; ", GameList[i].DEVODiscDelay); fprintf(f, "PrivateServer:%d; ", GameList[i].PrivateServer); fprintf(f, "CustomAddress:%s; ", GameList[i].CustomAddress.c_str()); + fprintf(f, "FriivMode:%d; ", GameList[i].FriivMode); + fprintf(f, "FriivModXml:%s; ", GameList[i].FriivModXml.c_str()); fprintf(f, "Locked:%d;\n", GameList[i].Locked); } fprintf(f, "# END\n"); @@ -557,6 +559,16 @@ bool CGameSettings::SetSetting(GameCFG & game, const char *name, const char *val game.NINLoaderPath = value; return true; } + else if(strcmp(name, "FriivMode") == 0) + { + game.FriivMode = atoi(value); + return true; + } + else if(strcmp(name, "FriivModXml") == 0) + { + game.FriivModXml = value; + return true; + } else if(strcmp(name, "DEVOMCEmulation") == 0) { game.DEVOMCEmulation = atoi(value); @@ -778,4 +790,6 @@ void CGameSettings::SetDefault(GameCFG &game) game.PrivateServer = INHERIT; game.CustomAddress.clear(); game.Locked = OFF; + game.FriivMode = OFF; + game.FriivModXml.clear(); } diff --git a/source/settings/CGameSettings.h b/source/settings/CGameSettings.h index 87bf68cd..8327e608 100644 --- a/source/settings/CGameSettings.h +++ b/source/settings/CGameSettings.h @@ -79,6 +79,8 @@ typedef struct _GameCFG short PrivateServer; std::string CustomAddress; short Locked; + short FriivMode; + std::string FriivModXml; void operator=(const struct _GameCFG &game) { @@ -152,6 +154,8 @@ typedef struct _GameCFG this->PrivateServer = game.PrivateServer; this->CustomAddress = game.CustomAddress; this->Locked = game.Locked; + this->FriivMode = game.FriivMode; + this->FriivModXml = game.FriivModXml; } } GameCFG; diff --git a/source/settings/CSettings.cpp b/source/settings/CSettings.cpp index f1fffef7..226d0880 100644 --- a/source/settings/CSettings.cpp +++ b/source/settings/CSettings.cpp @@ -75,6 +75,7 @@ void CSettings::SetDefault() snprintf(NandEmuPath, sizeof(NandEmuPath), "%s/nands/01/", BootDevice); snprintf(DEVOLoaderPath, sizeof(DEVOLoaderPath), "%s/apps/gc_devo/", BootDevice); snprintf(NINLoaderPath, sizeof(NINLoaderPath), "%s/apps/nintendont/", BootDevice); + snprintf(FriivLoaderPath, sizeof(FriivLoaderPath), "%s/apps/friivolution/", BootDevice); strlcpy(NandEmuChanPath, NandEmuPath, sizeof(NandEmuChanPath)); strlcpy(GameCubePath, "usb1:/games/", sizeof(GameCubePath)); strlcpy(GameCubeSDPath, "sd:/games/", sizeof(GameCubeSDPath)); @@ -529,6 +530,7 @@ bool CSettings::Save() fprintf(file, "DEVODiscDelay = %d\n", DEVODiscDelay); fprintf(file, "DEVOLoaderPath = %s\n", DEVOLoaderPath); fprintf(file, "NINLoaderPath = %s\n", NINLoaderPath); + fprintf(file, "FriivLoaderPath = %s\n", FriivLoaderPath); fprintf(file, "GCInstallCompressed = %d\n", GCInstallCompressed); fprintf(file, "GCInstallAligned = %d\n", GCInstallAligned); fprintf(file, "PrivateServer = %d\n", PrivateServer); @@ -1266,6 +1268,11 @@ bool CSettings::SetSetting(char *name, char *value) strlcpy(NINLoaderPath, value, sizeof(NINLoaderPath)); return true; } + else if (strcmp(name, "FriivLoaderPath") == 0) + { + strlcpy(FriivLoaderPath, value, sizeof(FriivLoaderPath)); + return true; + } else if (strcmp(name, "GCInstallCompressed") == 0) { GCInstallCompressed = atoi(value); diff --git a/source/settings/CSettings.h b/source/settings/CSettings.h index 17004b1c..e236b90a 100644 --- a/source/settings/CSettings.h +++ b/source/settings/CSettings.h @@ -90,6 +90,7 @@ class CSettings char GameCubeSDPath[100]; char DEVOLoaderPath[100]; char NINLoaderPath[100]; + char FriivLoaderPath[100]; char CustomAddress[300]; char URL_Banners[300]; char URL_Covers2D[300]; diff --git a/source/settings/menus/CustomPathsSM.cpp b/source/settings/menus/CustomPathsSM.cpp index 21991dad..6023c9ff 100644 --- a/source/settings/menus/CustomPathsSM.cpp +++ b/source/settings/menus/CustomPathsSM.cpp @@ -65,6 +65,7 @@ CustomPathsSM::CustomPathsSM() Options->SetName(Idx++, tr("SD GameCube Path")); Options->SetName(Idx++, tr("Devolution Loader Path")); Options->SetName(Idx++, tr("Nintendont Loader Path")); + Options->SetName(Idx++, tr("Friivolution Loader Path")); Options->SetName(Idx++, tr("Cache BNR Files Path")); Options->SetName(Idx++, tr("Cache Path")); @@ -138,6 +139,9 @@ void CustomPathsSM::SetOptionValues() //! Settings: GameCube Nintendont boot.dol Path Options->SetValue(Idx++, Settings.NINLoaderPath); + //! Settings: Friivolution Path + Options->SetValue(Idx++, Settings.FriivLoaderPath); + //! Settings: Cache BNR Files Path Options->SetValue(Idx++, Settings.BNRCachePath); @@ -356,6 +360,13 @@ int CustomPathsSM::GetMenuInternal() ChangePath(Settings.NINLoaderPath, sizeof(Settings.NINLoaderPath)); } + //! Settings: Friivolution boot.dol path + else if (ret == ++Idx) + { + titleTxt->SetText(tr( "Friivolution Loader Path" )); + ChangePath(Settings.FriivLoaderPath, sizeof(Settings.FriivLoaderPath)); + } + //! Settings: Cache BNR Files Path else if (ret == ++Idx) { diff --git a/source/settings/menus/GameLoadSM.cpp b/source/settings/menus/GameLoadSM.cpp index 731e1605..a50c47d5 100644 --- a/source/settings/menus/GameLoadSM.cpp +++ b/source/settings/menus/GameLoadSM.cpp @@ -30,6 +30,7 @@ #include "prompts/DiscBrowser.h" #include "prompts/filebrowser.h" #include "usbloader/AlternateDOLOffsets.h" +#include "riivolution/FriivAdapter.h" #include "language/gettext.h" #include "wad/nandtitle.h" #include "system/IosLoader.h" @@ -274,6 +275,8 @@ void GameLoadSM::SetOptionNames() { Options->SetName(Idx++, "%s", tr( "Alternate DOL" )); Options->SetName(Idx++, "%s", tr( "Select DOL Offset" )); + Options->SetName(Idx++, "%s", tr( "Friivolution" )); + Options->SetName(Idx++, "%s", tr( "Friivolution Mod" )); } } @@ -478,6 +481,15 @@ void GameLoadSM::SetOptionValues() else Options->SetValue(Idx++, "%i", (int)GameConfig.alternatedolstart); } + + //! Settings: Friivolution (Riivolution mods via the Friivolution app) + Options->SetValue(Idx++, "%s", tr( OnOffText[GameConfig.FriivMode == ON ? ON : OFF] )); + + //! Settings: Friivolution Mod (empty = every matching mod, as by hand) + if (GameConfig.FriivModXml.size() == 0) + Options->SetValue(Idx++, "%s", tr( "All mods" )); + else + Options->SetValue(Idx++, "%s", GameConfig.FriivModXml.c_str()); } } @@ -803,6 +815,33 @@ int GameLoadSM::GetMenuInternal() GameConfig.loadalternatedol = 0; if (parentWindow) parentWindow->SetState(STATE_DEFAULT); } + + //! Settings: Friivolution + else if (ret == ++Idx) + { + if (++GameConfig.FriivMode > ON) GameConfig.FriivMode = OFF; + } + + //! Settings: Friivolution Mod (cycles All mods -> each /riivolution/*.xml) + else if (ret == ++Idx) + { + char mods[FRIIV_MOD_MAX][FRIIV_MOD_NAME_MAX]; + int n = FriivCollectModNames(mods, FRIIV_MOD_MAX); + int cur = -1; + for (int i = 0; i < n; i++) + { + if (GameConfig.FriivModXml == mods[i]) + { + cur = i; + break; + } + } + if (++cur >= n) cur = -1; + if (cur < 0) + GameConfig.FriivModXml.clear(); + else + GameConfig.FriivModXml = mods[cur]; + } } SetOptionValues(); diff --git a/source/usbloader/GameBooter.cpp b/source/usbloader/GameBooter.cpp index 98d966fb..dd9d15cf 100644 --- a/source/usbloader/GameBooter.cpp +++ b/source/usbloader/GameBooter.cpp @@ -52,6 +52,8 @@ #include "memory/memory.h" #include "utils/StringTools.h" #include "homebrewboot/BootHomebrew.h" +#include "riivolution/FriivAdapter.h" +#include "homebrewboot/HomebrewXML.h" #include "GameBooter.hpp" #include "NandEmu.h" #include "SavePath.h" @@ -1988,6 +1990,129 @@ int GameBooter::BootNintendont(struct discHdr *gameHdr) } } +int GameBooter::BootFriivolution(struct discHdr *gameHdr) +{ + const char *LoaderName = "Friivolution"; + + if (!gameHdr) + return -1; + + /* Standalone route for Wii titles only; GameCube keeps its own loaders. */ + if (gameHdr->type != TYPE_GAME_WII_IMG && gameHdr->type != TYPE_GAME_WII_DISC) + { + WindowPrompt(tr("Error:"), tr(FriivResultText(FRIIV_ERR_TYPE)), tr("OK")); + return -1; + } + + GameCFG *game_cfg = GameSettings.GetGameCFG(gameHdr->id); + const char *modXml = game_cfg ? game_cfg->FriivModXml.c_str() : ""; + + /* Physical disc boots with an empty path; FAT backups are translated. */ + bool isDisc = (gameHdr->type == TYPE_GAME_WII_DISC); + const char *fatPath = NULL; + if (!isDisc) + { + if (gameHdr->is_ciso) + { + WindowPrompt(tr("Error:"), tr(FriivResultText(FRIIV_ERR_CISO)), tr("OK")); + return -1; + } + if (strlen(gameHdr->path) == 0) + { + WindowPrompt(tr("Error:"), tr(FriivResultText(FRIIV_ERR_NO_PATH)), tr("OK")); + return -1; + } + fatPath = gameHdr->path; + } + + char relPath[FRIIV_CFG_PATH_MAX]; + bool useUsb = false; + FriivBuildResult pr = FriivTranslateGamePath(fatPath, isDisc, relPath, sizeof(relPath), &useUsb); + if (pr != FRIIV_OK) + { + WindowPrompt(tr("Error:"), tr(FriivResultText(pr)), tr("OK")); + return -1; + } + + FRIIV_CFG friiv_cfg; + FriivBuildResult br = FriivBuildConfig(relPath, useUsb, gameHdr->id, modXml, &friiv_cfg); + if (br != FRIIV_OK) + { + WindowPrompt(tr("Error:"), tr(FriivResultText(br)), tr("OK")); + return -1; + } + + /* Locate the Friivolution application (configurable directory). */ + char loaderPath[MAX_FAT_PATH]; + size_t dirLen = strlen(Settings.FriivLoaderPath); + bool hasSlash = dirLen > 0 && Settings.FriivLoaderPath[dirLen - 1] == '/'; + snprintf(loaderPath, sizeof(loaderPath), "%s%sboot.dol", + Settings.FriivLoaderPath, hasSlash ? "" : "/"); + if (!CheckFile(loaderPath)) + { + snprintf(loaderPath, sizeof(loaderPath), "%s%sboot.elf", + Settings.FriivLoaderPath, hasSlash ? "" : "/"); + } + if (!CheckFile(loaderPath)) + { + WindowPrompt(tr("Error:"), + fmt(tr("To launch Wii games with %s you need its boot file in the Friivolution Loader Path."), LoaderName), + tr("OK")); + return -1; + } + gprintf("FRIIV: Loader path = %s\n", loaderPath); + gprintf("FRIIV: Game path = %s (%s)\n", relPath[0] ? relPath : "(disc)", useUsb ? "USB" : "SD"); + if (friiv_cfg.ModXml[0]) + gprintf("FRIIV: Mod XML = %s\n", friiv_cfg.ModXml); + + /* Support check: 1.01+ understands the argument. Older builds (and + * Riivolution itself) harmlessly ignore it and show their own menus. */ + { + char metaPath[MAX_FAT_PATH]; + snprintf(metaPath, sizeof(metaPath), "%s%smeta.xml", + Settings.FriivLoaderPath, hasSlash ? "" : "/"); + HomebrewXML meta; + if (meta.LoadHomebrewXMLData(metaPath) == 1 && meta.GetVersion()) + { + gprintf("FRIIV: meta.xml version = %s\n", meta.GetVersion()); + if (!strcmp(meta.GetVersion(), "1.00")) + { + int choice = WindowPrompt(tr("Warning:"), + tr("This Friivolution version predates loader handoff (need 1.01+). It will show its own menus. Launch anyway?"), + tr("Yes"), tr("Cancel")); + if (choice == 0) + return -1; + } + } + } + + /* Same binary-argument handoff GX uses for Nintendont. */ + u8 *buffer = NULL; + u32 filesize = 0; + LoadFileToMem(loaderPath, &buffer, &filesize); + if (!buffer) + return -1; + FreeHomebrewBuffer(); + CopyHomebrewMemory(buffer, 0, filesize); + + FRIIV_CFG *arg = (FRIIV_CFG *)MEM2_alloc(sizeof(FRIIV_CFG)); + if (!arg) + { + gprintf("FRIIV: out of memory for boot argument\n"); + WindowPrompt(tr("Error:"), tr("Could not allocate memory."), tr("OK")); + return -1; + } + memcpy(arg, &friiv_cfg, sizeof(*arg)); + + AddBootArgument(loaderPath); + AddBootArgument((char *)arg, sizeof(*arg)); + + MEM2_free(arg); + + /* Chainload; on success this never returns. */ + return !(BootHomebrewFromMem() < 0); +} + int GameBooter::BootNeek(struct discHdr *gameHdr) { struct discHdr gameHeader; diff --git a/source/usbloader/GameBooter.hpp b/source/usbloader/GameBooter.hpp index 5f208458..d5080429 100644 --- a/source/usbloader/GameBooter.hpp +++ b/source/usbloader/GameBooter.hpp @@ -25,6 +25,7 @@ class GameBooter public: static int BootGame(struct discHdr *gameHdr, const s8 useOcarina = -1); static int BootGCMode(struct discHdr *gameHdr); + static int BootFriivolution(struct discHdr *gameHdr); private: static void SetupAltDOL(u8 * gameID, u8 &alternatedol, u32 &alternatedoloffset); static void SetupNandEmu(u8 NandEmuMode, const char *NandEmuPath, struct discHdr &gameHeader);