diff --git a/source/settings/CGameSettings.cpp b/source/settings/CGameSettings.cpp index 342037a6..84df5984 100644 --- a/source/settings/CGameSettings.cpp +++ b/source/settings/CGameSettings.cpp @@ -252,6 +252,7 @@ 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, "RiivolutionPack:%s; ", GameList[i].RiivolutionPack.c_str()); fprintf(f, "Locked:%d;\n", GameList[i].Locked); } fprintf(f, "# END\n"); @@ -608,6 +609,11 @@ bool CGameSettings::SetSetting(GameCFG & game, const char *name, const char *val game.CustomAddress = value; return true; } + else if(strcmp(name, "RiivolutionPack") == 0) + { + game.RiivolutionPack = value; + return true; + } return false; } @@ -777,5 +783,6 @@ void CGameSettings::SetDefault(GameCFG &game) game.DEVODiscDelay = INHERIT; game.PrivateServer = INHERIT; game.CustomAddress.clear(); + game.RiivolutionPack.clear(); game.Locked = OFF; } diff --git a/source/settings/CGameSettings.h b/source/settings/CGameSettings.h index 87bf68cd..e260c901 100644 --- a/source/settings/CGameSettings.h +++ b/source/settings/CGameSettings.h @@ -78,6 +78,7 @@ typedef struct _GameCFG short DEVODiscDelay; short PrivateServer; std::string CustomAddress; + std::string RiivolutionPack; short Locked; void operator=(const struct _GameCFG &game) @@ -151,6 +152,7 @@ typedef struct _GameCFG this->DEVODiscDelay = game.DEVODiscDelay; this->PrivateServer = game.PrivateServer; this->CustomAddress = game.CustomAddress; + this->RiivolutionPack = game.RiivolutionPack; this->Locked = game.Locked; } } GameCFG; diff --git a/source/settings/menus/GameLoadSM.cpp b/source/settings/menus/GameLoadSM.cpp index 731e1605..8239d64e 100644 --- a/source/settings/menus/GameLoadSM.cpp +++ b/source/settings/menus/GameLoadSM.cpp @@ -33,6 +33,7 @@ #include "language/gettext.h" #include "wad/nandtitle.h" #include "system/IosLoader.h" +#include "usbloader/RiftWii.hpp" #include "GameLoadSM.hpp" #include "sys.h" @@ -274,6 +275,7 @@ void GameLoadSM::SetOptionNames() { Options->SetName(Idx++, "%s", tr( "Alternate DOL" )); Options->SetName(Idx++, "%s", tr( "Select DOL Offset" )); + Options->SetName(Idx++, "%s", tr( "Riivolution (RiftWii)" )); } } @@ -478,6 +480,12 @@ void GameLoadSM::SetOptionValues() else Options->SetValue(Idx++, "%i", (int)GameConfig.alternatedolstart); } + + //! Settings: Riivolution (RiftWii) + if (GameConfig.RiivolutionPack.size() == 0) + Options->SetValue(Idx++, "%s", tr( OnOffText[0] )); + else + Options->SetValue(Idx++, "%s", GameConfig.RiivolutionPack.c_str()); } } @@ -803,6 +811,25 @@ int GameLoadSM::GetMenuInternal() GameConfig.loadalternatedol = 0; if (parentWindow) parentWindow->SetState(STATE_DEFAULT); } + + //! Settings: Riivolution (RiftWii): OFF, then each pack for this game in turn + else if (ret == ++Idx) + { + std::vector packs; + RiftWii::ListPacks(GameConfig.id, packs); + if (RiftWii::LoaderPath().empty()) + WindowPrompt(tr( "RiftWii not found" ), tr( "Riivolution packs are played through RiftWii. Copy it to apps/riftwii on the SD card." ), tr( "OK" )); + else if (packs.empty()) + WindowPrompt(tr( "No Riivolution packs" ), tr( "No pack in sd:/riivolution is made for this game." ), tr( "OK" )); + else + { + size_t next = 0; + for (size_t i = 0; i < packs.size(); ++i) + if (packs[i] == GameConfig.RiivolutionPack) + next = i + 1; + GameConfig.RiivolutionPack = next < packs.size() ? packs[next] : ""; + } + } } SetOptionValues(); diff --git a/source/usbloader/GameBooter.cpp b/source/usbloader/GameBooter.cpp index 98d966fb..21bb7137 100644 --- a/source/usbloader/GameBooter.cpp +++ b/source/usbloader/GameBooter.cpp @@ -52,6 +52,7 @@ #include "memory/memory.h" #include "utils/StringTools.h" #include "homebrewboot/BootHomebrew.h" +#include "usbloader/RiftWii.hpp" #include "GameBooter.hpp" #include "NandEmu.h" #include "SavePath.h" @@ -439,6 +440,15 @@ int GameBooter::BootGame(struct discHdr *gameHdr, const s8 useOcarina) } } + //! A Riivolution pack is picked: RiftWii applies it and boots the game with these settings + if (game_cfg->RiivolutionPack.size() && (gameHeader.type == TYPE_GAME_WII_IMG || gameHeader.type == TYPE_GAME_WII_DISC)) + { + RiftWii::BootGame(gameHeader, game_cfg->RiivolutionPack, videoChoice, languageChoice, ocarinaChoice, deflicker, + videoWidth, PrivServChoice, customAddress, autoIOS, iosChoice, returnToChoice); + WindowPrompt(tr( "Error:" ), tr( "RiftWii could not be started. It goes in apps/riftwii on the SD card." ), tr( "OK" )); + return -1; + } + if (autoIOS == GAME_IOS_AUTO && d2x_list.size() && !isWiiVC) { u8 requestedIOS = 0; diff --git a/source/usbloader/RiftWii.cpp b/source/usbloader/RiftWii.cpp new file mode 100644 index 00000000..b86ddc7c --- /dev/null +++ b/source/usbloader/RiftWii.cpp @@ -0,0 +1,160 @@ +/**************************************************************************** + * Copyright (C) 2026 KakarottoCake + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + ****************************************************************************/ +#include +#include +#include +#include +#include "settings/CSettings.h" +#include "settings/SettingsEnums.h" +#include "FileOperations/fileops.h" +#include "homebrewboot/BootHomebrew.h" +#include "RiftWii.hpp" +#include "gecko.h" + +#define RIIVOLUTION_PATH "sd:/riivolution/" + +std::string RiftWii::LoaderPath() +{ + std::string path = Settings.homebrewapps_path; + if (path.size() && path.back() != '/') + path += '/'; + path += "riftwii/boot.dol"; + if (CheckFile(path.c_str())) + return path; + if (CheckFile("sd:/apps/riftwii/boot.dol")) + return "sd:/apps/riftwii/boot.dol"; + return ""; +} + +//! A pack names its game in (three to six characters) +static bool PackIsForGame(const char *filepath, const char *gameID) +{ + FILE *file = fopen(filepath, "rb"); + if (!file) + return false; + + char head[4096]; + size_t len = fread(head, 1, sizeof(head) - 1, file); + fclose(file); + head[len] = 0; + + const char *id = strstr(head, "= 3 && n <= 6 && strncmp(game, gameID, n) == 0; +} + +void RiftWii::ListPacks(const char *gameID, std::vector &packs) +{ + packs.clear(); + DIR *dir = opendir(RIIVOLUTION_PATH); + if (!dir) + return; + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) + { + const char *name = entry->d_name; + size_t len = strlen(name); + //! Hidden files are macOS leftovers; ';' would end the entry in GameSettings.cfg + if (name[0] == '.' || len < 5 || strcasecmp(name + len - 4, ".xml") != 0 || strchr(name, ';')) + continue; + std::string path = std::string(RIIVOLUTION_PATH) + name; + if (PackIsForGame(path.c_str(), gameID)) + packs.push_back(name); + } + closedir(dir); +} + +int RiftWii::BootGame(const struct discHdr &gameHeader, const std::string &pack, u8 videoChoice, u8 languageChoice, + u8 ocarinaChoice, u8 deflicker, u16 videoWidth, u8 privateServer, const char *customAddress, + u8 autoIOS, s32 iosChoice, u64 returnTo) +{ + static const char *videoModes[VIDEO_MODE_MAX] = { "system", "game", "pal50", "pal60", "ntsc", "system", "480p", "480p" }; + static const char *deflickers[DEFLICKER_MAX] = { "game", "off", "off", "low", "medium", "high" }; + static const char *languages[CONSOLE_DEFAULT + 1] = { "ja", "en", "de", "fr", "es", "it", "nl", "zh-hans", "zh-hant", "ko", "console" }; + static const char *servers[PRIVSERV_MAX_CHOICE] = { "off", "off", "wiimmfi", "altwfc", "custom" }; + + std::string loader = LoaderPath(); + if (loader.empty()) + return -1; + + std::vector args; + args.push_back("--launch"); + args.push_back(std::string("game=") + std::string((const char *)gameHeader.id, 6)); + if (gameHeader.type == TYPE_GAME_WII_DISC) + args.push_back("from=disc"); + else + args.push_back(Settings.SDMode ? "from=sd" : "from=usb"); + args.push_back("xml=" RIIVOLUTION_PATH + pack); + + args.push_back(std::string("video_mode=") + videoModes[videoChoice]); + args.push_back(std::string("deflicker=") + deflickers[deflicker]); + args.push_back(videoWidth == WIDTH_FRAMEBUFFER ? "video_width=framebuffer" : "video_width=game"); + args.push_back(std::string("language=") + languages[languageChoice]); + args.push_back(std::string("server=") + servers[privateServer]); + if (privateServer == PRIVSERV_CUSTOM && *customAddress) + args.push_back(std::string("wfc_domain=") + customAddress); + + if (autoIOS == GAME_IOS_CUSTOM && iosChoice >= 248 && iosChoice <= 252) + args.push_back("cios=" + std::to_string(iosChoice)); + else + args.push_back("cios=auto"); + + //! RiftWii reads codes from the SD card only + char gct[160]; + snprintf(gct, sizeof(gct), "%s%.6s.gct", Settings.Cheatcodespath, (const char *)gameHeader.id); + if (ocarinaChoice == OCARINA_ON && strncmp(gct, "sd:/", 4) == 0 && CheckFile(gct)) + args.push_back(std::string("gct=") + gct); + else + args.push_back("gct=none"); + + if (returnTo) + { + char title[17]; + snprintf(title, sizeof(title), "%016llx", returnTo); + args.push_back(std::string("return_to=") + title); + } + else + args.push_back("return_to=menu"); + + u8 *buffer = NULL; + u32 filesize = 0; + LoadFileToMem(loader.c_str(), &buffer, &filesize); + if (!buffer) + return -1; + FreeHomebrewBuffer(); + CopyHomebrewMemory(buffer, 0, filesize); + free(buffer); + + gprintf("RiftWii: %s", loader.c_str()); + AddBootArgument(loader.c_str()); + for (size_t i = 0; i < args.size(); i++) + { + gprintf(" %s", args[i].c_str()); + AddBootArgument(args[i].c_str()); + } + gprintf("\n"); + + return BootHomebrewFromMem(); +} diff --git a/source/usbloader/RiftWii.hpp b/source/usbloader/RiftWii.hpp new file mode 100644 index 00000000..e73ab1b9 --- /dev/null +++ b/source/usbloader/RiftWii.hpp @@ -0,0 +1,42 @@ +/**************************************************************************** + * Copyright (C) 2026 KakarottoCake + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + ****************************************************************************/ +#ifndef RIFTWII_HPP_ +#define RIFTWII_HPP_ + +#include +#include +#include +#include "usbloader/disc.h" + +//! Riivolution packs through RiftWii (https://github.com/KakarottoCake/Riftwii). +//! A game with a pack picked in its Game Load settings is handed to RiftWii, +//! which applies the pack and boots the game with the settings chosen here. +//! RiftWii's side is documented in its docs/HEADLESS.md. +namespace RiftWii +{ + //! Where RiftWii's boot.dol was found, or an empty string + std::string LoaderPath(); + //! The packs in sd:/riivolution made for this game (file names) + void ListPacks(const char *gameID, std::vector &packs); + //! Starts RiftWii with the game, the pack and the resolved game settings. + //! Only returns on failure. + int BootGame(const struct discHdr &gameHeader, const std::string &pack, u8 videoChoice, u8 languageChoice, + u8 ocarinaChoice, u8 deflicker, u16 videoWidth, u8 privateServer, const char *customAddress, + u8 autoIOS, s32 iosChoice, u64 returnTo); +} + +#endif