Merge pull request #10187 from AdmiralCurtiss/json-gamelist
Support for GameModDescriptor files in Game List.
This commit is contained in:
commit
185475fe03
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,12 +1740,18 @@ 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), [] {
|
||||
// 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);
|
||||
});
|
||||
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));
|
||||
|
||||
UpdateDevices();
|
||||
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ enum class BlobType
|
|||
TGC,
|
||||
WIA,
|
||||
RVZ,
|
||||
MOD_DESCRIPTOR,
|
||||
};
|
||||
|
||||
std::string GetName(BlobType blob_type, bool translate);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
@ -482,8 +522,12 @@ 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 no custom icon is found, go back to the non-custom one.
|
||||
m_pending.custom_banner = {};
|
||||
// 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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue