Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions source/settings/CGameSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -777,5 +783,6 @@ void CGameSettings::SetDefault(GameCFG &game)
game.DEVODiscDelay = INHERIT;
game.PrivateServer = INHERIT;
game.CustomAddress.clear();
game.RiivolutionPack.clear();
game.Locked = OFF;
}
2 changes: 2 additions & 0 deletions source/settings/CGameSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
27 changes: 27 additions & 0 deletions source/settings/menus/GameLoadSM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)" ));
}
}

Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -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<std::string> 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();
Expand Down
10 changes: 10 additions & 0 deletions source/usbloader/GameBooter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down
160 changes: 160 additions & 0 deletions source/usbloader/RiftWii.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
****************************************************************************/
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <malloc.h>
#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 <id game="RMC"> (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, "<id");
const char *game = id ? strstr(id, "game=\"") : NULL;
if (!game)
return false;
game += 6;

size_t n = 0;
while (game[n] && game[n] != '"')
n++;
return n >= 3 && n <= 6 && strncmp(game, gameID, n) == 0;
}

void RiftWii::ListPacks(const char *gameID, std::vector<std::string> &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<std::string> 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();
}
42 changes: 42 additions & 0 deletions source/usbloader/RiftWii.hpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
****************************************************************************/
#ifndef RIFTWII_HPP_
#define RIFTWII_HPP_

#include <string>
#include <vector>
#include <gctypes.h>
#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<std::string> &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