Merge pull request #10187 from AdmiralCurtiss/json-gamelist

Support for GameModDescriptor files in Game List.
This commit is contained in:
Léo Lam 2021-12-14 11:08:38 +01:00 committed by GitHub
commit 185475fe03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 575 additions and 66 deletions

View File

@ -30,7 +30,7 @@ import java.util.Set;
public final class FileBrowserHelper
{
public static final HashSet<String> GAME_EXTENSIONS = new HashSet<>(Arrays.asList(
"gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf"));
"gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf", "json"));
public static final HashSet<String> GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS);

View File

@ -155,6 +155,11 @@ const std::vector<u64>& BootSessionData::GetWiiSyncTitles() const
return m_wii_sync_titles;
}
const std::string& BootSessionData::GetWiiSyncRedirectFolder() const
{
return m_wii_sync_redirect_folder;
}
void BootSessionData::InvokeWiiSyncCleanup() const
{
if (m_wii_sync_cleanup)
@ -162,10 +167,12 @@ void BootSessionData::InvokeWiiSyncCleanup() const
}
void BootSessionData::SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs,
std::vector<u64> titles, WiiSyncCleanupFunction cleanup)
std::vector<u64> titles, std::string redirect_folder,
WiiSyncCleanupFunction cleanup)
{
m_wii_sync_fs = std::move(fs);
m_wii_sync_titles = std::move(titles);
m_wii_sync_redirect_folder = std::move(redirect_folder);
m_wii_sync_cleanup = std::move(cleanup);
}

View File

@ -66,9 +66,10 @@ public:
IOS::HLE::FS::FileSystem* GetWiiSyncFS() const;
const std::vector<u64>& GetWiiSyncTitles() const;
const std::string& GetWiiSyncRedirectFolder() const;
void InvokeWiiSyncCleanup() const;
void SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs, std::vector<u64> titles,
WiiSyncCleanupFunction cleanup);
std::string redirect_folder, WiiSyncCleanupFunction cleanup);
private:
std::optional<std::string> m_savestate_path;
@ -76,6 +77,7 @@ private:
std::unique_ptr<IOS::HLE::FS::FileSystem> m_wii_sync_fs;
std::vector<u64> m_wii_sync_titles;
std::string m_wii_sync_redirect_folder;
WiiSyncCleanupFunction m_wii_sync_cleanup;
};

View File

@ -1077,6 +1077,7 @@ void NetPlayClient::OnSyncSaveDataGCI(sf::Packet& packet)
void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet)
{
const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP;
std::string redirect_path = File::GetUserPath(D_USER_IDX) + "Redirect" GC_MEMCARD_NETPLAY DIR_SEP;
if (File::Exists(path) && !File::DeleteDirRecursively(path))
{
@ -1084,6 +1085,12 @@ void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet)
SyncSaveDataResponse(false);
return;
}
if (File::Exists(redirect_path) && !File::DeleteDirRecursively(redirect_path))
{
PanicAlertFmtT("Failed to reset NetPlay redirect folder. Verify your write permissions.");
SyncSaveDataResponse(false);
return;
}
auto temp_fs = std::make_unique<IOS::HLE::FS::HostFileSystem>(path);
std::vector<u64> titles;
@ -1190,7 +1197,19 @@ void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet)
}
}
SetWiiSyncData(std::move(temp_fs), std::move(titles));
bool has_redirected_save;
packet >> has_redirected_save;
if (has_redirected_save)
{
if (!DecompressPacketIntoFolder(packet, redirect_path))
{
PanicAlertFmtT("Failed to write redirected save.");
SyncSaveDataResponse(false);
return;
}
}
SetWiiSyncData(std::move(temp_fs), std::move(titles), std::move(redirect_path));
SyncSaveDataResponse(true);
}
@ -1721,11 +1740,17 @@ bool NetPlayClient::StartGame(const std::string& path)
// boot game
auto boot_session_data = std::make_unique<BootSessionData>();
boot_session_data->SetWiiSyncData(std::move(m_wii_sync_fs), std::move(m_wii_sync_titles), [] {
boot_session_data->SetWiiSyncData(
std::move(m_wii_sync_fs), std::move(m_wii_sync_titles), std::move(m_wii_sync_redirect_folder),
[] {
// on emulation end clean up the Wii save sync directory -- see OnSyncSaveDataWii()
const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP;
if (File::Exists(path))
File::DeleteDirRecursively(path);
const std::string redirect_path =
File::GetUserPath(D_USER_IDX) + "Redirect" GC_MEMCARD_NETPLAY DIR_SEP;
if (File::Exists(redirect_path))
File::DeleteDirRecursively(redirect_path);
});
m_dialog->BootGame(path, std::move(boot_session_data));
@ -2501,10 +2526,11 @@ void NetPlayClient::AdjustPadBufferSize(const unsigned int size)
}
void NetPlayClient::SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs,
std::vector<u64> titles)
std::vector<u64> titles, std::string redirect_folder)
{
m_wii_sync_fs = std::move(fs);
m_wii_sync_titles = std::move(titles);
m_wii_sync_redirect_folder = std::move(redirect_folder);
}
SyncIdentifier NetPlayClient::GetSDCardIdentifier()

View File

@ -86,7 +86,7 @@ public:
virtual void HideChunkedProgressDialog() = 0;
virtual void SetChunkedProgress(int pid, u64 progress) = 0;
virtual void SetHostWiiSyncTitles(std::vector<u64> titles) = 0;
virtual void SetHostWiiSyncData(std::vector<u64> titles, std::string redirect_folder) = 0;
};
class Player
@ -157,7 +157,8 @@ public:
void AdjustPadBufferSize(unsigned int size);
void SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs, std::vector<u64> titles);
void SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs, std::vector<u64> titles,
std::string redirect_folder);
static SyncIdentifier GetSDCardIdentifier();
@ -328,6 +329,7 @@ private:
std::unique_ptr<IOS::HLE::FS::FileSystem> m_wii_sync_fs;
std::vector<u64> m_wii_sync_titles;
std::string m_wii_sync_redirect_folder;
};
void NetPlay_Enable(NetPlayClient* const np);

View File

@ -3,6 +3,7 @@
#include "Core/NetPlayCommon.h"
#include <fmt/format.h>
#include <lzo/lzo1x.h>
#include "Common/FileUtil.h"
@ -84,6 +85,35 @@ bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet)
return true;
}
static bool CompressFolderIntoPacketInternal(const File::FSTEntry& folder, sf::Packet& packet)
{
const sf::Uint64 size = folder.children.size();
packet << size;
for (const auto& child : folder.children)
{
const bool is_folder = child.isDirectory;
packet << child.virtualName;
packet << is_folder;
const bool success = is_folder ? CompressFolderIntoPacketInternal(child, packet) :
CompressFileIntoPacket(child.physicalName, packet);
if (!success)
return false;
}
return true;
}
bool CompressFolderIntoPacket(const std::string& folder_path, sf::Packet& packet)
{
if (!File::IsDirectory(folder_path))
{
packet << false;
return true;
}
packet << true;
return CompressFolderIntoPacketInternal(File::ScanDirectoryTree(folder_path, true), packet);
}
bool CompressBufferIntoPacket(const std::vector<u8>& in_buffer, sf::Packet& packet)
{
const sf::Uint64 size = in_buffer.size();
@ -187,6 +217,47 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path)
return true;
}
static bool DecompressPacketIntoFolderInternal(sf::Packet& packet, const std::string& folder_path)
{
if (!File::CreateFullPath(folder_path + "/"))
return false;
sf::Uint64 size;
packet >> size;
for (size_t i = 0; i < size; ++i)
{
std::string name;
packet >> name;
if (name.find('/') != std::string::npos)
return false;
#ifdef _WIN32
if (name.find('\\') != std::string::npos)
return false;
#endif
if (std::all_of(name.begin(), name.end(), [](char c) { return c == '.'; }))
return false;
bool is_folder;
packet >> is_folder;
std::string path = fmt::format("{}/{}", folder_path, name);
const bool success = is_folder ? DecompressPacketIntoFolderInternal(packet, path) :
DecompressPacketIntoFile(packet, path);
if (!success)
return false;
}
return true;
}
bool DecompressPacketIntoFolder(sf::Packet& packet, const std::string& folder_path)
{
bool folder_existed;
packet >> folder_existed;
if (!folder_existed)
return true;
return DecompressPacketIntoFolderInternal(packet, folder_path);
}
std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet)
{
u64 size = Common::PacketReadU64(packet);

View File

@ -15,7 +15,9 @@
namespace NetPlay
{
bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet);
bool CompressFolderIntoPacket(const std::string& folder_path, sf::Packet& packet);
bool CompressBufferIntoPacket(const std::vector<u8>& in_buffer, sf::Packet& packet);
bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path);
bool DecompressPacketIntoFolder(sf::Packet& packet, const std::string& folder_path);
std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet);
} // namespace NetPlay

View File

@ -31,6 +31,7 @@
#include "Common/Version.h"
#include "Core/ActionReplay.h"
#include "Core/Boot/Boot.h"
#include "Core/Config/GraphicsSettings.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/NetplaySettings.h"
@ -60,6 +61,7 @@
#include "Core/SyncIdentifier.h"
#include "DiscIO/Enums.h"
#include "DiscIO/RiivolutionPatcher.h"
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
#include "InputCommon/GCPadStatus.h"
@ -1616,6 +1618,17 @@ bool NetPlayServer::SyncSaveData()
save_count++;
}
std::optional<DiscIO::Riivolution::SavegameRedirect> redirected_save;
if (wii_save && game->GetBlobType() == DiscIO::BlobType::MOD_DESCRIPTOR)
{
auto boot_params = BootParameters::GenerateFromFile(game->GetFilePath());
if (boot_params)
{
redirected_save =
DiscIO::Riivolution::ExtractSavegameRedirect(boot_params->riivolution_patches);
}
}
for (const auto& config : m_gba_config)
{
if (config.enabled && config.has_rom)
@ -1818,8 +1831,20 @@ bool NetPlayServer::SyncSaveData()
}
}
if (redirected_save)
{
pac << true;
if (!CompressFolderIntoPacket(redirected_save->m_target_path, pac))
return false;
}
else
{
pac << false; // no redirected save
}
// Set titles for host-side loading in WiiRoot
m_dialog->SetHostWiiSyncTitles(std::move(titles));
m_dialog->SetHostWiiSyncData(std::move(titles),
redirected_save ? redirected_save->m_target_path : "");
SendChunkedToClients(std::move(pac), 1, "Wii Save Synchronization");
}

View File

@ -35,9 +35,19 @@ namespace Core
namespace FS = IOS::HLE::FS;
static std::string s_temp_wii_root;
static std::string s_temp_redirect_root;
static bool s_wii_root_initialized = false;
static std::vector<IOS::HLE::FS::NandRedirect> s_nand_redirects;
// When Temp NAND + Redirects are both active, we need to keep track of where each redirect path
// should be copied back to after a successful session finish.
struct TempRedirectPath
{
std::string real_path;
std::string temp_path;
};
static std::vector<TempRedirectPath> s_temp_nand_redirects;
const std::vector<IOS::HLE::FS::NandRedirect>& GetActiveNandRedirects()
{
return s_nand_redirects;
@ -175,6 +185,28 @@ static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs,
WARN_LOG_FMT(CORE, "Failed to copy Mii database to the NAND");
}
}
const auto& netplay_redirect_folder = boot_session_data.GetWiiSyncRedirectFolder();
if (!netplay_redirect_folder.empty())
File::CopyDir(netplay_redirect_folder, s_temp_redirect_root + "/");
}
}
static void MoveToBackupIfExists(const std::string& path)
{
if (File::Exists(path))
{
const std::string backup_path = path.substr(0, path.size() - 1) + ".backup" DIR_SEP;
WARN_LOG_FMT(IOS_FS, "Temporary directory at {} exists, moving to backup...", path);
// If backup exists, delete it as we don't want a mess
if (File::Exists(backup_path))
{
WARN_LOG_FMT(IOS_FS, "Temporary backup directory at {} exists, deleting...", backup_path);
File::DeleteDirRecursively(backup_path);
}
File::CopyDir(path, backup_path, true);
}
}
@ -185,24 +217,13 @@ void InitializeWiiRoot(bool use_temporary)
if (use_temporary)
{
s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP;
s_temp_redirect_root = File::GetUserPath(D_USER_IDX) + "RedirectSession" DIR_SEP;
WARN_LOG_FMT(IOS_FS, "Using temporary directory {} for minimal Wii FS", s_temp_wii_root);
WARN_LOG_FMT(IOS_FS, "Using temporary directory {} for redirected saves", s_temp_redirect_root);
// If directory exists, make a backup
if (File::Exists(s_temp_wii_root))
{
const std::string backup_path =
s_temp_wii_root.substr(0, s_temp_wii_root.size() - 1) + ".backup" DIR_SEP;
WARN_LOG_FMT(IOS_FS, "Temporary Wii FS directory exists, moving to backup...");
// If backup exists, delete it as we don't want a mess
if (File::Exists(backup_path))
{
WARN_LOG_FMT(IOS_FS, "Temporary Wii FS backup directory exists, deleting...");
File::DeleteDirRecursively(backup_path);
}
File::CopyDir(s_temp_wii_root, backup_path, true);
}
MoveToBackupIfExists(s_temp_wii_root);
MoveToBackupIfExists(s_temp_redirect_root);
File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root);
}
@ -221,6 +242,9 @@ void ShutdownWiiRoot()
{
File::DeleteDirRecursively(s_temp_wii_root);
s_temp_wii_root.clear();
File::DeleteDirRecursively(s_temp_redirect_root);
s_temp_redirect_root.clear();
s_temp_nand_redirects.clear();
}
s_nand_redirects.clear();
@ -312,7 +336,8 @@ void InitializeWiiFileSystemContents(
if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, ""))
WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND");
if (WiiRootIsTemporary())
const bool is_temp_nand = WiiRootIsTemporary();
if (is_temp_nand)
{
// Generate a SYSCONF with default settings for the temporary Wii NAND.
SysConf sysconf{fs};
@ -320,16 +345,26 @@ void InitializeWiiFileSystemContents(
InitializeDeterministicWiiSaves(fs.get(), boot_session_data);
}
else if (save_redirect)
if (save_redirect)
{
const u64 title_id = SConfig::GetInstance().GetTitleID();
std::string source_path = Common::GetTitleDataPath(title_id);
if (is_temp_nand)
{
// remember the actual path for copying back on shutdown and redirect to a temp folder instead
s_temp_nand_redirects.emplace_back(
TempRedirectPath{save_redirect->m_target_path, s_temp_redirect_root});
save_redirect->m_target_path = s_temp_redirect_root;
}
if (!File::IsDirectory(save_redirect->m_target_path))
{
File::CreateFullPath(save_redirect->m_target_path + "/");
if (save_redirect->m_clone)
{
File::CopyDir(Common::GetTitleDataPath(title_id, Common::FROM_CONFIGURED_ROOT),
File::CopyDir(Common::GetTitleDataPath(title_id, Common::FROM_SESSION_ROOT),
save_redirect->m_target_path);
}
}
@ -347,7 +382,16 @@ void CleanUpWiiFileSystemContents(const BootSessionData& boot_session_data)
return;
}
// copy back the temp nand redirected files to where they should normally be redirected to
for (const auto& redirect : s_temp_nand_redirects)
File::CopyDir(redirect.temp_path, redirect.real_path + "/", true);
IOS::HLE::EmulationKernel* ios = IOS::HLE::GetIOS();
// clear the redirects in the session FS, otherwise the back-copy might grab redirected files
s_nand_redirects.clear();
ios->GetFS()->SetNandRedirects({});
const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured);
// Copy back Mii data

View File

@ -50,6 +50,8 @@ std::string GetName(BlobType blob_type, bool translate)
return "WIA";
case BlobType::RVZ:
return "RVZ";
case BlobType::MOD_DESCRIPTOR:
return translate_str("Mod");
default:
return "";
}

View File

@ -39,6 +39,7 @@ enum class BlobType
TGC,
WIA,
RVZ,
MOD_DESCRIPTOR,
};
std::string GetName(BlobType blob_type, bool translate);

View File

@ -115,10 +115,15 @@ std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view j
return std::nullopt;
GameModDescriptor descriptor;
bool is_game_mod_descriptor = false;
bool is_valid_version = false;
for (const auto& [key, value] : json_root.get<picojson::object>())
{
if (key == "version" && value.is<double>())
if (key == "type" && value.is<std::string>())
{
is_game_mod_descriptor = value.get<std::string>() == "dolphin-game-mod-descriptor";
}
else if (key == "version" && value.is<double>())
{
is_valid_version = value.get<double>() == 1.0;
}
@ -140,8 +145,80 @@ std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view j
ParseRiivolutionObject(json_directory, value.get<picojson::object>());
}
}
if (!is_valid_version)
if (!is_game_mod_descriptor || !is_valid_version)
return std::nullopt;
return descriptor;
}
static picojson::object
WriteGameModDescriptorRiivolution(const GameModDescriptorRiivolution& riivolution)
{
picojson::array json_patches;
for (const auto& patch : riivolution.patches)
{
picojson::object json_patch;
if (!patch.xml.empty())
json_patch["xml"] = picojson::value(patch.xml);
if (!patch.root.empty())
json_patch["root"] = picojson::value(patch.root);
if (!patch.options.empty())
{
picojson::array json_options;
for (const auto& option : patch.options)
{
picojson::object json_option;
if (!option.section_name.empty())
json_option["section-name"] = picojson::value(option.section_name);
if (!option.option_id.empty())
json_option["option-id"] = picojson::value(option.option_id);
if (!option.option_name.empty())
json_option["option-name"] = picojson::value(option.option_name);
json_option["choice"] = picojson::value(static_cast<double>(option.choice));
json_options.emplace_back(std::move(json_option));
}
json_patch["options"] = picojson::value(std::move(json_options));
}
json_patches.emplace_back(std::move(json_patch));
}
picojson::object json_riivolution;
json_riivolution["patches"] = picojson::value(std::move(json_patches));
return json_riivolution;
}
std::string WriteGameModDescriptorString(const GameModDescriptor& descriptor, bool pretty)
{
picojson::object json_root;
json_root["type"] = picojson::value("dolphin-game-mod-descriptor");
json_root["version"] = picojson::value(1.0);
if (!descriptor.base_file.empty())
json_root["base-file"] = picojson::value(descriptor.base_file);
if (!descriptor.display_name.empty())
json_root["display-name"] = picojson::value(descriptor.display_name);
if (!descriptor.banner.empty())
json_root["banner"] = picojson::value(descriptor.banner);
if (descriptor.riivolution)
{
json_root["riivolution"] =
picojson::value(WriteGameModDescriptorRiivolution(*descriptor.riivolution));
}
return picojson::value(json_root).serialize(pretty);
}
bool WriteGameModDescriptorFile(const std::string& filename, const GameModDescriptor& descriptor,
bool pretty)
{
auto json = WriteGameModDescriptorString(descriptor, pretty);
if (json.empty())
return false;
::File::IOFile f(filename, "wb");
if (!f)
return false;
if (!f.WriteString(json))
return false;
return true;
}
} // namespace DiscIO

View File

@ -43,4 +43,7 @@ struct GameModDescriptor
std::optional<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename);
std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view json,
std::string_view json_path);
std::string WriteGameModDescriptorString(const GameModDescriptor& descriptor, bool pretty);
bool WriteGameModDescriptorFile(const std::string& filename, const GameModDescriptor& descriptor,
bool pretty);
} // namespace DiscIO

View File

@ -352,16 +352,17 @@ void GameList::ShowContextMenu(const QPoint&)
else
{
const auto game = GetSelectedGame();
const bool is_mod_descriptor = game->IsModDescriptor();
DiscIO::Platform platform = game->GetPlatform();
menu->addAction(tr("&Properties"), this, &GameList::OpenProperties);
if (platform != DiscIO::Platform::ELFOrDOL)
if (!is_mod_descriptor && platform != DiscIO::Platform::ELFOrDOL)
{
menu->addAction(tr("&Wiki"), this, &GameList::OpenWiki);
}
menu->addSeparator();
if (DiscIO::IsDisc(platform))
if (!is_mod_descriptor && DiscIO::IsDisc(platform))
{
menu->addAction(tr("Start with Riivolution Patches..."), this,
&GameList::StartWithRiivolution);
@ -382,7 +383,7 @@ void GameList::ShowContextMenu(const QPoint&)
menu->addSeparator();
}
if (platform == DiscIO::Platform::WiiDisc)
if (!is_mod_descriptor && platform == DiscIO::Platform::WiiDisc)
{
auto* perform_disc_update = menu->addAction(tr("Perform System Update"), this,
[this, file_path = game->GetFilePath()] {
@ -394,7 +395,7 @@ void GameList::ShowContextMenu(const QPoint&)
perform_disc_update->setEnabled(!Core::IsRunning() || !SConfig::GetInstance().bWii);
}
if (platform == DiscIO::Platform::WiiWAD)
if (!is_mod_descriptor && platform == DiscIO::Platform::WiiWAD)
{
QAction* wad_install_action = new QAction(tr("Install to the NAND"), menu);
QAction* wad_uninstall_action = new QAction(tr("Uninstall from the NAND"), menu);
@ -420,14 +421,15 @@ void GameList::ShowContextMenu(const QPoint&)
menu->addSeparator();
}
if (platform == DiscIO::Platform::WiiWAD || platform == DiscIO::Platform::WiiDisc)
if (!is_mod_descriptor &&
(platform == DiscIO::Platform::WiiWAD || platform == DiscIO::Platform::WiiDisc))
{
menu->addAction(tr("Open Wii &Save Folder"), this, &GameList::OpenWiiSaveFolder);
menu->addAction(tr("Export Wii Save"), this, &GameList::ExportWiiSave);
menu->addSeparator();
}
if (platform == DiscIO::Platform::GameCubeDisc)
if (!is_mod_descriptor && platform == DiscIO::Platform::GameCubeDisc)
{
menu->addAction(tr("Open GameCube &Save Folder"), this, &GameList::OpenGCSaveFolder);
menu->addSeparator();

View File

@ -27,7 +27,7 @@ static const QStringList game_filters{
QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"),
QStringLiteral("*.[wW][iI][aA]"), QStringLiteral("*.[rR][vV][zZ]"),
QStringLiteral("*.[wW][aA][dD]"), QStringLiteral("*.[eE][lL][fF]"),
QStringLiteral("*.[dD][oO][lL]")};
QStringLiteral("*.[dD][oO][lL]"), QStringLiteral("*.[jJ][sS][oO][nN]")};
GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent)
{

View File

@ -733,8 +733,10 @@ QStringList MainWindow::PromptFileNames()
QStringList paths = DolphinFileDialog::getOpenFileNames(
this, tr("Select a File"),
settings.value(QStringLiteral("mainwindow/lastdir"), QString{}).toString(),
tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
"*.dff *.m3u);;All Files (*)"));
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
"*.dff *.m3u *.json);;%2 (*)")
.arg(tr("All GC/Wii files"))
.arg(tr("All Files")));
if (!paths.isEmpty())
{
@ -1845,7 +1847,7 @@ void MainWindow::ShowRiivolutionBootWidget(const UICommon::GameFile& game)
auto& disc = std::get<BootParameters::Disc>(boot_params->parameters);
RiivolutionBootWidget w(disc.volume->GetGameID(), disc.volume->GetRevision(),
disc.volume->GetDiscNumber(), this);
disc.volume->GetDiscNumber(), game.GetFilePath(), this);
w.exec();
if (!w.ShouldBoot())
return;

View File

@ -1179,9 +1179,9 @@ void NetPlayDialog::SetChunkedProgress(const int pid, const u64 progress)
});
}
void NetPlayDialog::SetHostWiiSyncTitles(std::vector<u64> titles)
void NetPlayDialog::SetHostWiiSyncData(std::vector<u64> titles, std::string redirect_folder)
{
auto client = Settings::Instance().GetNetPlayClient();
if (client)
client->SetWiiSyncData(nullptr, std::move(titles));
client->SetWiiSyncData(nullptr, std::move(titles), std::move(redirect_folder));
}

View File

@ -95,7 +95,7 @@ public:
void HideChunkedProgressDialog() override;
void SetChunkedProgress(int pid, u64 progress) override;
void SetHostWiiSyncTitles(std::vector<u64> titles) override;
void SetHostWiiSyncData(std::vector<u64> titles, std::string redirect_folder) override;
signals:
void Stop();

View File

@ -23,6 +23,7 @@
#include "Common/FileSearch.h"
#include "Common/FileUtil.h"
#include "DiscIO/GameModDescriptor.h"
#include "DiscIO/RiivolutionParser.h"
#include "DiscIO/RiivolutionPatcher.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
@ -38,8 +39,10 @@ struct GuiRiivolutionPatchIndex
Q_DECLARE_METATYPE(GuiRiivolutionPatchIndex);
RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
std::optional<u8> disc, QWidget* parent)
: QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc)
std::optional<u8> disc, std::string base_game_path,
QWidget* parent)
: QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc),
m_base_game_path(std::move(base_game_path))
{
setWindowTitle(tr("Start with Riivolution Patches"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
@ -57,6 +60,7 @@ void RiivolutionBootWidget::CreateWidgets()
auto* open_xml_button = new QPushButton(tr("Open Riivolution XML..."));
auto* boot_game_button = new QPushButton(tr("Start"));
boot_game_button->setDefault(true);
auto* save_preset_button = new QPushButton(tr("Save as Preset..."));
auto* group_box = new QGroupBox();
auto* scroll_area = new QScrollArea();
@ -71,6 +75,7 @@ void RiivolutionBootWidget::CreateWidgets()
auto* button_layout = new QHBoxLayout();
button_layout->addStretch();
button_layout->addWidget(open_xml_button, 0, Qt::AlignRight);
button_layout->addWidget(save_preset_button, 0, Qt::AlignRight);
button_layout->addWidget(boot_game_button, 0, Qt::AlignRight);
auto* layout = new QVBoxLayout();
@ -80,6 +85,7 @@ void RiivolutionBootWidget::CreateWidgets()
connect(open_xml_button, &QPushButton::clicked, this, &RiivolutionBootWidget::OpenXML);
connect(boot_game_button, &QPushButton::clicked, this, &RiivolutionBootWidget::BootGame);
connect(save_preset_button, &QPushButton::clicked, this, &RiivolutionBootWidget::SaveAsPreset);
}
void RiivolutionBootWidget::LoadMatchingXMLs()
@ -144,13 +150,14 @@ void RiivolutionBootWidget::OpenXML()
}
}
void RiivolutionBootWidget::MakeGUIForParsedFile(const std::string& path, std::string root,
void RiivolutionBootWidget::MakeGUIForParsedFile(std::string path, std::string root,
DiscIO::Riivolution::Disc input_disc)
{
const size_t disc_index = m_discs.size();
const auto& disc = m_discs.emplace_back(DiscWithRoot{std::move(input_disc), std::move(root)});
const auto& disc =
m_discs.emplace_back(DiscWithRoot{std::move(input_disc), std::move(root), std::move(path)});
auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(path)).fileName());
auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(disc.path)).fileName());
auto* disc_layout = new QVBoxLayout();
disc_box->setLayout(disc_layout);
@ -279,3 +286,52 @@ void RiivolutionBootWidget::BootGame()
m_should_boot = true;
close();
}
void RiivolutionBootWidget::SaveAsPreset()
{
DiscIO::GameModDescriptor descriptor;
descriptor.base_file = m_base_game_path;
DiscIO::GameModDescriptorRiivolution riivolution_descriptor;
for (const auto& disc : m_discs)
{
// filter out XMLs that don't actually contribute to the preset
auto patches = disc.disc.GeneratePatches(m_game_id);
if (patches.empty())
continue;
auto& descriptor_patch = riivolution_descriptor.patches.emplace_back();
descriptor_patch.xml = disc.path;
descriptor_patch.root = disc.root;
for (const auto& section : disc.disc.m_sections)
{
for (const auto& option : section.m_options)
{
auto& descriptor_option = descriptor_patch.options.emplace_back();
descriptor_option.section_name = section.m_name;
if (!option.m_id.empty())
descriptor_option.option_id = option.m_id;
else
descriptor_option.option_name = option.m_name;
descriptor_option.choice = option.m_selected_choice;
}
}
}
if (!riivolution_descriptor.patches.empty())
descriptor.riivolution = std::move(riivolution_descriptor);
QDir dir = QFileInfo(QString::fromStdString(m_base_game_path)).dir();
QString target_path = QFileDialog::getSaveFileName(this, tr("Save Preset"), dir.absolutePath(),
QStringLiteral("%1 (*.json);;%2 (*)")
.arg(tr("Dolphin Game Mod Preset"))
.arg(tr("All Files")));
if (target_path.isEmpty())
return;
descriptor.display_name = QFileInfo(target_path).fileName().toStdString();
auto dot = descriptor.display_name.rfind('.');
if (dot != std::string::npos)
descriptor.display_name = descriptor.display_name.substr(0, dot);
DiscIO::WriteGameModDescriptorFile(target_path.toStdString(), descriptor, true);
}

View File

@ -19,7 +19,8 @@ class RiivolutionBootWidget : public QDialog
Q_OBJECT
public:
explicit RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
std::optional<u8> disc, QWidget* parent = nullptr);
std::optional<u8> disc, std::string base_game_path,
QWidget* parent = nullptr);
~RiivolutionBootWidget();
bool ShouldBoot() const { return m_should_boot; }
@ -30,21 +31,24 @@ private:
void LoadMatchingXMLs();
void OpenXML();
void MakeGUIForParsedFile(const std::string& path, std::string root,
void MakeGUIForParsedFile(std::string path, std::string root,
DiscIO::Riivolution::Disc input_disc);
std::optional<DiscIO::Riivolution::Config> LoadConfigXML(const std::string& root_directory);
void SaveConfigXMLs();
void BootGame();
void SaveAsPreset();
std::string m_game_id;
std::optional<u16> m_revision;
std::optional<u8> m_disc_number;
std::string m_base_game_path;
bool m_should_boot = false;
struct DiscWithRoot
{
DiscIO::Riivolution::Disc disc;
std::string root;
std::string path;
};
std::vector<DiscWithRoot> m_discs;
std::vector<DiscIO::Riivolution::Patch> m_patches;

View File

@ -44,8 +44,10 @@ void PathPane::BrowseDefaultGame()
{
QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName(
this, tr("Select a Game"), Settings::Instance().GetDefaultGame(),
tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs "
"*.ciso *.gcz *.wia *.rvz *.wad *.m3u);;All Files (*)")));
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
"*.m3u *.json);;%2 (*)")
.arg(tr("All GC/Wii files"))
.arg(tr("All Files"))));
if (!file.isEmpty())
Settings::Instance().SetDefaultGame(file);

View File

@ -22,6 +22,7 @@
#include <mbedtls/sha1.h>
#include <pugixml.hpp>
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
@ -43,6 +44,7 @@
#include "DiscIO/Blob.h"
#include "DiscIO/DiscExtractor.h"
#include "DiscIO/Enums.h"
#include "DiscIO/GameModDescriptor.h"
#include "DiscIO/Volume.h"
#include "DiscIO/WiiSaveBanner.h"
@ -163,6 +165,32 @@ GameFile::GameFile(std::string path) : m_file_path(std::move(path))
m_platform = DiscIO::Platform::ELFOrDOL;
m_blob_type = DiscIO::BlobType::DIRECTORY;
}
if (!IsValid() && GetExtension() == ".json")
{
auto descriptor = DiscIO::ParseGameModDescriptorFile(m_file_path);
if (descriptor)
{
GameFile proxy(descriptor->base_file);
if (proxy.IsValid())
{
m_valid = true;
m_file_size = File::GetSize(m_file_path);
m_long_names.emplace(DiscIO::Language::English, std::move(descriptor->display_name));
m_internal_name = proxy.GetInternalName();
m_game_id = proxy.GetGameID();
m_gametdb_id = proxy.GetGameTDBID();
m_title_id = proxy.GetTitleID();
m_maker_id = proxy.GetMakerID();
m_region = proxy.GetRegion();
m_country = proxy.GetCountry();
m_platform = proxy.GetPlatform();
m_revision = proxy.GetRevision();
m_disc_number = proxy.GetDiscNumber();
m_blob_type = DiscIO::BlobType::MOD_DESCRIPTOR;
}
}
}
}
GameFile::~GameFile() = default;
@ -470,6 +498,18 @@ bool GameFile::ReadPNGBanner(const std::string& path)
return true;
}
bool GameFile::TryLoadGameModDescriptorBanner()
{
if (m_blob_type != DiscIO::BlobType::MOD_DESCRIPTOR)
return false;
auto descriptor = DiscIO::ParseGameModDescriptorFile(m_file_path);
if (!descriptor)
return false;
return ReadPNGBanner(descriptor->banner);
}
bool GameFile::CustomBannerChanged()
{
std::string path, name;
@ -481,11 +521,15 @@ bool GameFile::CustomBannerChanged()
{
// Homebrew Channel icon naming. Typical for DOLs and ELFs, but we also support it for volumes.
if (!ReadPNGBanner(path + "icon.png"))
{
// If it's a game mod descriptor file, it may specify its own custom banner.
if (!TryLoadGameModDescriptorBanner())
{
// If no custom icon is found, go back to the non-custom one.
m_pending.custom_banner = {};
}
}
}
return m_pending.custom_banner != m_custom_banner;
}
@ -499,6 +543,8 @@ const std::string& GameFile::GetName(const Core::TitleDatabase& title_database)
{
if (!m_custom_name.empty())
return m_custom_name;
if (IsModDescriptor())
return GetName(Variant::LongAndPossiblyCustom);
const std::string& database_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage());
return database_name.empty() ? GetName(Variant::LongAndPossiblyCustom) : database_name;
@ -579,15 +625,75 @@ std::string GameFile::GetNetPlayName(const Core::TitleDatabase& title_database)
return name + " (" + ss.str() + ")";
}
static std::array<u8, 20> GetHash(u32 value)
{
auto data = Common::BitCastToArray<u8>(value);
std::array<u8, 20> hash;
mbedtls_sha1_ret(reinterpret_cast<const unsigned char*>(data.data()), data.size(), hash.data());
return hash;
}
static std::array<u8, 20> GetHash(std::string_view str)
{
std::array<u8, 20> hash;
mbedtls_sha1_ret(reinterpret_cast<const unsigned char*>(str.data()), str.size(), hash.data());
return hash;
}
static std::optional<std::array<u8, 20>> GetFileHash(const std::string& path)
{
std::string buffer;
if (!File::ReadFileToString(path, buffer))
return std::nullopt;
return GetHash(buffer);
}
static std::optional<std::array<u8, 20>> MixHash(const std::optional<std::array<u8, 20>>& lhs,
const std::optional<std::array<u8, 20>>& rhs)
{
if (!lhs && !rhs)
return std::nullopt;
if (!lhs || !rhs)
return !rhs ? lhs : rhs;
std::array<u8, 20> result;
for (size_t i = 0; i < result.size(); ++i)
result[i] = (*lhs)[i] ^ (*rhs)[(i + 1) % result.size()];
return result;
}
std::array<u8, 20> GameFile::GetSyncHash() const
{
std::array<u8, 20> hash{};
std::optional<std::array<u8, 20>> hash;
if (m_platform == DiscIO::Platform::ELFOrDOL)
{
std::string buffer;
if (File::ReadFileToString(m_file_path, buffer))
mbedtls_sha1_ret(reinterpret_cast<unsigned char*>(buffer.data()), buffer.size(), hash.data());
hash = GetFileHash(m_file_path);
}
else if (m_blob_type == DiscIO::BlobType::MOD_DESCRIPTOR)
{
auto descriptor = DiscIO::ParseGameModDescriptorFile(m_file_path);
if (descriptor)
{
GameFile proxy(descriptor->base_file);
if (proxy.IsValid())
hash = proxy.GetSyncHash();
// add patches to hash if they're enabled
if (descriptor->riivolution)
{
for (const auto& patch : descriptor->riivolution->patches)
{
hash = MixHash(hash, GetFileHash(patch.xml));
for (const auto& option : patch.options)
{
hash = MixHash(hash, GetHash(option.section_name));
hash = MixHash(hash, GetHash(option.option_id));
hash = MixHash(hash, GetHash(option.option_name));
hash = MixHash(hash, GetHash(option.choice));
}
}
}
}
}
else
{
@ -595,7 +701,7 @@ std::array<u8, 20> GameFile::GetSyncHash() const
hash = volume->GetSyncHash();
}
return hash;
return hash.value_or(std::array<u8, 20>{});
}
NetPlay::SyncIdentifier GameFile::GetSyncIdentifier() const
@ -652,6 +758,7 @@ bool GameFile::ShouldShowFileFormatDetails() const
case DiscIO::BlobType::PLAIN:
break;
case DiscIO::BlobType::DRIVE:
case DiscIO::BlobType::MOD_DESCRIPTOR:
return false;
default:
return true;
@ -699,6 +806,11 @@ bool GameFile::ShouldAllowConversion() const
return DiscIO::IsDisc(m_platform) && m_volume_size_is_accurate;
}
bool GameFile::IsModDescriptor() const
{
return m_blob_type == DiscIO::BlobType::MOD_DESCRIPTOR;
}
const GameBanner& GameFile::GetBannerImage() const
{
return m_custom_banner.empty() ? m_volume_banner : m_custom_banner;

View File

@ -107,6 +107,7 @@ public:
bool IsVolumeSizeAccurate() const { return m_volume_size_is_accurate; }
bool IsDatelDisc() const { return m_is_datel_disc; }
bool IsNKit() const { return m_is_nkit; }
bool IsModDescriptor() const;
const GameBanner& GetBannerImage() const;
const GameCover& GetCoverImage() const;
void DoState(PointerWrap& p);
@ -132,6 +133,7 @@ private:
bool IsElfOrDol() const;
bool ReadXMLMetadata(const std::string& path);
bool ReadPNGBanner(const std::string& path);
bool TryLoadGameModDescriptorBanner();
// IMPORTANT: Nearly all data members must be save/restored in DoState.
// If anything is changed, make sure DoState handles it properly and

View File

@ -27,13 +27,14 @@
namespace UICommon
{
static constexpr u32 CACHE_REVISION = 20; // Last changed in PR 9461
static constexpr u32 CACHE_REVISION = 21; // Last changed in PR 10187
std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan)
{
static const std::vector<std::string> search_extensions = {
".gcm", ".tgc", ".iso", ".ciso", ".gcz", ".wbfs", ".wia", ".rvz", ".wad", ".dol", ".elf"};
static const std::vector<std::string> search_extensions = {".gcm", ".tgc", ".iso", ".ciso",
".gcz", ".wbfs", ".wia", ".rvz",
".wad", ".dol", ".elf", ".json"};
// TODO: We could process paths iteratively as they are found
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);

View File

@ -0,0 +1,66 @@
{
"$schema": "https://raw.githubusercontent.com/dolphin-emu/dolphin/master/docs/game-mod-descriptor.json",
"title": "Dolphin Game Mod Descriptor",
"type": "object",
"required": ["type", "version", "base-file"],
"properties": {
"type": {
"type": "string",
"pattern": "^dolphin-game-mod-descriptor$"
},
"version": {
"type": "integer"
},
"base-file": {
"type": "string"
},
"display-name": {
"type": "string"
},
"banner": {
"type": "string"
},
"riivolution": {
"type": "object",
"required": ["patches"],
"properties": {
"patches": {
"type": "array",
"items": {
"type": "object",
"required": ["xml", "root", "options"],
"properties": {
"xml": {
"type": "string"
},
"root": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"type": "object",
"required": ["choice"],
"properties": {
"section-name": {
"type": "string"
},
"option-id": {
"type": "string"
},
"option-name": {
"type": "string"
},
"choice": {
"type": "integer"
}
}
}
}
}
}
}
}
}
}
}