mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-08-04 23:14:33 +02:00
Merge pull request #10187 from AdmiralCurtiss/json-gamelist
Support for GameModDescriptor files in Game List.
This commit is contained in:
@@ -30,7 +30,7 @@ import java.util.Set;
|
|||||||
public final class FileBrowserHelper
|
public final class FileBrowserHelper
|
||||||
{
|
{
|
||||||
public static final HashSet<String> GAME_EXTENSIONS = new HashSet<>(Arrays.asList(
|
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);
|
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;
|
return m_wii_sync_titles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string& BootSessionData::GetWiiSyncRedirectFolder() const
|
||||||
|
{
|
||||||
|
return m_wii_sync_redirect_folder;
|
||||||
|
}
|
||||||
|
|
||||||
void BootSessionData::InvokeWiiSyncCleanup() const
|
void BootSessionData::InvokeWiiSyncCleanup() const
|
||||||
{
|
{
|
||||||
if (m_wii_sync_cleanup)
|
if (m_wii_sync_cleanup)
|
||||||
@@ -162,10 +167,12 @@ void BootSessionData::InvokeWiiSyncCleanup() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BootSessionData::SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs,
|
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_fs = std::move(fs);
|
||||||
m_wii_sync_titles = std::move(titles);
|
m_wii_sync_titles = std::move(titles);
|
||||||
|
m_wii_sync_redirect_folder = std::move(redirect_folder);
|
||||||
m_wii_sync_cleanup = std::move(cleanup);
|
m_wii_sync_cleanup = std::move(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,9 +66,10 @@ public:
|
|||||||
|
|
||||||
IOS::HLE::FS::FileSystem* GetWiiSyncFS() const;
|
IOS::HLE::FS::FileSystem* GetWiiSyncFS() const;
|
||||||
const std::vector<u64>& GetWiiSyncTitles() const;
|
const std::vector<u64>& GetWiiSyncTitles() const;
|
||||||
|
const std::string& GetWiiSyncRedirectFolder() const;
|
||||||
void InvokeWiiSyncCleanup() const;
|
void InvokeWiiSyncCleanup() const;
|
||||||
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,
|
||||||
WiiSyncCleanupFunction cleanup);
|
std::string redirect_folder, WiiSyncCleanupFunction cleanup);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::optional<std::string> m_savestate_path;
|
std::optional<std::string> m_savestate_path;
|
||||||
@@ -76,6 +77,7 @@ private:
|
|||||||
|
|
||||||
std::unique_ptr<IOS::HLE::FS::FileSystem> m_wii_sync_fs;
|
std::unique_ptr<IOS::HLE::FS::FileSystem> m_wii_sync_fs;
|
||||||
std::vector<u64> m_wii_sync_titles;
|
std::vector<u64> m_wii_sync_titles;
|
||||||
|
std::string m_wii_sync_redirect_folder;
|
||||||
WiiSyncCleanupFunction m_wii_sync_cleanup;
|
WiiSyncCleanupFunction m_wii_sync_cleanup;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1077,6 +1077,7 @@ void NetPlayClient::OnSyncSaveDataGCI(sf::Packet& packet)
|
|||||||
void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet)
|
void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet)
|
||||||
{
|
{
|
||||||
const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP;
|
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))
|
if (File::Exists(path) && !File::DeleteDirRecursively(path))
|
||||||
{
|
{
|
||||||
@@ -1084,6 +1085,12 @@ void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet)
|
|||||||
SyncSaveDataResponse(false);
|
SyncSaveDataResponse(false);
|
||||||
return;
|
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);
|
auto temp_fs = std::make_unique<IOS::HLE::FS::HostFileSystem>(path);
|
||||||
std::vector<u64> titles;
|
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);
|
SyncSaveDataResponse(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1721,12 +1740,18 @@ bool NetPlayClient::StartGame(const std::string& path)
|
|||||||
|
|
||||||
// boot game
|
// boot game
|
||||||
auto boot_session_data = std::make_unique<BootSessionData>();
|
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(
|
||||||
// on emulation end clean up the Wii save sync directory -- see OnSyncSaveDataWii()
|
std::move(m_wii_sync_fs), std::move(m_wii_sync_titles), std::move(m_wii_sync_redirect_folder),
|
||||||
const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP;
|
[] {
|
||||||
if (File::Exists(path))
|
// on emulation end clean up the Wii save sync directory -- see OnSyncSaveDataWii()
|
||||||
File::DeleteDirRecursively(path);
|
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));
|
m_dialog->BootGame(path, std::move(boot_session_data));
|
||||||
|
|
||||||
UpdateDevices();
|
UpdateDevices();
|
||||||
@@ -2501,10 +2526,11 @@ void NetPlayClient::AdjustPadBufferSize(const unsigned int size)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetPlayClient::SetWiiSyncData(std::unique_ptr<IOS::HLE::FS::FileSystem> fs,
|
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_fs = std::move(fs);
|
||||||
m_wii_sync_titles = std::move(titles);
|
m_wii_sync_titles = std::move(titles);
|
||||||
|
m_wii_sync_redirect_folder = std::move(redirect_folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncIdentifier NetPlayClient::GetSDCardIdentifier()
|
SyncIdentifier NetPlayClient::GetSDCardIdentifier()
|
||||||
|
@@ -86,7 +86,7 @@ public:
|
|||||||
virtual void HideChunkedProgressDialog() = 0;
|
virtual void HideChunkedProgressDialog() = 0;
|
||||||
virtual void SetChunkedProgress(int pid, u64 progress) = 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
|
class Player
|
||||||
@@ -157,7 +157,8 @@ public:
|
|||||||
|
|
||||||
void AdjustPadBufferSize(unsigned int size);
|
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();
|
static SyncIdentifier GetSDCardIdentifier();
|
||||||
|
|
||||||
@@ -328,6 +329,7 @@ private:
|
|||||||
|
|
||||||
std::unique_ptr<IOS::HLE::FS::FileSystem> m_wii_sync_fs;
|
std::unique_ptr<IOS::HLE::FS::FileSystem> m_wii_sync_fs;
|
||||||
std::vector<u64> m_wii_sync_titles;
|
std::vector<u64> m_wii_sync_titles;
|
||||||
|
std::string m_wii_sync_redirect_folder;
|
||||||
};
|
};
|
||||||
|
|
||||||
void NetPlay_Enable(NetPlayClient* const np);
|
void NetPlay_Enable(NetPlayClient* const np);
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "Core/NetPlayCommon.h"
|
#include "Core/NetPlayCommon.h"
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <lzo/lzo1x.h>
|
#include <lzo/lzo1x.h>
|
||||||
|
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
@@ -84,6 +85,35 @@ bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet)
|
|||||||
return true;
|
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)
|
bool CompressBufferIntoPacket(const std::vector<u8>& in_buffer, sf::Packet& packet)
|
||||||
{
|
{
|
||||||
const sf::Uint64 size = in_buffer.size();
|
const sf::Uint64 size = in_buffer.size();
|
||||||
@@ -187,6 +217,47 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path)
|
|||||||
return true;
|
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)
|
std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet)
|
||||||
{
|
{
|
||||||
u64 size = Common::PacketReadU64(packet);
|
u64 size = Common::PacketReadU64(packet);
|
||||||
|
@@ -15,7 +15,9 @@
|
|||||||
namespace NetPlay
|
namespace NetPlay
|
||||||
{
|
{
|
||||||
bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet);
|
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 CompressBufferIntoPacket(const std::vector<u8>& in_buffer, sf::Packet& packet);
|
||||||
bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path);
|
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);
|
std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet);
|
||||||
} // namespace NetPlay
|
} // namespace NetPlay
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
#include "Common/Version.h"
|
#include "Common/Version.h"
|
||||||
|
|
||||||
#include "Core/ActionReplay.h"
|
#include "Core/ActionReplay.h"
|
||||||
|
#include "Core/Boot/Boot.h"
|
||||||
#include "Core/Config/GraphicsSettings.h"
|
#include "Core/Config/GraphicsSettings.h"
|
||||||
#include "Core/Config/MainSettings.h"
|
#include "Core/Config/MainSettings.h"
|
||||||
#include "Core/Config/NetplaySettings.h"
|
#include "Core/Config/NetplaySettings.h"
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
#include "Core/SyncIdentifier.h"
|
#include "Core/SyncIdentifier.h"
|
||||||
|
|
||||||
#include "DiscIO/Enums.h"
|
#include "DiscIO/Enums.h"
|
||||||
|
#include "DiscIO/RiivolutionPatcher.h"
|
||||||
|
|
||||||
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
|
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
|
||||||
#include "InputCommon/GCPadStatus.h"
|
#include "InputCommon/GCPadStatus.h"
|
||||||
@@ -1616,6 +1618,17 @@ bool NetPlayServer::SyncSaveData()
|
|||||||
save_count++;
|
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)
|
for (const auto& config : m_gba_config)
|
||||||
{
|
{
|
||||||
if (config.enabled && config.has_rom)
|
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
|
// 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");
|
SendChunkedToClients(std::move(pac), 1, "Wii Save Synchronization");
|
||||||
}
|
}
|
||||||
|
@@ -35,9 +35,19 @@ namespace Core
|
|||||||
namespace FS = IOS::HLE::FS;
|
namespace FS = IOS::HLE::FS;
|
||||||
|
|
||||||
static std::string s_temp_wii_root;
|
static std::string s_temp_wii_root;
|
||||||
|
static std::string s_temp_redirect_root;
|
||||||
static bool s_wii_root_initialized = false;
|
static bool s_wii_root_initialized = false;
|
||||||
static std::vector<IOS::HLE::FS::NandRedirect> s_nand_redirects;
|
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()
|
const std::vector<IOS::HLE::FS::NandRedirect>& GetActiveNandRedirects()
|
||||||
{
|
{
|
||||||
return s_nand_redirects;
|
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");
|
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)
|
if (use_temporary)
|
||||||
{
|
{
|
||||||
s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP;
|
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 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 directory exists, make a backup
|
||||||
if (File::Exists(s_temp_wii_root))
|
MoveToBackupIfExists(s_temp_wii_root);
|
||||||
{
|
MoveToBackupIfExists(s_temp_redirect_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);
|
|
||||||
}
|
|
||||||
|
|
||||||
File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root);
|
File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root);
|
||||||
}
|
}
|
||||||
@@ -221,6 +242,9 @@ void ShutdownWiiRoot()
|
|||||||
{
|
{
|
||||||
File::DeleteDirRecursively(s_temp_wii_root);
|
File::DeleteDirRecursively(s_temp_wii_root);
|
||||||
s_temp_wii_root.clear();
|
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();
|
s_nand_redirects.clear();
|
||||||
@@ -312,7 +336,8 @@ void InitializeWiiFileSystemContents(
|
|||||||
if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, ""))
|
if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, ""))
|
||||||
WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND");
|
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.
|
// Generate a SYSCONF with default settings for the temporary Wii NAND.
|
||||||
SysConf sysconf{fs};
|
SysConf sysconf{fs};
|
||||||
@@ -320,16 +345,26 @@ void InitializeWiiFileSystemContents(
|
|||||||
|
|
||||||
InitializeDeterministicWiiSaves(fs.get(), boot_session_data);
|
InitializeDeterministicWiiSaves(fs.get(), boot_session_data);
|
||||||
}
|
}
|
||||||
else if (save_redirect)
|
|
||||||
|
if (save_redirect)
|
||||||
{
|
{
|
||||||
const u64 title_id = SConfig::GetInstance().GetTitleID();
|
const u64 title_id = SConfig::GetInstance().GetTitleID();
|
||||||
std::string source_path = Common::GetTitleDataPath(title_id);
|
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))
|
if (!File::IsDirectory(save_redirect->m_target_path))
|
||||||
{
|
{
|
||||||
File::CreateFullPath(save_redirect->m_target_path + "/");
|
File::CreateFullPath(save_redirect->m_target_path + "/");
|
||||||
if (save_redirect->m_clone)
|
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);
|
save_redirect->m_target_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,7 +382,16 @@ void CleanUpWiiFileSystemContents(const BootSessionData& boot_session_data)
|
|||||||
return;
|
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();
|
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);
|
const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured);
|
||||||
|
|
||||||
// Copy back Mii data
|
// Copy back Mii data
|
||||||
|
@@ -50,6 +50,8 @@ std::string GetName(BlobType blob_type, bool translate)
|
|||||||
return "WIA";
|
return "WIA";
|
||||||
case BlobType::RVZ:
|
case BlobType::RVZ:
|
||||||
return "RVZ";
|
return "RVZ";
|
||||||
|
case BlobType::MOD_DESCRIPTOR:
|
||||||
|
return translate_str("Mod");
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@@ -39,6 +39,7 @@ enum class BlobType
|
|||||||
TGC,
|
TGC,
|
||||||
WIA,
|
WIA,
|
||||||
RVZ,
|
RVZ,
|
||||||
|
MOD_DESCRIPTOR,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string GetName(BlobType blob_type, bool translate);
|
std::string GetName(BlobType blob_type, bool translate);
|
||||||
|
@@ -115,10 +115,15 @@ std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view j
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
GameModDescriptor descriptor;
|
GameModDescriptor descriptor;
|
||||||
|
bool is_game_mod_descriptor = false;
|
||||||
bool is_valid_version = false;
|
bool is_valid_version = false;
|
||||||
for (const auto& [key, value] : json_root.get<picojson::object>())
|
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;
|
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>());
|
ParseRiivolutionObject(json_directory, value.get<picojson::object>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!is_valid_version)
|
if (!is_game_mod_descriptor || !is_valid_version)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
return descriptor;
|
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
|
} // namespace DiscIO
|
||||||
|
@@ -43,4 +43,7 @@ struct GameModDescriptor
|
|||||||
std::optional<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename);
|
std::optional<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename);
|
||||||
std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view json,
|
std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view json,
|
||||||
std::string_view json_path);
|
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
|
} // namespace DiscIO
|
||||||
|
@@ -352,16 +352,17 @@ void GameList::ShowContextMenu(const QPoint&)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
const auto game = GetSelectedGame();
|
const auto game = GetSelectedGame();
|
||||||
|
const bool is_mod_descriptor = game->IsModDescriptor();
|
||||||
DiscIO::Platform platform = game->GetPlatform();
|
DiscIO::Platform platform = game->GetPlatform();
|
||||||
menu->addAction(tr("&Properties"), this, &GameList::OpenProperties);
|
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->addAction(tr("&Wiki"), this, &GameList::OpenWiki);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
|
|
||||||
if (DiscIO::IsDisc(platform))
|
if (!is_mod_descriptor && DiscIO::IsDisc(platform))
|
||||||
{
|
{
|
||||||
menu->addAction(tr("Start with Riivolution Patches..."), this,
|
menu->addAction(tr("Start with Riivolution Patches..."), this,
|
||||||
&GameList::StartWithRiivolution);
|
&GameList::StartWithRiivolution);
|
||||||
@@ -382,7 +383,7 @@ void GameList::ShowContextMenu(const QPoint&)
|
|||||||
menu->addSeparator();
|
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,
|
auto* perform_disc_update = menu->addAction(tr("Perform System Update"), this,
|
||||||
[this, file_path = game->GetFilePath()] {
|
[this, file_path = game->GetFilePath()] {
|
||||||
@@ -394,7 +395,7 @@ void GameList::ShowContextMenu(const QPoint&)
|
|||||||
perform_disc_update->setEnabled(!Core::IsRunning() || !SConfig::GetInstance().bWii);
|
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_install_action = new QAction(tr("Install to the NAND"), menu);
|
||||||
QAction* wad_uninstall_action = new QAction(tr("Uninstall from 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();
|
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("Open Wii &Save Folder"), this, &GameList::OpenWiiSaveFolder);
|
||||||
menu->addAction(tr("Export Wii Save"), this, &GameList::ExportWiiSave);
|
menu->addAction(tr("Export Wii Save"), this, &GameList::ExportWiiSave);
|
||||||
menu->addSeparator();
|
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->addAction(tr("Open GameCube &Save Folder"), this, &GameList::OpenGCSaveFolder);
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
|
@@ -27,7 +27,7 @@ static const QStringList game_filters{
|
|||||||
QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"),
|
QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"),
|
||||||
QStringLiteral("*.[wW][iI][aA]"), QStringLiteral("*.[rR][vV][zZ]"),
|
QStringLiteral("*.[wW][iI][aA]"), QStringLiteral("*.[rR][vV][zZ]"),
|
||||||
QStringLiteral("*.[wW][aA][dD]"), QStringLiteral("*.[eE][lL][fF]"),
|
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)
|
GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent)
|
||||||
{
|
{
|
||||||
|
@@ -733,8 +733,10 @@ QStringList MainWindow::PromptFileNames()
|
|||||||
QStringList paths = DolphinFileDialog::getOpenFileNames(
|
QStringList paths = DolphinFileDialog::getOpenFileNames(
|
||||||
this, tr("Select a File"),
|
this, tr("Select a File"),
|
||||||
settings.value(QStringLiteral("mainwindow/lastdir"), QString{}).toString(),
|
settings.value(QStringLiteral("mainwindow/lastdir"), QString{}).toString(),
|
||||||
tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
|
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
|
||||||
"*.dff *.m3u);;All Files (*)"));
|
"*.dff *.m3u *.json);;%2 (*)")
|
||||||
|
.arg(tr("All GC/Wii files"))
|
||||||
|
.arg(tr("All Files")));
|
||||||
|
|
||||||
if (!paths.isEmpty())
|
if (!paths.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -1845,7 +1847,7 @@ void MainWindow::ShowRiivolutionBootWidget(const UICommon::GameFile& game)
|
|||||||
|
|
||||||
auto& disc = std::get<BootParameters::Disc>(boot_params->parameters);
|
auto& disc = std::get<BootParameters::Disc>(boot_params->parameters);
|
||||||
RiivolutionBootWidget w(disc.volume->GetGameID(), disc.volume->GetRevision(),
|
RiivolutionBootWidget w(disc.volume->GetGameID(), disc.volume->GetRevision(),
|
||||||
disc.volume->GetDiscNumber(), this);
|
disc.volume->GetDiscNumber(), game.GetFilePath(), this);
|
||||||
w.exec();
|
w.exec();
|
||||||
if (!w.ShouldBoot())
|
if (!w.ShouldBoot())
|
||||||
return;
|
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();
|
auto client = Settings::Instance().GetNetPlayClient();
|
||||||
if (client)
|
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 HideChunkedProgressDialog() override;
|
||||||
void SetChunkedProgress(int pid, u64 progress) 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:
|
signals:
|
||||||
void Stop();
|
void Stop();
|
||||||
|
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include "Common/FileSearch.h"
|
#include "Common/FileSearch.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
|
#include "DiscIO/GameModDescriptor.h"
|
||||||
#include "DiscIO/RiivolutionParser.h"
|
#include "DiscIO/RiivolutionParser.h"
|
||||||
#include "DiscIO/RiivolutionPatcher.h"
|
#include "DiscIO/RiivolutionPatcher.h"
|
||||||
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
||||||
@@ -38,8 +39,10 @@ struct GuiRiivolutionPatchIndex
|
|||||||
Q_DECLARE_METATYPE(GuiRiivolutionPatchIndex);
|
Q_DECLARE_METATYPE(GuiRiivolutionPatchIndex);
|
||||||
|
|
||||||
RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
|
RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
|
||||||
std::optional<u8> disc, QWidget* parent)
|
std::optional<u8> disc, std::string base_game_path,
|
||||||
: QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc)
|
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"));
|
setWindowTitle(tr("Start with Riivolution Patches"));
|
||||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||||
@@ -57,6 +60,7 @@ void RiivolutionBootWidget::CreateWidgets()
|
|||||||
auto* open_xml_button = new QPushButton(tr("Open Riivolution XML..."));
|
auto* open_xml_button = new QPushButton(tr("Open Riivolution XML..."));
|
||||||
auto* boot_game_button = new QPushButton(tr("Start"));
|
auto* boot_game_button = new QPushButton(tr("Start"));
|
||||||
boot_game_button->setDefault(true);
|
boot_game_button->setDefault(true);
|
||||||
|
auto* save_preset_button = new QPushButton(tr("Save as Preset..."));
|
||||||
auto* group_box = new QGroupBox();
|
auto* group_box = new QGroupBox();
|
||||||
auto* scroll_area = new QScrollArea();
|
auto* scroll_area = new QScrollArea();
|
||||||
|
|
||||||
@@ -71,6 +75,7 @@ void RiivolutionBootWidget::CreateWidgets()
|
|||||||
auto* button_layout = new QHBoxLayout();
|
auto* button_layout = new QHBoxLayout();
|
||||||
button_layout->addStretch();
|
button_layout->addStretch();
|
||||||
button_layout->addWidget(open_xml_button, 0, Qt::AlignRight);
|
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);
|
button_layout->addWidget(boot_game_button, 0, Qt::AlignRight);
|
||||||
|
|
||||||
auto* layout = new QVBoxLayout();
|
auto* layout = new QVBoxLayout();
|
||||||
@@ -80,6 +85,7 @@ void RiivolutionBootWidget::CreateWidgets()
|
|||||||
|
|
||||||
connect(open_xml_button, &QPushButton::clicked, this, &RiivolutionBootWidget::OpenXML);
|
connect(open_xml_button, &QPushButton::clicked, this, &RiivolutionBootWidget::OpenXML);
|
||||||
connect(boot_game_button, &QPushButton::clicked, this, &RiivolutionBootWidget::BootGame);
|
connect(boot_game_button, &QPushButton::clicked, this, &RiivolutionBootWidget::BootGame);
|
||||||
|
connect(save_preset_button, &QPushButton::clicked, this, &RiivolutionBootWidget::SaveAsPreset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RiivolutionBootWidget::LoadMatchingXMLs()
|
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)
|
DiscIO::Riivolution::Disc input_disc)
|
||||||
{
|
{
|
||||||
const size_t disc_index = m_discs.size();
|
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();
|
auto* disc_layout = new QVBoxLayout();
|
||||||
disc_box->setLayout(disc_layout);
|
disc_box->setLayout(disc_layout);
|
||||||
|
|
||||||
@@ -279,3 +286,52 @@ void RiivolutionBootWidget::BootGame()
|
|||||||
m_should_boot = true;
|
m_should_boot = true;
|
||||||
close();
|
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
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
|
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();
|
~RiivolutionBootWidget();
|
||||||
|
|
||||||
bool ShouldBoot() const { return m_should_boot; }
|
bool ShouldBoot() const { return m_should_boot; }
|
||||||
@@ -30,21 +31,24 @@ private:
|
|||||||
|
|
||||||
void LoadMatchingXMLs();
|
void LoadMatchingXMLs();
|
||||||
void OpenXML();
|
void OpenXML();
|
||||||
void MakeGUIForParsedFile(const std::string& path, std::string root,
|
void MakeGUIForParsedFile(std::string path, std::string root,
|
||||||
DiscIO::Riivolution::Disc input_disc);
|
DiscIO::Riivolution::Disc input_disc);
|
||||||
std::optional<DiscIO::Riivolution::Config> LoadConfigXML(const std::string& root_directory);
|
std::optional<DiscIO::Riivolution::Config> LoadConfigXML(const std::string& root_directory);
|
||||||
void SaveConfigXMLs();
|
void SaveConfigXMLs();
|
||||||
void BootGame();
|
void BootGame();
|
||||||
|
void SaveAsPreset();
|
||||||
|
|
||||||
std::string m_game_id;
|
std::string m_game_id;
|
||||||
std::optional<u16> m_revision;
|
std::optional<u16> m_revision;
|
||||||
std::optional<u8> m_disc_number;
|
std::optional<u8> m_disc_number;
|
||||||
|
std::string m_base_game_path;
|
||||||
|
|
||||||
bool m_should_boot = false;
|
bool m_should_boot = false;
|
||||||
struct DiscWithRoot
|
struct DiscWithRoot
|
||||||
{
|
{
|
||||||
DiscIO::Riivolution::Disc disc;
|
DiscIO::Riivolution::Disc disc;
|
||||||
std::string root;
|
std::string root;
|
||||||
|
std::string path;
|
||||||
};
|
};
|
||||||
std::vector<DiscWithRoot> m_discs;
|
std::vector<DiscWithRoot> m_discs;
|
||||||
std::vector<DiscIO::Riivolution::Patch> m_patches;
|
std::vector<DiscIO::Riivolution::Patch> m_patches;
|
||||||
|
@@ -44,8 +44,10 @@ void PathPane::BrowseDefaultGame()
|
|||||||
{
|
{
|
||||||
QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName(
|
QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName(
|
||||||
this, tr("Select a Game"), Settings::Instance().GetDefaultGame(),
|
this, tr("Select a Game"), Settings::Instance().GetDefaultGame(),
|
||||||
tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs "
|
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
|
||||||
"*.ciso *.gcz *.wia *.rvz *.wad *.m3u);;All Files (*)")));
|
"*.m3u *.json);;%2 (*)")
|
||||||
|
.arg(tr("All GC/Wii files"))
|
||||||
|
.arg(tr("All Files"))));
|
||||||
|
|
||||||
if (!file.isEmpty())
|
if (!file.isEmpty())
|
||||||
Settings::Instance().SetDefaultGame(file);
|
Settings::Instance().SetDefaultGame(file);
|
||||||
|
@@ -22,6 +22,7 @@
|
|||||||
#include <mbedtls/sha1.h>
|
#include <mbedtls/sha1.h>
|
||||||
#include <pugixml.hpp>
|
#include <pugixml.hpp>
|
||||||
|
|
||||||
|
#include "Common/BitUtils.h"
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
#include "Common/CommonPaths.h"
|
#include "Common/CommonPaths.h"
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
#include "DiscIO/Blob.h"
|
#include "DiscIO/Blob.h"
|
||||||
#include "DiscIO/DiscExtractor.h"
|
#include "DiscIO/DiscExtractor.h"
|
||||||
#include "DiscIO/Enums.h"
|
#include "DiscIO/Enums.h"
|
||||||
|
#include "DiscIO/GameModDescriptor.h"
|
||||||
#include "DiscIO/Volume.h"
|
#include "DiscIO/Volume.h"
|
||||||
#include "DiscIO/WiiSaveBanner.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_platform = DiscIO::Platform::ELFOrDOL;
|
||||||
m_blob_type = DiscIO::BlobType::DIRECTORY;
|
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;
|
GameFile::~GameFile() = default;
|
||||||
@@ -470,6 +498,18 @@ bool GameFile::ReadPNGBanner(const std::string& path)
|
|||||||
return true;
|
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()
|
bool GameFile::CustomBannerChanged()
|
||||||
{
|
{
|
||||||
std::string path, name;
|
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.
|
// Homebrew Channel icon naming. Typical for DOLs and ELFs, but we also support it for volumes.
|
||||||
if (!ReadPNGBanner(path + "icon.png"))
|
if (!ReadPNGBanner(path + "icon.png"))
|
||||||
{
|
{
|
||||||
// If no custom icon is found, go back to the non-custom one.
|
// If it's a game mod descriptor file, it may specify its own custom banner.
|
||||||
m_pending.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())
|
if (!m_custom_name.empty())
|
||||||
return m_custom_name;
|
return m_custom_name;
|
||||||
|
if (IsModDescriptor())
|
||||||
|
return GetName(Variant::LongAndPossiblyCustom);
|
||||||
|
|
||||||
const std::string& database_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage());
|
const std::string& database_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage());
|
||||||
return database_name.empty() ? GetName(Variant::LongAndPossiblyCustom) : database_name;
|
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() + ")";
|
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> GameFile::GetSyncHash() const
|
||||||
{
|
{
|
||||||
std::array<u8, 20> hash{};
|
std::optional<std::array<u8, 20>> hash;
|
||||||
|
|
||||||
if (m_platform == DiscIO::Platform::ELFOrDOL)
|
if (m_platform == DiscIO::Platform::ELFOrDOL)
|
||||||
{
|
{
|
||||||
std::string buffer;
|
hash = GetFileHash(m_file_path);
|
||||||
if (File::ReadFileToString(m_file_path, buffer))
|
}
|
||||||
mbedtls_sha1_ret(reinterpret_cast<unsigned char*>(buffer.data()), buffer.size(), hash.data());
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -595,7 +701,7 @@ std::array<u8, 20> GameFile::GetSyncHash() const
|
|||||||
hash = volume->GetSyncHash();
|
hash = volume->GetSyncHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash;
|
return hash.value_or(std::array<u8, 20>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
NetPlay::SyncIdentifier GameFile::GetSyncIdentifier() const
|
NetPlay::SyncIdentifier GameFile::GetSyncIdentifier() const
|
||||||
@@ -652,6 +758,7 @@ bool GameFile::ShouldShowFileFormatDetails() const
|
|||||||
case DiscIO::BlobType::PLAIN:
|
case DiscIO::BlobType::PLAIN:
|
||||||
break;
|
break;
|
||||||
case DiscIO::BlobType::DRIVE:
|
case DiscIO::BlobType::DRIVE:
|
||||||
|
case DiscIO::BlobType::MOD_DESCRIPTOR:
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
@@ -699,6 +806,11 @@ bool GameFile::ShouldAllowConversion() const
|
|||||||
return DiscIO::IsDisc(m_platform) && m_volume_size_is_accurate;
|
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
|
const GameBanner& GameFile::GetBannerImage() const
|
||||||
{
|
{
|
||||||
return m_custom_banner.empty() ? m_volume_banner : m_custom_banner;
|
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 IsVolumeSizeAccurate() const { return m_volume_size_is_accurate; }
|
||||||
bool IsDatelDisc() const { return m_is_datel_disc; }
|
bool IsDatelDisc() const { return m_is_datel_disc; }
|
||||||
bool IsNKit() const { return m_is_nkit; }
|
bool IsNKit() const { return m_is_nkit; }
|
||||||
|
bool IsModDescriptor() const;
|
||||||
const GameBanner& GetBannerImage() const;
|
const GameBanner& GetBannerImage() const;
|
||||||
const GameCover& GetCoverImage() const;
|
const GameCover& GetCoverImage() const;
|
||||||
void DoState(PointerWrap& p);
|
void DoState(PointerWrap& p);
|
||||||
@@ -132,6 +133,7 @@ private:
|
|||||||
bool IsElfOrDol() const;
|
bool IsElfOrDol() const;
|
||||||
bool ReadXMLMetadata(const std::string& path);
|
bool ReadXMLMetadata(const std::string& path);
|
||||||
bool ReadPNGBanner(const std::string& path);
|
bool ReadPNGBanner(const std::string& path);
|
||||||
|
bool TryLoadGameModDescriptorBanner();
|
||||||
|
|
||||||
// IMPORTANT: Nearly all data members must be save/restored in DoState.
|
// IMPORTANT: Nearly all data members must be save/restored in DoState.
|
||||||
// If anything is changed, make sure DoState handles it properly and
|
// If anything is changed, make sure DoState handles it properly and
|
||||||
|
@@ -27,13 +27,14 @@
|
|||||||
|
|
||||||
namespace UICommon
|
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,
|
std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
|
||||||
bool recursive_scan)
|
bool recursive_scan)
|
||||||
{
|
{
|
||||||
static const std::vector<std::string> search_extensions = {
|
static const std::vector<std::string> search_extensions = {".gcm", ".tgc", ".iso", ".ciso",
|
||||||
".gcm", ".tgc", ".iso", ".ciso", ".gcz", ".wbfs", ".wia", ".rvz", ".wad", ".dol", ".elf"};
|
".gcz", ".wbfs", ".wia", ".rvz",
|
||||||
|
".wad", ".dol", ".elf", ".json"};
|
||||||
|
|
||||||
// TODO: We could process paths iteratively as they are found
|
// TODO: We could process paths iteratively as they are found
|
||||||
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);
|
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);
|
||||||
|
66
docs/game-mod-descriptor.json
Normal file
66
docs/game-mod-descriptor.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user