From 4407854e9cbffeb8da1d7a8e70186b6d7498bccb Mon Sep 17 00:00:00 2001 From: Techjar Date: Wed, 4 Jul 2018 17:01:50 -0400 Subject: [PATCH] NetPlay save data synchronization This adds the functionality of sending the host's save data (raw memory cards, as well as GCI files and Wii saves with a matching GameID) to all other clients. The data is compressed using LZO1X to greatly reduce its size while keeping compression/decompression fast. Save synchronization is enabled by default, and toggleable with a checkbox in the NetPlay dialog. On clicking start, if the option is enabled, game boot will be delayed until all players have received the save data sent by the host. If any player fails to receive it properly, boot will be cancelled to prevent desyncs. --- Source/Core/Common/CMakeLists.txt | 1 + Source/Core/Common/Common.vcxproj | 2 + Source/Core/Common/Common.vcxproj.filters | 2 + Source/Core/Common/CommonPaths.h | 1 + Source/Core/Common/SFMLHelper.cpp | 38 ++ Source/Core/Common/SFMLHelper.h | 23 + Source/Core/Core/Config/MainSettings.cpp | 6 + Source/Core/Core/Config/MainSettings.h | 3 + .../ConfigLoaders/NetPlayConfigLoader.cpp | 19 + Source/Core/Core/ConfigManager.cpp | 54 --- Source/Core/Core/ConfigManager.h | 3 - Source/Core/Core/Core.vcxproj | 1 + Source/Core/Core/Core.vcxproj.filters | 3 + .../Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp | 51 ++- .../Core/HW/GCMemcard/GCMemcardDirectory.cpp | 62 ++- .../Core/HW/GCMemcard/GCMemcardDirectory.h | 2 + .../Core/Core/HW/GCMemcard/GCMemcardRaw.cpp | 49 +++ Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h | 1 + Source/Core/Core/HW/WiiSave.cpp | 91 +--- Source/Core/Core/HW/WiiSaveStructs.h | 112 +++++ Source/Core/Core/Movie.cpp | 4 +- Source/Core/Core/NetPlayClient.cpp | 349 +++++++++++++++- Source/Core/Core/NetPlayClient.h | 18 + Source/Core/Core/NetPlayProto.h | 25 +- Source/Core/Core/NetPlayServer.cpp | 393 +++++++++++++++++- Source/Core/Core/NetPlayServer.h | 7 + Source/Core/Core/WiiRoot.cpp | 48 ++- Source/Core/Core/WiiRoot.h | 2 +- Source/Core/DolphinQt/MainWindow.cpp | 3 + .../Core/DolphinQt/NetPlay/NetPlayDialog.cpp | 66 ++- Source/Core/DolphinQt/NetPlay/NetPlayDialog.h | 6 + .../Core/DolphinQt/Settings/GameCubePane.cpp | 14 +- 32 files changed, 1250 insertions(+), 209 deletions(-) create mode 100644 Source/Core/Common/SFMLHelper.cpp create mode 100644 Source/Core/Common/SFMLHelper.h create mode 100644 Source/Core/Core/HW/WiiSaveStructs.h diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index f93bd67efb..d7327a2a47 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(common QoSSession.cpp Random.cpp SDCardUtil.cpp + SFMLHelper.cpp SettingsHandler.cpp StringUtil.cpp SymbolDB.cpp diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 2a38dd721c..9939e0ccd3 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -147,6 +147,7 @@ + @@ -210,6 +211,7 @@ + diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters index 76793bd9ae..3737a5a1ff 100644 --- a/Source/Core/Common/Common.vcxproj.filters +++ b/Source/Core/Common/Common.vcxproj.filters @@ -68,6 +68,7 @@ + @@ -299,6 +300,7 @@ + diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index 2255b594f5..3022e7c5b6 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -109,6 +109,7 @@ #define GC_SRAM "SRAM.raw" #define GC_MEMCARDA "MemoryCardA" #define GC_MEMCARDB "MemoryCardB" +#define GC_MEMCARD_NETPLAY "NetPlayTemp" #define WII_STATE "state.dat" diff --git a/Source/Core/Common/SFMLHelper.cpp b/Source/Core/Common/SFMLHelper.cpp new file mode 100644 index 0000000000..367bd93a9e --- /dev/null +++ b/Source/Core/Common/SFMLHelper.cpp @@ -0,0 +1,38 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "Common/SFMLHelper.h" + +#include + +namespace Common +{ +// This only exists as a helper for BigEndianValue +u16 PacketReadU16(sf::Packet& packet) +{ + u16 tmp; + packet >> tmp; + return tmp; +} + +// This only exists as a helper for BigEndianValue +u32 PacketReadU32(sf::Packet& packet) +{ + u32 tmp; + packet >> tmp; + return tmp; +} + +u64 PacketReadU64(sf::Packet& packet) +{ + u32 low, high; + packet >> low >> high; + return low | (static_cast(high) << 32); +} + +void PacketWriteU64(sf::Packet& packet, const u64 value) +{ + packet << static_cast(value) << static_cast(value >> 32); +} +} // namespace Common diff --git a/Source/Core/Common/SFMLHelper.h b/Source/Core/Common/SFMLHelper.h new file mode 100644 index 0000000000..459af6203b --- /dev/null +++ b/Source/Core/Common/SFMLHelper.h @@ -0,0 +1,23 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include "Common/CommonTypes.h" + +namespace sf +{ +class Packet; +} + +namespace Common +{ +template +struct BigEndianValue; + +u16 PacketReadU16(sf::Packet& packet); +u32 PacketReadU32(sf::Packet& packet); +u64 PacketReadU64(sf::Packet& packet); +void PacketWriteU64(sf::Packet& packet, u64 value); +} // namespace Common diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 8f0c32252b..027c4916a2 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -36,6 +36,12 @@ const ConfigInfo MAIN_MEMCARD_A_PATH{{System::Main, "Core", "Memcar const ConfigInfo MAIN_MEMCARD_B_PATH{{System::Main, "Core", "MemcardBPath"}, ""}; const ConfigInfo MAIN_AGP_CART_A_PATH{{System::Main, "Core", "AgpCartAPath"}, ""}; const ConfigInfo MAIN_AGP_CART_B_PATH{{System::Main, "Core", "AgpCartBPath"}, ""}; +const ConfigInfo MAIN_GCI_FOLDER_A_PATH_OVERRIDE{ + {System::Main, "Core", "GCIFolderAPathOverride"}, ""}; +const ConfigInfo MAIN_GCI_FOLDER_B_PATH_OVERRIDE{ + {System::Main, "Core", "GCIFolderBPathOverride"}, ""}; +const ConfigInfo MAIN_GCI_FOLDER_CURRENT_GAME_ONLY{ + {System::Main, "Core", "GCIFolderCurrentGameOnly"}, false}; const ConfigInfo MAIN_SLOT_A{{System::Main, "Core", "SlotA"}, ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER}; const ConfigInfo MAIN_SLOT_B{{System::Main, "Core", "SlotB"}, diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index e23e96882d..260c16a1bc 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -37,6 +37,9 @@ extern const ConfigInfo MAIN_MEMCARD_A_PATH; extern const ConfigInfo MAIN_MEMCARD_B_PATH; extern const ConfigInfo MAIN_AGP_CART_A_PATH; extern const ConfigInfo MAIN_AGP_CART_B_PATH; +extern const ConfigInfo MAIN_GCI_FOLDER_A_PATH_OVERRIDE; +extern const ConfigInfo MAIN_GCI_FOLDER_B_PATH_OVERRIDE; +extern const ConfigInfo MAIN_GCI_FOLDER_CURRENT_GAME_ONLY; extern const ConfigInfo MAIN_SLOT_A; extern const ConfigInfo MAIN_SLOT_B; extern const ConfigInfo MAIN_SERIAL_PORT_1; diff --git a/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp b/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp index 1d959a286f..4f70d58399 100644 --- a/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp +++ b/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp @@ -6,7 +6,9 @@ #include +#include "Common/CommonPaths.h" #include "Common/Config/Config.h" +#include "Common/FileUtil.h" #include "Core/Config/MainSettings.h" #include "Core/Config/SYSCONFSettings.h" #include "Core/NetPlayProto.h" @@ -39,6 +41,23 @@ public: layer->Set(Config::SYSCONF_PROGRESSIVE_SCAN, m_settings.m_ProgressiveScan); layer->Set(Config::SYSCONF_PAL60, m_settings.m_PAL60); + + if (m_settings.m_SyncSaveData) + { + if (!m_settings.m_IsHosting) + { + const std::string path = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY DIR_SEP; + layer->Set(Config::MAIN_GCI_FOLDER_A_PATH_OVERRIDE, path + "Card A"); + layer->Set(Config::MAIN_GCI_FOLDER_B_PATH_OVERRIDE, path + "Card B"); + + const std::string file = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY + "%c." + + m_settings.m_SaveDataRegion + ".raw"; + layer->Set(Config::MAIN_MEMCARD_A_PATH, StringFromFormat(file.c_str(), 'A')); + layer->Set(Config::MAIN_MEMCARD_B_PATH, StringFromFormat(file.c_str(), 'B')); + } + + layer->Set(Config::MAIN_GCI_FOLDER_CURRENT_GAME_ONLY, true); + } } void Save(Config::Layer* layer) override diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index e2747ee415..3eba2b6f6e 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -227,8 +227,6 @@ void SConfig::SaveCoreSettings(IniFile& ini) core->Set("AudioLatency", iLatency); core->Set("AudioStretch", m_audio_stretch); core->Set("AudioStretchMaxLatency", m_audio_stretch_max_latency); - core->Set("MemcardAPath", m_strMemoryCardA); - core->Set("MemcardBPath", m_strMemoryCardB); core->Set("AgpCartAPath", m_strGbaCartA); core->Set("AgpCartBPath", m_strGbaCartB); core->Set("SlotA", m_EXIDevice[0]); @@ -505,8 +503,6 @@ void SConfig::LoadCoreSettings(IniFile& ini) core->Get("AudioLatency", &iLatency, 20); core->Get("AudioStretch", &m_audio_stretch, false); core->Get("AudioStretchMaxLatency", &m_audio_stretch_max_latency, 80); - core->Get("MemcardAPath", &m_strMemoryCardA); - core->Get("MemcardBPath", &m_strMemoryCardB); core->Get("AgpCartAPath", &m_strGbaCartA); core->Get("AgpCartBPath", &m_strGbaCartB); core->Get("SlotA", (int*)&m_EXIDevice[0], ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER); @@ -947,62 +943,12 @@ bool SConfig::SetPathsAndGameMetadata(const BootParameters& boot) // Set up paths const std::string region_dir = GetDirectoryForRegion(ToGameCubeRegion(m_region)); - CheckMemcardPath(SConfig::GetInstance().m_strMemoryCardA, region_dir, true); - CheckMemcardPath(SConfig::GetInstance().m_strMemoryCardB, region_dir, false); m_strSRAM = File::GetUserPath(F_GCSRAM_IDX); m_strBootROM = GetBootROMPath(region_dir); return true; } -void SConfig::CheckMemcardPath(std::string& memcardPath, const std::string& gameRegion, - bool isSlotA) -{ - std::string ext("." + gameRegion + ".raw"); - if (memcardPath.empty()) - { - // Use default memcard path if there is no user defined name - std::string defaultFilename = isSlotA ? GC_MEMCARDA : GC_MEMCARDB; - memcardPath = File::GetUserPath(D_GCUSER_IDX) + defaultFilename + ext; - } - else - { - std::string filename = memcardPath; - std::string region = filename.substr(filename.size() - 7, 3); - bool hasregion = false; - hasregion |= region.compare(USA_DIR) == 0; - hasregion |= region.compare(JAP_DIR) == 0; - hasregion |= region.compare(EUR_DIR) == 0; - if (!hasregion) - { - // filename doesn't have region in the extension - if (File::Exists(filename)) - { - // If the old file exists we are polite and ask if we should copy it - std::string oldFilename = filename; - filename.replace(filename.size() - 4, 4, ext); - if (PanicYesNoT("Memory Card filename in Slot %c is incorrect\n" - "Region not specified\n\n" - "Slot %c path was changed to\n" - "%s\n" - "Would you like to copy the old file to this new location?\n", - isSlotA ? 'A' : 'B', isSlotA ? 'A' : 'B', filename.c_str())) - { - if (!File::Copy(oldFilename, filename)) - PanicAlertT("Copy failed"); - } - } - memcardPath = filename; // Always correct the path! - } - else if (region.compare(gameRegion) != 0) - { - // filename has region, but it's not == gameRegion - // Just set the correct filename, the EXI Device will create it if it doesn't exist - memcardPath = filename.replace(filename.size() - ext.size(), ext.size(), ext); - } - } -} - DiscIO::Language SConfig::GetCurrentLanguage(bool wii) const { int language_value; diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index c21b22e009..f461f8698d 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -212,7 +212,6 @@ struct SConfig static const char* GetDirectoryForRegion(DiscIO::Region region); std::string GetBootROMPath(const std::string& region_directory) const; bool SetPathsAndGameMetadata(const BootParameters& boot); - void CheckMemcardPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA); DiscIO::Language GetCurrentLanguage(bool wii) const; IniFile LoadDefaultGameIni() const; @@ -223,8 +222,6 @@ struct SConfig static IniFile LoadLocalGameIni(const std::string& id, std::optional revision); static IniFile LoadGameIni(const std::string& id, std::optional revision); - std::string m_strMemoryCardA; - std::string m_strMemoryCardB; std::string m_strGbaCartA; std::string m_strGbaCartB; ExpansionInterface::TEXIDevices m_EXIDevice[3]; diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj index cb9d82584b..0fcf274b9c 100644 --- a/Source/Core/Core/Core.vcxproj +++ b/Source/Core/Core/Core.vcxproj @@ -448,6 +448,7 @@ + diff --git a/Source/Core/Core/Core.vcxproj.filters b/Source/Core/Core/Core.vcxproj.filters index b60f775010..0e084ca51e 100644 --- a/Source/Core/Core/Core.vcxproj.filters +++ b/Source/Core/Core/Core.vcxproj.filters @@ -1272,6 +1272,9 @@ HW %28Flipper/Hollywood%29 + + HW %28Flipper/Hollywood%29 + DSPCore diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp index c7a57d2373..5d8eeee0ef 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp @@ -13,12 +13,14 @@ #include "Common/ChunkFile.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" +#include "Common/Config/Config.h" #include "Common/FileUtil.h" #include "Common/IniFile.h" #include "Common/Logging/Log.h" #include "Common/NandPaths.h" #include "Common/StringUtil.h" #include "Core/CommonTitles.h" +#include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/CoreTiming.h" #include "Core/HW/EXI/EXI.h" @@ -31,6 +33,7 @@ #include "Core/HW/Sram.h" #include "Core/HW/SystemTimers.h" #include "Core/Movie.h" +#include "Core/NetPlayProto.h" #include "DiscIO/Enums.h" namespace ExpansionInterface @@ -169,24 +172,46 @@ void CEXIMemoryCard::SetupGciFolder(u16 sizeMb) std::string strDirectoryName = File::GetUserPath(D_GCUSER_IDX); + bool migrate = true; + if (Movie::IsPlayingInput() && Movie::IsConfigSaved() && Movie::IsUsingMemcard(card_index) && Movie::IsStartingFromClearSave()) + { strDirectoryName += "Movie" DIR_SEP; + migrate = false; + } - strDirectoryName = strDirectoryName + SConfig::GetDirectoryForRegion(region) + DIR_SEP + - StringFromFormat("Card %c", 'A' + card_index); + const std::string path_override = + Config::Get(card_index == 0 ? Config::MAIN_GCI_FOLDER_A_PATH_OVERRIDE : + Config::MAIN_GCI_FOLDER_B_PATH_OVERRIDE); + if (!path_override.empty()) + { + strDirectoryName = path_override; + migrate = false; + } + else + { + strDirectoryName = strDirectoryName + SConfig::GetDirectoryForRegion(region) + DIR_SEP + + StringFromFormat("Card %c", 'A' + card_index); + } const File::FileInfo file_info(strDirectoryName); - if (!file_info.Exists()) // first use of memcard folder, migrate automatically + if (!file_info.Exists()) { - MigrateFromMemcardFile(strDirectoryName + DIR_SEP, card_index); + if (migrate) // first use of memcard folder, migrate automatically + MigrateFromMemcardFile(strDirectoryName + DIR_SEP, card_index); + else + File::CreateFullPath(strDirectoryName + DIR_SEP); } else if (!file_info.IsDirectory()) { if (File::Rename(strDirectoryName, strDirectoryName + ".original")) { PanicAlertT("%s was not a directory, moved to *.original", strDirectoryName.c_str()); - MigrateFromMemcardFile(strDirectoryName + DIR_SEP, card_index); + if (migrate) + MigrateFromMemcardFile(strDirectoryName + DIR_SEP, card_index); + else + File::CreateFullPath(strDirectoryName + DIR_SEP); } else // we tried but the user wants to crash { @@ -204,17 +229,21 @@ void CEXIMemoryCard::SetupGciFolder(u16 sizeMb) void CEXIMemoryCard::SetupRawMemcard(u16 sizeMb) { - std::string filename = (card_index == 0) ? SConfig::GetInstance().m_strMemoryCardA : - SConfig::GetInstance().m_strMemoryCardB; + const bool is_slot_a = card_index == 0; + std::string filename = is_slot_a ? Config::Get(Config::MAIN_MEMCARD_A_PATH) : + Config::Get(Config::MAIN_MEMCARD_B_PATH); if (Movie::IsPlayingInput() && Movie::IsConfigSaved() && Movie::IsUsingMemcard(card_index) && Movie::IsStartingFromClearSave()) - filename = File::GetUserPath(D_GCUSER_IDX) + - StringFromFormat("Movie%s.raw", (card_index == 0) ? "A" : "B"); + filename = + File::GetUserPath(D_GCUSER_IDX) + StringFromFormat("Movie%s.raw", is_slot_a ? "A" : "B"); + + const std::string region_dir = + SConfig::GetDirectoryForRegion(SConfig::ToGameCubeRegion(SConfig::GetInstance().m_region)); + MemoryCard::CheckPath(filename, region_dir, is_slot_a); if (sizeMb == MemCard251Mb) - { filename.insert(filename.find_last_of("."), ".251"); - } + memorycard = std::make_unique(filename, card_index, sizeMb); } diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp index 38e24eca42..476aae5e3c 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp @@ -16,6 +16,7 @@ #include "Common/Assert.h" #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" +#include "Common/Config/Config.h" #include "Common/File.h" #include "Common/FileSearch.h" #include "Common/FileUtil.h" @@ -23,8 +24,10 @@ #include "Common/MsgHandler.h" #include "Common/StringUtil.h" #include "Common/Thread.h" +#include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" +#include "Core/NetPlayProto.h" const int NO_INDEX = -1; static const char* MC_HDR = "MC_SYSTEM_AREA"; @@ -121,6 +124,58 @@ int GCMemcardDirectory::LoadGCI(const std::string& file_name, bool current_game_ return NO_INDEX; } +// This is only used by NetPlay but it made sense to put it here to keep the relevant code together +std::vector GCMemcardDirectory::GetFileNamesForGameID(const std::string& directory, + const std::string& game_id) +{ + std::vector filenames; + + u32 game_code = 0; + if (game_id.length() >= 4 && game_id != "00000000") + game_code = BE32(reinterpret_cast(game_id.c_str())); + + std::vector loaded_saves; + for (const std::string& file_name : Common::DoFileSearch({directory}, {".gci"})) + { + File::IOFile gci_file(file_name, "rb"); + if (!gci_file) + continue; + + GCIFile gci; + gci.m_filename = file_name; + gci.m_dirty = false; + if (!gci_file.ReadBytes(&gci.m_gci_header, DENTRY_SIZE)) + continue; + + const std::string gci_filename = gci.m_gci_header.GCI_FileName(); + if (std::find(loaded_saves.begin(), loaded_saves.end(), gci_filename) != loaded_saves.end()) + continue; + + const u16 num_blocks = BE16(gci.m_gci_header.BlockCount); + // largest number of free blocks on a memory card + // in reality, there are not likely any valid gci files > 251 blocks + if (num_blocks > 2043) + continue; + + const u32 size = num_blocks * BLOCK_SIZE; + const u64 file_size = gci_file.GetSize(); + if (file_size != size + DENTRY_SIZE) + continue; + + // There's technically other available block checks to prevent overfilling the virtual memory + // card (see above method), but since we're only loading the saves for one GameID here, we're + // definitely not going to run out of space. + + if (game_code == BE32(gci.m_gci_header.Gamecode)) + { + loaded_saves.push_back(gci_filename); + filenames.push_back(file_name); + } + } + + return filenames; +} + GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, int slot, u16 size_mbits, bool shift_jis, int game_id) : MemoryCardBase(slot, size_mbits), m_game_id(game_id), m_last_block(-1), @@ -151,7 +206,8 @@ GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, int slot, u m_save_directory.c_str()); break; } - int index = LoadGCI(gci_file, m_saves.size() > 112); + int index = LoadGCI(gci_file, m_saves.size() > 112 || + Config::Get(Config::MAIN_GCI_FOLDER_CURRENT_GAME_ONLY)); if (index != NO_INDEX) { m_loaded_saves.push_back(m_saves.at(index).m_gci_header.GCI_FileName()); @@ -687,8 +743,8 @@ void GCIFile::DoState(PointerWrap& p) void MigrateFromMemcardFile(const std::string& directory_name, int card_index) { File::CreateFullPath(directory_name); - std::string ini_memcard = (card_index == 0) ? SConfig::GetInstance().m_strMemoryCardA : - SConfig::GetInstance().m_strMemoryCardB; + std::string ini_memcard = (card_index == 0) ? Config::Get(Config::MAIN_MEMCARD_A_PATH) : + Config::Get(Config::MAIN_MEMCARD_B_PATH); if (File::Exists(ini_memcard)) { GCMemcard memcard(ini_memcard.c_str()); diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h index 363c326a8a..ce784b8101 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h @@ -28,6 +28,8 @@ public: GCMemcardDirectory(GCMemcardDirectory&&) = default; GCMemcardDirectory& operator=(GCMemcardDirectory&&) = default; + static std::vector GetFileNamesForGameID(const std::string& directory, + const std::string& game_id); void FlushToFile(); void FlushThread(); s32 Read(u32 src_address, s32 length, u8* dest_address) override; diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp index 4981aad225..1093b10d13 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp @@ -12,10 +12,12 @@ #include #include "Common/ChunkFile.h" +#include "Common/CommonPaths.h" #include "Common/CommonTypes.h" #include "Common/File.h" #include "Common/FileUtil.h" #include "Common/Logging/Log.h" +#include "Common/MsgHandler.h" #include "Common/StringUtil.h" #include "Common/Thread.h" #include "Core/ConfigManager.h" @@ -71,6 +73,53 @@ MemoryCard::~MemoryCard() } } +void MemoryCard::CheckPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA) +{ + std::string ext("." + gameRegion + ".raw"); + if (memcardPath.empty()) + { + // Use default memcard path if there is no user defined name + std::string defaultFilename = isSlotA ? GC_MEMCARDA : GC_MEMCARDB; + memcardPath = File::GetUserPath(D_GCUSER_IDX) + defaultFilename + ext; + } + else + { + std::string filename = memcardPath; + std::string region = filename.substr(filename.size() - 7, 3); + bool hasregion = false; + hasregion |= region.compare(USA_DIR) == 0; + hasregion |= region.compare(JAP_DIR) == 0; + hasregion |= region.compare(EUR_DIR) == 0; + if (!hasregion) + { + // filename doesn't have region in the extension + if (File::Exists(filename)) + { + // If the old file exists we are polite and ask if we should copy it + std::string oldFilename = filename; + filename.replace(filename.size() - 4, 4, ext); + if (PanicYesNoT("Memory Card filename in Slot %c is incorrect\n" + "Region not specified\n\n" + "Slot %c path was changed to\n" + "%s\n" + "Would you like to copy the old file to this new location?\n", + isSlotA ? 'A' : 'B', isSlotA ? 'A' : 'B', filename.c_str())) + { + if (!File::Copy(oldFilename, filename)) + PanicAlertT("Copy failed"); + } + } + memcardPath = filename; // Always correct the path! + } + else if (region.compare(gameRegion) != 0) + { + // filename has region, but it's not == gameRegion + // Just set the correct filename, the EXI Device will create it if it doesn't exist + memcardPath = filename.replace(filename.size() - ext.size(), ext.size(), ext); + } + } +} + void MemoryCard::FlushThread() { if (!SConfig::GetInstance().bEnableMemcardSdWriting) diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h index 69d664d4bb..0659e33d3a 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h @@ -19,6 +19,7 @@ class MemoryCard : public MemoryCardBase public: MemoryCard(const std::string& filename, int card_index, u16 size_mbits = MemCard2043Mb); ~MemoryCard(); + static void CheckPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA); void FlushThread(); void MakeDirty(); diff --git a/Source/Core/Core/HW/WiiSave.cpp b/Source/Core/Core/HW/WiiSave.cpp index 67f5340ef8..ddf9eb7f72 100644 --- a/Source/Core/Core/HW/WiiSave.cpp +++ b/Source/Core/Core/HW/WiiSave.cpp @@ -32,6 +32,7 @@ #include "Common/StringUtil.h" #include "Common/Swap.h" #include "Core/CommonTitles.h" +#include "Core/HW/WiiSaveStructs.h" #include "Core/IOS/ES/ES.h" #include "Core/IOS/FS/FileSystem.h" #include "Core/IOS/IOS.h" @@ -48,96 +49,6 @@ constexpr Md5 s_md5_blanker{{0x0E, 0x65, 0x37, 0x81, 0x99, 0xBE, 0x45, 0x17, 0xA 0x45, 0x1A, 0x57, 0x93}}; constexpr u32 s_ng_id = 0x0403AC68; -enum -{ - BLOCK_SZ = 0x40, - ICON_SZ = 0x1200, - BNR_SZ = 0x60a0, - FULL_BNR_MIN = 0x72a0, // BNR_SZ + 1*ICON_SZ - FULL_BNR_MAX = 0xF0A0, // BNR_SZ + 8*ICON_SZ - BK_LISTED_SZ = 0x70, // Size before rounding to nearest block - SIG_SZ = 0x40, - FULL_CERT_SZ = 0x3C0, // SIG_SZ + NG_CERT_SZ + AP_CERT_SZ + 0x80? - - BK_HDR_MAGIC = 0x426B0001, - FILE_HDR_MAGIC = 0x03adf17e -}; - -#pragma pack(push, 1) -struct Header -{ - Common::BigEndianValue tid; - Common::BigEndianValue banner_size; // (0x72A0 or 0xF0A0, also seen 0xBAA0) - u8 permissions; - u8 unk1; // maybe permissions is a be16 - std::array md5; // md5 of plaintext header with md5 blanker applied - Common::BigEndianValue unk2; - u8 banner[FULL_BNR_MAX]; -}; -static_assert(sizeof(Header) == 0xf0c0, "Header has an incorrect size"); - -struct BkHeader -{ - Common::BigEndianValue size; // 0x00000070 - // u16 magic; // 'Bk' - // u16 magic2; // or version (0x0001) - Common::BigEndianValue magic; // 0x426B0001 - Common::BigEndianValue ngid; - Common::BigEndianValue number_of_files; - Common::BigEndianValue size_of_files; - Common::BigEndianValue unk1; - Common::BigEndianValue unk2; - Common::BigEndianValue total_size; - std::array unk3; - Common::BigEndianValue tid; - std::array mac_address; - std::array padding; -}; -static_assert(sizeof(BkHeader) == 0x80, "BkHeader has an incorrect size"); - -struct FileHDR -{ - Common::BigEndianValue magic; // 0x03adf17e - Common::BigEndianValue size; - u8 permissions; - u8 attrib; - u8 type; // (1=file, 2=directory) - std::array name; - std::array padding; - std::array iv; - std::array unk; -}; -static_assert(sizeof(FileHDR) == 0x80, "FileHDR has an incorrect size"); -#pragma pack(pop) - -class Storage -{ -public: - struct SaveFile - { - enum class Type : u8 - { - File = 1, - Directory = 2, - }; - u8 mode, attributes; - Type type; - /// File name relative to the title data directory. - std::string path; - // Only valid for regular (i.e. non-directory) files. - Common::Lazy>> data; - }; - - virtual ~Storage() = default; - virtual bool SaveExists() { return true; } - virtual std::optional
ReadHeader() = 0; - virtual std::optional ReadBkHeader() = 0; - virtual std::optional> ReadFiles() = 0; - virtual bool WriteHeader(const Header& header) = 0; - virtual bool WriteBkHeader(const BkHeader& bk_header) = 0; - virtual bool WriteFiles(const std::vector& files) = 0; -}; - void StorageDeleter::operator()(Storage* p) const { delete p; diff --git a/Source/Core/Core/HW/WiiSaveStructs.h b/Source/Core/Core/HW/WiiSaveStructs.h new file mode 100644 index 0000000000..6ef5a38feb --- /dev/null +++ b/Source/Core/Core/HW/WiiSaveStructs.h @@ -0,0 +1,112 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +// Based off of tachtig/twintig http://git.infradead.org/?p=users/segher/wii.git +// Copyright 2007,2008 Segher Boessenkool +// Licensed under the terms of the GNU GPL, version 2 +// http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Lazy.h" +#include "Common/Swap.h" + +namespace WiiSave +{ +enum +{ + BLOCK_SZ = 0x40, + ICON_SZ = 0x1200, + BNR_SZ = 0x60a0, + FULL_BNR_MIN = 0x72a0, // BNR_SZ + 1*ICON_SZ + FULL_BNR_MAX = 0xF0A0, // BNR_SZ + 8*ICON_SZ + BK_LISTED_SZ = 0x70, // Size before rounding to nearest block + SIG_SZ = 0x40, + FULL_CERT_SZ = 0x3C0, // SIG_SZ + NG_CERT_SZ + AP_CERT_SZ + 0x80? + + BK_HDR_MAGIC = 0x426B0001, + FILE_HDR_MAGIC = 0x03adf17e +}; + +#pragma pack(push, 1) +struct Header +{ + Common::BigEndianValue tid; + Common::BigEndianValue banner_size; // (0x72A0 or 0xF0A0, also seen 0xBAA0) + u8 permissions; + u8 unk1; // maybe permissions is a be16 + std::array md5; // md5 of plaintext header with md5 blanker applied + Common::BigEndianValue unk2; + u8 banner[FULL_BNR_MAX]; +}; +static_assert(sizeof(Header) == 0xf0c0, "Header has an incorrect size"); + +struct BkHeader +{ + Common::BigEndianValue size; // 0x00000070 + // u16 magic; // 'Bk' + // u16 magic2; // or version (0x0001) + Common::BigEndianValue magic; // 0x426B0001 + Common::BigEndianValue ngid; + Common::BigEndianValue number_of_files; + Common::BigEndianValue size_of_files; + Common::BigEndianValue unk1; + Common::BigEndianValue unk2; + Common::BigEndianValue total_size; + std::array unk3; + Common::BigEndianValue tid; + std::array mac_address; + std::array padding; +}; +static_assert(sizeof(BkHeader) == 0x80, "BkHeader has an incorrect size"); + +struct FileHDR +{ + Common::BigEndianValue magic; // 0x03adf17e + Common::BigEndianValue size; + u8 permissions; + u8 attrib; + u8 type; // (1=file, 2=directory) + std::array name; + std::array padding; + std::array iv; + std::array unk; +}; +static_assert(sizeof(FileHDR) == 0x80, "FileHDR has an incorrect size"); +#pragma pack(pop) + +class Storage +{ +public: + struct SaveFile + { + enum class Type : u8 + { + File = 1, + Directory = 2, + }; + u8 mode, attributes; + Type type; + /// File name relative to the title data directory. + std::string path; + // Only valid for regular (i.e. non-directory) files. + Common::Lazy>> data; + }; + + virtual ~Storage() = default; + virtual bool SaveExists() { return true; } + virtual std::optional
ReadHeader() = 0; + virtual std::optional ReadBkHeader() = 0; + virtual std::optional> ReadFiles() = 0; + virtual bool WriteHeader(const Header& header) = 0; + virtual bool WriteBkHeader(const BkHeader& bk_header) = 0; + virtual bool WriteFiles(const std::vector& files) = 0; +}; +} // namespace WiiSave diff --git a/Source/Core/Core/Movie.cpp b/Source/Core/Core/Movie.cpp index 7a32e04410..d779f0a71d 100644 --- a/Source/Core/Core/Movie.cpp +++ b/Source/Core/Core/Movie.cpp @@ -1398,7 +1398,7 @@ void GetSettings() } else { - s_bClearSave = !File::Exists(SConfig::GetInstance().m_strMemoryCardA); + s_bClearSave = !File::Exists(Config::Get(Config::MAIN_MEMCARD_A_PATH)); } s_memcards |= (SConfig::GetInstance().m_EXIDevice[0] == ExpansionInterface::EXIDEVICE_MEMORYCARD || @@ -1491,4 +1491,4 @@ void Shutdown() s_currentInputCount = s_totalInputCount = s_totalFrames = s_tickCountAtLastInput = 0; s_temp_input.clear(); } -}; +} // namespace Movie diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index 83a0aa0938..d29b931ae0 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -13,18 +13,23 @@ #include #include #include +#include +#include #include #include "Common/Assert.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" #include "Common/ENetUtil.h" +#include "Common/File.h" #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/MD5.h" #include "Common/MsgHandler.h" +#include "Common/NandPaths.h" #include "Common/QoSSession.h" +#include "Common/SFMLHelper.h" #include "Common/StringUtil.h" #include "Common/Timer.h" #include "Common/Version.h" @@ -34,12 +39,19 @@ #include "Core/HW/SI/SI.h" #include "Core/HW/SI/SI_DeviceGCController.h" #include "Core/HW/Sram.h" +#include "Core/HW/WiiSave.h" +#include "Core/HW/WiiSaveStructs.h" #include "Core/HW/WiimoteEmu/WiimoteEmu.h" #include "Core/HW/WiimoteReal/WiimoteReal.h" +#include "Core/IOS/FS/FileSystem.h" +#include "Core/IOS/FS/HostBackend/FS.h" #include "Core/IOS/USB/Bluetooth/BTEmu.h" +#include "Core/IOS/Uids.h" #include "Core/Movie.h" #include "Core/PowerPC/PowerPC.h" +#include "Core/WiiRoot.h" #include "InputCommon/GCAdapter.h" +#include "UICommon/GameFile.h" #include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/VideoConfig.h" @@ -47,6 +59,7 @@ namespace NetPlay { static std::mutex crit_netplay_client; static NetPlayClient* netplay_client = nullptr; +static std::unique_ptr s_wii_sync_fs; // called from ---GUI--- thread NetPlayClient::~NetPlayClient() @@ -467,10 +480,11 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) packet >> tmp; m_net_settings.m_EXIDevice[1] = static_cast(tmp); - u32 time_low, time_high; - packet >> time_low; - packet >> time_high; - g_netplay_initial_rtc = time_low | ((u64)time_high << 32); + g_netplay_initial_rtc = Common::PacketReadU64(packet); + + packet >> m_net_settings.m_SyncSaveData; + packet >> m_net_settings.m_SaveDataRegion; + m_net_settings.m_IsHosting = m_dialog->IsHosting(); } m_dialog->OnMsgStartGame(); @@ -553,6 +567,194 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) } break; + case NP_MSG_SYNC_SAVE_DATA: + { + MessageId sub_id; + packet >> sub_id; + + switch (sub_id) + { + case SYNC_SAVE_DATA_NOTIFY: + { + packet >> m_sync_save_data_count; + m_sync_save_data_success_count = 0; + + if (m_sync_save_data_count == 0) + SyncSaveDataResponse(true); + else + m_dialog->AppendChat(GetStringT("Synchronizing save data...")); + } + break; + + case SYNC_SAVE_DATA_RAW: + { + if (m_dialog->IsHosting()) + return 0; + + bool is_slot_a; + std::string region; + bool mc251; + packet >> is_slot_a >> region >> mc251; + + const std::string path = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY + + (is_slot_a ? "A." : "B.") + region + (mc251 ? ".251" : "") + ".raw"; + if (File::Exists(path) && !File::Delete(path)) + { + PanicAlertT("Failed to delete NetPlay memory card. Verify your write permissions."); + SyncSaveDataResponse(false); + return 0; + } + + const bool success = DecompressPacketIntoFile(packet, path); + SyncSaveDataResponse(success); + } + break; + + case SYNC_SAVE_DATA_GCI: + { + if (m_dialog->IsHosting()) + return 0; + + bool is_slot_a; + u8 file_count; + packet >> is_slot_a >> file_count; + + const std::string path = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY DIR_SEP + + StringFromFormat("Card %c", is_slot_a ? 'A' : 'B'); + + if ((File::Exists(path) && !File::DeleteDirRecursively(path + DIR_SEP)) || + !File::CreateFullPath(path + DIR_SEP)) + { + PanicAlertT("Failed to reset NetPlay GCI folder. Verify your write permissions."); + SyncSaveDataResponse(false); + return 0; + } + + for (u8 i = 0; i < file_count; i++) + { + std::string file_name; + packet >> file_name; + + if (!DecompressPacketIntoFile(packet, path + DIR_SEP + file_name)) + { + SyncSaveDataResponse(false); + return 0; + } + } + + SyncSaveDataResponse(true); + } + break; + + case SYNC_SAVE_DATA_WII: + { + if (m_dialog->IsHosting()) + return 0; + + const auto game = m_dialog->FindGameFile(m_selected_game); + if (game == nullptr) + { + SyncSaveDataResponse(true); // whatever, we won't be booting anyways + return 0; + } + + const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP; + + if (File::Exists(path) && !File::DeleteDirRecursively(path)) + { + PanicAlertT("Failed to reset NetPlay NAND folder. Verify your write permissions."); + SyncSaveDataResponse(false); + return 0; + } + + auto temp_fs = std::make_unique(path); + temp_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, + Common::GetTitleDataPath(game->GetTitleID()), 0, + {IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite, + IOS::HLE::FS::Mode::ReadWrite}); + auto save = WiiSave::MakeNandStorage(temp_fs.get(), game->GetTitleID()); + + bool exists; + packet >> exists; + if (exists) + { + // Header + WiiSave::Header header; + header.tid = Common::PacketReadU64(packet); + header.banner_size = Common::PacketReadU32(packet); + packet >> header.permissions; + packet >> header.unk1; + for (size_t i = 0; i < header.md5.size(); i++) + packet >> header.md5[i]; + header.unk2 = Common::PacketReadU16(packet); + for (size_t i = 0; i < header.banner_size; i++) + packet >> header.banner[i]; + + // BkHeader + WiiSave::BkHeader bk_header; + bk_header.size = Common::PacketReadU32(packet); + bk_header.magic = Common::PacketReadU32(packet); + bk_header.ngid = Common::PacketReadU32(packet); + bk_header.number_of_files = Common::PacketReadU32(packet); + bk_header.size_of_files = Common::PacketReadU32(packet); + bk_header.unk1 = Common::PacketReadU32(packet); + bk_header.unk2 = Common::PacketReadU32(packet); + bk_header.total_size = Common::PacketReadU32(packet); + for (size_t i = 0; i < bk_header.unk3.size(); i++) + packet >> bk_header.unk3[i]; + bk_header.tid = Common::PacketReadU64(packet); + for (size_t i = 0; i < bk_header.mac_address.size(); i++) + packet >> bk_header.mac_address[i]; + + // Files + std::vector files; + for (u32 i = 0; i < bk_header.number_of_files; i++) + { + WiiSave::Storage::SaveFile file; + packet >> file.mode >> file.attributes; + { + u8 tmp; + packet >> tmp; + file.type = static_cast(tmp); + } + packet >> file.path; + + if (file.type == WiiSave::Storage::SaveFile::Type::File) + { + auto buffer = DecompressPacketIntoBuffer(packet); + if (!buffer) + { + SyncSaveDataResponse(false); + return 0; + } + + file.data = std::move(*buffer); + } + + files.push_back(std::move(file)); + } + + if (!save->WriteHeader(header) || !save->WriteBkHeader(bk_header) || + !save->WriteFiles(files)) + { + PanicAlertT("Failed to write Wii save."); + SyncSaveDataResponse(false); + return 0; + } + } + + SetWiiSyncFS(std::move(temp_fs)); + SyncSaveDataResponse(true); + } + break; + + default: + PanicAlertT("Unknown SYNC_SAVE_DATA message received with id: %d", sub_id); + break; + } + } + break; + case NP_MSG_COMPUTE_MD5: { std::string file_identifier; @@ -895,6 +1097,120 @@ bool NetPlayClient::StartGame(const std::string& path) return true; } +void NetPlayClient::SyncSaveDataResponse(const bool success) +{ + m_dialog->AppendChat(success ? GetStringT("Data received!") : + GetStringT("Error processing data.")); + + if (success) + { + if (++m_sync_save_data_success_count >= m_sync_save_data_count) + { + sf::Packet response_packet; + response_packet << static_cast(NP_MSG_SYNC_SAVE_DATA); + response_packet << static_cast(SYNC_SAVE_DATA_SUCCESS); + + Send(response_packet); + } + } + else + { + sf::Packet response_packet; + response_packet << static_cast(NP_MSG_SYNC_SAVE_DATA); + response_packet << static_cast(SYNC_SAVE_DATA_FAILURE); + + Send(response_packet); + } +} + +bool NetPlayClient::DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path) +{ + u64 file_size = Common::PacketReadU64(packet); + ; + + if (file_size == 0) + return true; + + File::IOFile file(file_path, "wb"); + if (!file) + { + PanicAlertT("Failed to open file \"%s\". Verify your write permissions.", file_path.c_str()); + return false; + } + + std::vector in_buffer(NETPLAY_LZO_OUT_LEN); + std::vector out_buffer(NETPLAY_LZO_IN_LEN); + + while (true) + { + lzo_uint32 cur_len = 0; // number of bytes to read + lzo_uint new_len = 0; // number of bytes to write + + packet >> cur_len; + if (!cur_len) + break; // We reached the end of the data stream + + for (size_t j = 0; j < cur_len; j++) + { + packet >> in_buffer[j]; + } + + if (lzo1x_decompress(in_buffer.data(), cur_len, out_buffer.data(), &new_len, nullptr) != + LZO_E_OK) + { + PanicAlertT("Internal LZO Error - decompression failed"); + return false; + } + + if (!file.WriteBytes(out_buffer.data(), new_len)) + { + PanicAlertT("Error writing file: %s", file_path.c_str()); + return false; + } + } + + return true; +} + +std::optional> NetPlayClient::DecompressPacketIntoBuffer(sf::Packet& packet) +{ + u64 size = Common::PacketReadU64(packet); + ; + + std::vector out_buffer(size); + + if (size == 0) + return out_buffer; + + std::vector in_buffer(NETPLAY_LZO_OUT_LEN); + + lzo_uint i = 0; + while (true) + { + lzo_uint32 cur_len = 0; // number of bytes to read + lzo_uint new_len = 0; // number of bytes to write + + packet >> cur_len; + if (!cur_len) + break; // We reached the end of the data stream + + for (size_t j = 0; j < cur_len; j++) + { + packet >> in_buffer[j]; + } + + if (lzo1x_decompress(in_buffer.data(), cur_len, &out_buffer[i], &new_len, nullptr) != LZO_E_OK) + { + PanicAlertT("Internal LZO Error - decompression failed"); + return {}; + } + + i += new_len; + } + + return out_buffer; +} + // called from ---GUI--- thread bool NetPlayClient::ChangeGame(const std::string&) { @@ -1178,6 +1494,8 @@ bool NetPlayClient::StopGame() // stop game m_dialog->StopGame(); + ClearWiiSyncFS(); + return true; } @@ -1278,8 +1596,7 @@ void NetPlayClient::SendTimeBase() sf::Packet packet; packet << static_cast(NP_MSG_TIMEBASE); - packet << static_cast(timebase); - packet << static_cast(timebase << 32); + Common::PacketWriteU64(packet, timebase); packet << netplay_client->m_timebase_frame; netplay_client->SendAsync(std::move(packet)); @@ -1358,6 +1675,26 @@ const NetSettings& GetNetSettings() return netplay_client->GetNetSettings(); } +IOS::HLE::FS::FileSystem* GetWiiSyncFS() +{ + return s_wii_sync_fs.get(); +} + +void SetWiiSyncFS(std::unique_ptr fs) +{ + s_wii_sync_fs = std::move(fs); +} + +void ClearWiiSyncFS() +{ + // We're just assuming it will always be here because it is + const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP; + if (File::Exists(path)) + File::DeleteDirRecursively(path); + + s_wii_sync_fs.reset(); +} + void NetPlay_Enable(NetPlayClient* const np) { std::lock_guard lk(crit_netplay_client); diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index 5306c0f369..ad9d3ee971 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -7,10 +7,13 @@ #include #include #include +#include #include +#include #include #include #include + #include "Common/CommonTypes.h" #include "Common/Event.h" #include "Common/SPSCQueue.h" @@ -18,6 +21,11 @@ #include "Core/NetPlayProto.h" #include "InputCommon/GCPadStatus.h" +namespace UICommon +{ +class GameFile; +} + namespace NetPlay { class NetPlayUI @@ -26,6 +34,7 @@ public: virtual ~NetPlayUI() {} virtual void BootGame(const std::string& filename) = 0; virtual void StopGame() = 0; + virtual bool IsHosting() const = 0; virtual void Update() = 0; virtual void AppendChat(const std::string& msg) = 0; @@ -38,8 +47,11 @@ public: virtual void OnConnectionLost() = 0; virtual void OnConnectionError(const std::string& message) = 0; virtual void OnTraversalError(TraversalClient::FailureReason error) = 0; + virtual void OnSaveDataSyncFailure() = 0; + virtual bool IsRecording() = 0; virtual std::string FindGame(const std::string& game) = 0; + virtual std::shared_ptr FindGameFile(const std::string& game) = 0; virtual void ShowMD5Dialog(const std::string& file_identifier) = 0; virtual void SetMD5Progress(int pid, int progress) = 0; virtual void SetMD5Result(int pid, const std::string& result) = 0; @@ -159,6 +171,10 @@ private: void SendStartGamePacket(); void SendStopGamePacket(); + void SyncSaveDataResponse(bool success); + bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path); + std::optional> DecompressPacketIntoBuffer(sf::Packet& packet); + void UpdateDevices(); void AddPadStateToPacket(int in_game_pad, const GCPadStatus& np, sf::Packet& packet); void SendWiimoteState(int in_game_pad, const NetWiimote& nw); @@ -184,6 +200,8 @@ private: bool m_should_compute_MD5 = false; Common::Event m_gc_pad_event; Common::Event m_wii_pad_event; + u8 m_sync_save_data_count = 0; + u8 m_sync_save_data_success_count = 0; u32 m_timebase_frame = 0; }; diff --git a/Source/Core/Core/NetPlayProto.h b/Source/Core/Core/NetPlayProto.h index b6eaad0dfa..367af26872 100644 --- a/Source/Core/Core/NetPlayProto.h +++ b/Source/Core/Core/NetPlayProto.h @@ -9,6 +9,10 @@ #include "Common/CommonTypes.h" #include "Core/HW/EXI/EXI_Device.h" +namespace IOS::HLE::FS +{ +class FileSystem; +} namespace PowerPC { enum class CPUCore; @@ -33,6 +37,9 @@ struct NetSettings bool m_OCEnable; float m_OCFactor; ExpansionInterface::TEXIDevices m_EXIDevice[2]; + bool m_SyncSaveData; + std::string m_SaveDataRegion; + bool m_IsHosting; }; struct NetTraversalConfig @@ -94,6 +101,7 @@ enum NP_MSG_PLAYER_PING_DATA = 0xE2, NP_MSG_SYNC_GC_SRAM = 0xF0, + NP_MSG_SYNC_SAVE_DATA = 0xF1, }; enum @@ -103,6 +111,19 @@ enum CON_ERR_VERSION_MISMATCH = 3 }; +enum +{ + SYNC_SAVE_DATA_NOTIFY = 0, + SYNC_SAVE_DATA_SUCCESS = 1, + SYNC_SAVE_DATA_FAILURE = 2, + SYNC_SAVE_DATA_RAW = 3, + SYNC_SAVE_DATA_GCI = 4, + SYNC_SAVE_DATA_WII = 5 +}; + +constexpr u32 NETPLAY_LZO_IN_LEN = 1024 * 64; +constexpr u32 NETPLAY_LZO_OUT_LEN = NETPLAY_LZO_IN_LEN + (NETPLAY_LZO_IN_LEN / 16) + 64 + 3; + using NetWiimote = std::vector; using MessageId = u8; using PlayerId = u8; @@ -111,8 +132,10 @@ using PadMapping = s8; using PadMappingArray = std::array; bool IsNetPlayRunning(); - // Precondition: A netplay client instance must be present. In other words, // IsNetPlayRunning() must be true before calling this. const NetSettings& GetNetSettings(); +IOS::HLE::FS::FileSystem* GetWiiSyncFS(); +void SetWiiSyncFS(std::unique_ptr fs); +void ClearWiiSyncFS(); } // namespace NetPlay diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index 0e9116562f..a884aa2dce 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -9,24 +9,38 @@ #include #include #include +#include #include #include #include #include #include +#include + +#include "Common/CommonPaths.h" #include "Common/ENetUtil.h" +#include "Common/File.h" #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" +#include "Common/SFMLHelper.h" #include "Common/StringUtil.h" #include "Common/UPnP.h" #include "Common/Version.h" +#include "Core/Config/MainSettings.h" #include "Core/Config/NetplaySettings.h" #include "Core/ConfigManager.h" +#include "Core/HW/GCMemcard/GCMemcardDirectory.h" +#include "Core/HW/GCMemcard/GCMemcardRaw.h" #include "Core/HW/Sram.h" +#include "Core/HW/WiiSave.h" +#include "Core/HW/WiiSaveStructs.h" +#include "Core/IOS/FS/FileSystem.h" #include "Core/NetPlayClient.h" //for NetPlayUI +#include "DiscIO/Enums.h" #include "InputCommon/GCPadStatus.h" +#include "UICommon/GameFile.h" #if !defined(_WIN32) #include @@ -650,15 +664,13 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player) case NP_MSG_TIMEBASE: { - u32 x, y, frame; - packet >> x; - packet >> y; + u64 timebase = Common::PacketReadU64(packet); + u32 frame; packet >> frame; if (m_desync_detected) break; - u64 timebase = x | ((u64)y << 32); std::vector>& timebases = m_timebase_by_frame[frame]; timebases.emplace_back(player.pid, timebase); if (timebases.size() >= m_players.size()) @@ -737,12 +749,50 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player) } break; + case NP_MSG_SYNC_SAVE_DATA: + { + MessageId sub_id; + packet >> sub_id; + + switch (sub_id) + { + case SYNC_SAVE_DATA_SUCCESS: + { + if (m_start_pending) + { + m_save_data_synced_players++; + if (m_save_data_synced_players >= m_players.size() - 1) + { + m_dialog->AppendChat(GetStringT("All players synchronized.")); + StartGame(); + } + } + } + break; + + case SYNC_SAVE_DATA_FAILURE: + { + m_dialog->AppendChat( + StringFromFormat(GetStringT("%s failed to synchronize.").c_str(), player.name.c_str())); + m_dialog->OnSaveDataSyncFailure(); + m_start_pending = false; + } + break; + + default: + PanicAlertT( + "Unknown SYNC_SAVE_DATA message with id:%d received from player:%d Kicking player!", + sub_id, player.pid); + return 1; + } + } + break; + default: PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid, player.pid); // unknown message, kick the client return 1; - break; } return 0; @@ -812,6 +862,27 @@ void NetPlayServer::SetNetSettings(const NetSettings& settings) } // called from ---GUI--- thread +bool NetPlayServer::RequestStartGame() +{ + if (m_settings.m_SyncSaveData && m_players.size() > 1) + { + if (!SyncSaveData()) + { + PanicAlertT("Error synchronizing save data!"); + return false; + } + + m_start_pending = true; + } + else + { + return StartGame(); + } + + return true; +} + +// called from multiple threads bool NetPlayServer::StartGame() { m_timebase_by_frame.clear(); @@ -827,6 +898,9 @@ bool NetPlayServer::StartGame() else g_netplay_initial_rtc = Common::Timer::GetLocalTimeSinceJan1970(); + const std::string region = SConfig::GetDirectoryForRegion( + SConfig::ToGameCubeRegion(m_dialog->FindGameFile(m_selected_game)->GetRegion())); + // tell clients to start game sf::Packet spac; spac << static_cast(NP_MSG_START_GAME); @@ -847,16 +921,321 @@ bool NetPlayServer::StartGame() spac << m_settings.m_ReducePollingRate; spac << m_settings.m_EXIDevice[0]; spac << m_settings.m_EXIDevice[1]; - spac << static_cast(g_netplay_initial_rtc); - spac << static_cast(g_netplay_initial_rtc >> 32); + Common::PacketWriteU64(spac, g_netplay_initial_rtc); + spac << m_settings.m_SyncSaveData; + spac << region; SendAsyncToClients(std::move(spac)); + m_start_pending = false; m_is_running = true; return true; } +// called from ---GUI--- thread +bool NetPlayServer::SyncSaveData() +{ + m_save_data_synced_players = 0; + + u8 save_count = 0; + + constexpr size_t exi_device_count = 2; + for (size_t i = 0; i < exi_device_count; i++) + { + if (m_settings.m_EXIDevice[i] == ExpansionInterface::EXIDEVICE_MEMORYCARD || + SConfig::GetInstance().m_EXIDevice[i] == ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER) + { + save_count++; + } + } + + const auto game = m_dialog->FindGameFile(m_selected_game); + if (game == nullptr) + { + PanicAlertT("Selected game doesn't exist in game list!"); + return false; + } + + bool wii_save = false; + if (m_settings.m_CopyWiiSave && (game->GetPlatform() == DiscIO::Platform::WiiDisc || + game->GetPlatform() == DiscIO::Platform::WiiWAD)) + { + wii_save = true; + save_count++; + } + + { + sf::Packet pac; + pac << static_cast(NP_MSG_SYNC_SAVE_DATA); + pac << static_cast(SYNC_SAVE_DATA_NOTIFY); + pac << save_count; + + SendAsyncToClients(std::move(pac)); + } + + if (save_count == 0) + return true; + + const std::string region = + SConfig::GetDirectoryForRegion(SConfig::ToGameCubeRegion(game->GetRegion())); + + for (size_t i = 0; i < exi_device_count; i++) + { + const bool is_slot_a = i == 0; + + if (m_settings.m_EXIDevice[i] == ExpansionInterface::EXIDEVICE_MEMORYCARD) + { + std::string path = is_slot_a ? Config::Get(Config::MAIN_MEMCARD_A_PATH) : + Config::Get(Config::MAIN_MEMCARD_B_PATH); + + MemoryCard::CheckPath(path, region, is_slot_a); + + bool mc251; + IniFile gameIni = SConfig::LoadGameIni(game->GetGameID(), game->GetRevision()); + gameIni.GetOrCreateSection("Core")->Get("MemoryCard251", &mc251, false); + + if (mc251) + path.insert(path.find_last_of('.'), ".251"); + + sf::Packet pac; + pac << static_cast(NP_MSG_SYNC_SAVE_DATA); + pac << static_cast(SYNC_SAVE_DATA_RAW); + pac << is_slot_a << region << mc251; + + if (File::Exists(path)) + { + if (!CompressFileIntoPacket(path, pac)) + return false; + } + else + { + // No file, so we'll say the size is 0 + Common::PacketWriteU64(pac, 0); + } + + SendAsyncToClients(std::move(pac)); + } + else if (SConfig::GetInstance().m_EXIDevice[i] == + ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER) + { + const std::string path = File::GetUserPath(D_GCUSER_IDX) + region + DIR_SEP + + StringFromFormat("Card %c", is_slot_a ? 'A' : 'B'); + + sf::Packet pac; + pac << static_cast(NP_MSG_SYNC_SAVE_DATA); + pac << static_cast(SYNC_SAVE_DATA_GCI); + pac << is_slot_a; + + if (File::IsDirectory(path)) + { + std::vector files = + GCMemcardDirectory::GetFileNamesForGameID(path + DIR_SEP, game->GetGameID()); + + pac << static_cast(files.size()); + + for (const std::string& file : files) + { + pac << file.substr(file.find_last_of('/') + 1); + if (!CompressFileIntoPacket(file, pac)) + return false; + } + } + else + { + pac << static_cast(0); + } + + SendAsyncToClients(std::move(pac)); + } + } + + if (wii_save) + { + const auto configured_fs = IOS::HLE::FS::MakeFileSystem(IOS::HLE::FS::Location::Configured); + const auto save = WiiSave::MakeNandStorage(configured_fs.get(), game->GetTitleID()); + + sf::Packet pac; + pac << static_cast(NP_MSG_SYNC_SAVE_DATA); + pac << static_cast(SYNC_SAVE_DATA_WII); + + if (save->SaveExists()) + { + const std::optional header = save->ReadHeader(); + const std::optional bk_header = save->ReadBkHeader(); + const std::optional> files = save->ReadFiles(); + if (!header || !bk_header || !files) + return false; + + pac << true; // save exists + + // Header + Common::PacketWriteU64(pac, header->tid); + pac << header->banner_size << header->permissions << header->unk1; + for (size_t i = 0; i < header->md5.size(); i++) + pac << header->md5[i]; + pac << header->unk2; + for (size_t i = 0; i < header->banner_size; i++) + pac << header->banner[i]; + + // BkHeader + pac << bk_header->size << bk_header->magic << bk_header->ngid << bk_header->number_of_files + << bk_header->size_of_files << bk_header->unk1 << bk_header->unk2 + << bk_header->total_size; + for (size_t i = 0; i < bk_header->unk3.size(); i++) + pac << bk_header->unk3[i]; + Common::PacketWriteU64(pac, bk_header->tid); + for (size_t i = 0; i < bk_header->mac_address.size(); i++) + pac << bk_header->mac_address[i]; + + // Files + for (const WiiSave::Storage::SaveFile& file : *files) + { + pac << file.mode << file.attributes << static_cast(file.type) << file.path; + + if (file.type == WiiSave::Storage::SaveFile::Type::File) + { + const std::optional>& data = *file.data; + if (!data || !CompressBufferIntoPacket(*data, pac)) + return false; + } + } + } + else + { + pac << false; // save does not exist + } + + SendAsyncToClients(std::move(pac)); + } + + return true; +} + +bool NetPlayServer::CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet) +{ + File::IOFile file(file_path, "rb"); + if (!file) + { + PanicAlertT("Failed to open file \"%s\".", file_path.c_str()); + return false; + } + + const u64 size = file.GetSize(); + Common::PacketWriteU64(packet, size); + + if (size == 0) + return true; + + std::vector in_buffer(NETPLAY_LZO_IN_LEN); + std::vector out_buffer(NETPLAY_LZO_OUT_LEN); + std::vector wrkmem(LZO1X_1_MEM_COMPRESS); + + lzo_uint i = 0; + while (true) + { + lzo_uint32 cur_len = 0; // number of bytes to read + lzo_uint out_len = 0; // number of bytes to write + + if ((i + NETPLAY_LZO_IN_LEN) >= size) + { + cur_len = static_cast(size - i); + } + else + { + cur_len = NETPLAY_LZO_IN_LEN; + } + + if (cur_len <= 0) + break; // EOF + + if (!file.ReadBytes(in_buffer.data(), cur_len)) + { + PanicAlertT("Error reading file: %s", file_path.c_str()); + return false; + } + + if (lzo1x_1_compress(in_buffer.data(), cur_len, out_buffer.data(), &out_len, wrkmem.data()) != + LZO_E_OK) + { + PanicAlertT("Internal LZO Error - compression failed"); + return false; + } + + // The size of the data to write is 'out_len' + packet << static_cast(out_len); + for (size_t j = 0; j < out_len; j++) + { + packet << out_buffer[j]; + } + + if (cur_len != NETPLAY_LZO_IN_LEN) + break; + + i += cur_len; + } + + // Mark end of data + packet << static_cast(0); + + return true; +} + +bool NetPlayServer::CompressBufferIntoPacket(const std::vector& in_buffer, sf::Packet& packet) +{ + const u64 size = in_buffer.size(); + Common::PacketWriteU64(packet, size); + + if (size == 0) + return true; + + std::vector out_buffer(NETPLAY_LZO_OUT_LEN); + std::vector wrkmem(LZO1X_1_MEM_COMPRESS); + + lzo_uint i = 0; + while (true) + { + lzo_uint32 cur_len = 0; // number of bytes to read + lzo_uint out_len = 0; // number of bytes to write + + if ((i + NETPLAY_LZO_IN_LEN) >= size) + { + cur_len = static_cast(size - i); + } + else + { + cur_len = NETPLAY_LZO_IN_LEN; + } + + if (cur_len <= 0) + break; // end of buffer + + if (lzo1x_1_compress(&in_buffer[i], cur_len, out_buffer.data(), &out_len, wrkmem.data()) != + LZO_E_OK) + { + PanicAlertT("Internal LZO Error - compression failed"); + return false; + } + + // The size of the data to write is 'out_len' + packet << static_cast(out_len); + for (size_t j = 0; j < out_len; j++) + { + packet << out_buffer[j]; + } + + if (cur_len != NETPLAY_LZO_IN_LEN) + break; + + i += cur_len; + } + + // Mark end of data + packet << static_cast(0); + + return true; +} + // called from multiple threads void NetPlayServer::SendToClients(const sf::Packet& packet, const PlayerId skip_pid) { diff --git a/Source/Core/Core/NetPlayServer.h b/Source/Core/Core/NetPlayServer.h index 1dd01ed7ad..9295cc9d7e 100644 --- a/Source/Core/Core/NetPlayServer.h +++ b/Source/Core/Core/NetPlayServer.h @@ -41,6 +41,7 @@ public: void SetNetSettings(const NetSettings& settings); bool StartGame(); + bool RequestStartGame(); PadMappingArray GetPadMapping() const; void SetPadMapping(const PadMappingArray& mappings); @@ -78,6 +79,10 @@ private: bool operator==(const Client& other) const { return this == &other; } }; + bool SyncSaveData(); + bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet); + bool CompressBufferIntoPacket(const std::vector& in_buffer, sf::Packet& packet); + void SendToClients(const sf::Packet& packet, const PlayerId skip_pid = 0); void Send(ENetPeer* socket, const sf::Packet& packet); unsigned int OnConnect(ENetPeer* socket); @@ -102,6 +107,8 @@ private: unsigned int m_target_buffer_size = 0; PadMappingArray m_pad_map; PadMappingArray m_wiimote_map; + unsigned int m_save_data_synced_players = 0; + bool m_start_pending = false; std::map m_players; diff --git a/Source/Core/Core/WiiRoot.cpp b/Source/Core/Core/WiiRoot.cpp index ede9e51752..ef6cea8fe5 100644 --- a/Source/Core/Core/WiiRoot.cpp +++ b/Source/Core/Core/WiiRoot.cpp @@ -26,10 +26,10 @@ namespace Core { -static std::string s_temp_wii_root; - namespace FS = IOS::HLE::FS; +static std::string s_temp_wii_root; + static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs) { const u64 title_id = SConfig::GetInstance().GetTitleID(); @@ -52,7 +52,9 @@ static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs) (Movie::IsMovieActive() && !Movie::IsStartingFromClearSave())) { // Copy the current user's save to the Blank NAND - const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id); + auto* sync_fs = NetPlay::GetWiiSyncFS(); + const auto user_save = + WiiSave::MakeNandStorage(sync_fs ? sync_fs : configured_fs.get(), title_id); const auto session_save = WiiSave::MakeNandStorage(session_fs, title_id); WiiSave::Copy(user_save.get(), session_save.get()); } @@ -62,13 +64,26 @@ void InitializeWiiRoot(bool use_temporary) { if (use_temporary) { - s_temp_wii_root = File::CreateTempDir(); - if (s_temp_wii_root.empty()) - { - ERROR_LOG(IOS_FS, "Could not create temporary directory"); - return; - } + s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP; WARN_LOG(IOS_FS, "Using temporary directory %s for minimal Wii FS", s_temp_wii_root.c_str()); + + // 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(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(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); } else @@ -148,8 +163,11 @@ void InitializeWiiFileSystemContents() void CleanUpWiiFileSystemContents() { - if (s_temp_wii_root.empty() || !SConfig::GetInstance().bEnableMemcardSdWriting) + if (s_temp_wii_root.empty() || !SConfig::GetInstance().bEnableMemcardSdWriting || + NetPlay::GetWiiSyncFS()) + { return; + } const u64 title_id = SConfig::GetInstance().GetTitleID(); @@ -157,6 +175,16 @@ void CleanUpWiiFileSystemContents() const auto session_save = WiiSave::MakeNandStorage(ios->GetFS().get(), title_id); const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured); + + // FS won't write the save if the directory doesn't exist + const std::string title_path = Common::GetTitleDataPath(title_id); + if (!configured_fs->GetMetadata(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path)) + { + configured_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path, 0, + {IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite, + IOS::HLE::FS::Mode::ReadWrite}); + } + const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id); const std::string backup_path = diff --git a/Source/Core/Core/WiiRoot.h b/Source/Core/Core/WiiRoot.h index 8aab1dc25e..c62aaa103f 100644 --- a/Source/Core/Core/WiiRoot.h +++ b/Source/Core/Core/WiiRoot.h @@ -12,4 +12,4 @@ void ShutdownWiiRoot(); // Initialize or clean up the filesystem contents. void InitializeWiiFileSystemContents(); void CleanUpWiiFileSystemContents(); -} +} // namespace Core diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index ab773e9b2d..30fdaef58f 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -1100,6 +1100,9 @@ bool MainWindow::NetPlayJoin() return false; } + if (Settings::Instance().GetNetPlayServer() != nullptr) + Settings::Instance().GetNetPlayServer()->SetNetPlayUI(m_netplay_dialog); + m_netplay_setup_dialog->close(); m_netplay_dialog->show(nickname, is_traversal); diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index d4cfacb605..ce296acef8 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -86,6 +86,7 @@ void NetPlayDialog::CreateMainLayout() m_buffer_size_box = new QSpinBox; m_save_sd_box = new QCheckBox(tr("Write save/SD data")); m_load_wii_box = new QCheckBox(tr("Load Wii Save")); + m_sync_save_data_box = new QCheckBox(tr("Sync Saves")); m_record_input_box = new QCheckBox(tr("Record inputs")); m_reduce_polling_rate_box = new QCheckBox(tr("Reduce Polling Rate")); m_buffer_label = new QLabel(tr("Buffer:")); @@ -95,6 +96,8 @@ void NetPlayDialog::CreateMainLayout() m_game_button->setDefault(false); m_game_button->setAutoDefault(false); + m_sync_save_data_box->setChecked(true); + auto* default_button = new QAction(tr("Calculate MD5 hash"), m_md5_button); auto* menu = new QMenu(this); @@ -140,6 +143,7 @@ void NetPlayDialog::CreateMainLayout() options_widget->addWidget(m_buffer_size_box); options_widget->addWidget(m_save_sd_box); options_widget->addWidget(m_load_wii_box); + options_widget->addWidget(m_sync_save_data_box); options_widget->addWidget(m_record_input_box); options_widget->addWidget(m_reduce_polling_rate_box); options_widget->addWidget(m_quit_button); @@ -305,9 +309,11 @@ void NetPlayDialog::OnStart() settings.m_ReducePollingRate = m_reduce_polling_rate_box->isChecked(); settings.m_EXIDevice[0] = instance.m_EXIDevice[0]; settings.m_EXIDevice[1] = instance.m_EXIDevice[1]; + settings.m_SyncSaveData = m_sync_save_data_box->isChecked(); Settings::Instance().GetNetPlayServer()->SetNetSettings(settings); - Settings::Instance().GetNetPlayServer()->StartGame(); + if (Settings::Instance().GetNetPlayServer()->RequestStartGame()) + SetOptionsEnabled(false); } void NetPlayDialog::reject() @@ -346,6 +352,7 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal) m_start_button->setHidden(!is_hosting); m_save_sd_box->setHidden(!is_hosting); m_load_wii_box->setHidden(!is_hosting); + m_sync_save_data_box->setHidden(!is_hosting); m_reduce_polling_rate_box->setHidden(!is_hosting); m_buffer_size_box->setHidden(!is_hosting); m_buffer_label->setHidden(!is_hosting); @@ -374,7 +381,6 @@ void NetPlayDialog::UpdateGUI() m_player_count = static_cast(players.size()); - int selection_pid = m_players_list->currentItem() ? m_players_list->currentItem()->data(Qt::UserRole).toInt() : -1; @@ -487,6 +493,11 @@ void NetPlayDialog::StopGame() emit Stop(); } +bool NetPlayDialog::IsHosting() const +{ + return Settings::Instance().GetNetPlayServer() != nullptr; +} + void NetPlayDialog::Update() { QueueOnObject(this, &NetPlayDialog::UpdateGUI); @@ -539,19 +550,23 @@ void NetPlayDialog::GameStatusChanged(bool running) if (!running && !m_got_stop_request) Settings::Instance().GetNetPlayClient()->RequestStopGame(); - QueueOnObject(this, [this, running] { - if (Settings::Instance().GetNetPlayServer() != nullptr) - { - m_start_button->setEnabled(!running); - m_game_button->setEnabled(!running); - m_load_wii_box->setEnabled(!running); - m_save_sd_box->setEnabled(!running); - m_assign_ports_button->setEnabled(!running); - m_reduce_polling_rate_box->setEnabled(!running); - } + QueueOnObject(this, [this, running] { SetOptionsEnabled(!running); }); +} - m_record_input_box->setEnabled(!running); - }); +void NetPlayDialog::SetOptionsEnabled(bool enabled) +{ + if (Settings::Instance().GetNetPlayServer() != nullptr) + { + m_start_button->setEnabled(enabled); + m_game_button->setEnabled(enabled); + m_load_wii_box->setEnabled(enabled); + m_save_sd_box->setEnabled(enabled); + m_sync_save_data_box->setEnabled(enabled); + m_assign_ports_button->setEnabled(enabled); + m_reduce_polling_rate_box->setEnabled(enabled); + } + + m_record_input_box->setEnabled(enabled); } void NetPlayDialog::OnMsgStartGame() @@ -618,6 +633,11 @@ void NetPlayDialog::OnTraversalError(TraversalClient::FailureReason error) }); } +void NetPlayDialog::OnSaveDataSyncFailure() +{ + QueueOnObject(this, [this] { SetOptionsEnabled(true); }); +} + bool NetPlayDialog::IsRecording() { std::optional is_recording = RunOnObject(m_record_input_box, &QCheckBox::isChecked); @@ -628,7 +648,7 @@ bool NetPlayDialog::IsRecording() std::string NetPlayDialog::FindGame(const std::string& game) { - std::optional path = RunOnObject(this, [this, game] { + std::optional path = RunOnObject(this, [this, &game] { for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) { if (m_game_list_model->GetUniqueIdentifier(i).toStdString() == game) @@ -641,6 +661,22 @@ std::string NetPlayDialog::FindGame(const std::string& game) return std::string(""); } +std::shared_ptr NetPlayDialog::FindGameFile(const std::string& game) +{ + std::optional> game_file = + RunOnObject(this, [this, &game] { + for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) + { + if (m_game_list_model->GetUniqueIdentifier(i).toStdString() == game) + return m_game_list_model->GetGameFile(i); + } + return static_cast>(nullptr); + }); + if (game_file) + return *game_file; + return nullptr; +} + void NetPlayDialog::ShowMD5Dialog(const std::string& file_identifier) { QueueOnObject(this, [this, file_identifier] { diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h index 92ea74f38f..1603bc40a8 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h @@ -38,6 +38,7 @@ public: // NetPlayUI methods void BootGame(const std::string& filename) override; void StopGame() override; + bool IsHosting() const override; void Update() override; void AppendChat(const std::string& msg) override; @@ -50,8 +51,11 @@ public: void OnConnectionLost() override; void OnConnectionError(const std::string& message) override; void OnTraversalError(TraversalClient::FailureReason error) override; + void OnSaveDataSyncFailure() override; + bool IsRecording() override; std::string FindGame(const std::string& game) override; + std::shared_ptr FindGameFile(const std::string& game) override; void ShowMD5Dialog(const std::string& file_identifier) override; void SetMD5Progress(int pid, int progress) override; void SetMD5Result(int pid, const std::string& result) override; @@ -71,6 +75,7 @@ private: int duration = OSD::Duration::NORMAL); void UpdateGUI(); void GameStatusChanged(bool running); + void SetOptionsEnabled(bool enabled); void SetGame(const QString& game_path); @@ -97,6 +102,7 @@ private: QSpinBox* m_buffer_size_box; QCheckBox* m_save_sd_box; QCheckBox* m_load_wii_box; + QCheckBox* m_sync_save_data_box; QCheckBox* m_record_input_box; QCheckBox* m_reduce_polling_rate_box; QPushButton* m_quit_button; diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.cpp b/Source/Core/DolphinQt/Settings/GameCubePane.cpp index 7b50677cfe..4dff22c4bf 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp +++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp @@ -16,8 +16,10 @@ #include #include "Common/CommonPaths.h" +#include "Common/Config/Config.h" #include "Common/FileUtil.h" +#include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/HW/EXI/EXI.h" @@ -200,8 +202,8 @@ void GameCubePane::OnConfigPressed(int slot) if (other_slot_memcard) { QString path_b = - QFileInfo(QString::fromStdString(slot == 0 ? SConfig::GetInstance().m_strMemoryCardB : - SConfig::GetInstance().m_strMemoryCardA)) + QFileInfo(QString::fromStdString(slot == 0 ? Config::Get(Config::MAIN_MEMCARD_B_PATH) : + Config::Get(Config::MAIN_MEMCARD_A_PATH))) .absoluteFilePath(); if (path_abs == path_b) @@ -216,8 +218,8 @@ void GameCubePane::OnConfigPressed(int slot) if (memcard) { path_old = - QFileInfo(QString::fromStdString(slot == 0 ? SConfig::GetInstance().m_strMemoryCardA : - SConfig::GetInstance().m_strMemoryCardB)) + QFileInfo(QString::fromStdString(slot == 0 ? Config::Get(Config::MAIN_MEMCARD_A_PATH) : + Config::Get(Config::MAIN_MEMCARD_B_PATH))) .absoluteFilePath(); } else @@ -231,11 +233,11 @@ void GameCubePane::OnConfigPressed(int slot) { if (slot == SLOT_A_INDEX) { - SConfig::GetInstance().m_strMemoryCardA = path_abs.toStdString(); + Config::SetBase(Config::MAIN_MEMCARD_A_PATH, path_abs.toStdString()); } else { - SConfig::GetInstance().m_strMemoryCardB = path_abs.toStdString(); + Config::SetBase(Config::MAIN_MEMCARD_B_PATH, path_abs.toStdString()); } } else