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.
This commit is contained in:
Techjar 2018-07-04 17:01:50 -04:00
parent f5e8af7b6c
commit 4407854e9c
32 changed files with 1250 additions and 209 deletions

View File

@ -36,6 +36,7 @@ add_library(common
QoSSession.cpp QoSSession.cpp
Random.cpp Random.cpp
SDCardUtil.cpp SDCardUtil.cpp
SFMLHelper.cpp
SettingsHandler.cpp SettingsHandler.cpp
StringUtil.cpp StringUtil.cpp
SymbolDB.cpp SymbolDB.cpp

View File

@ -147,6 +147,7 @@
<ClInclude Include="Result.h" /> <ClInclude Include="Result.h" />
<ClInclude Include="ScopeGuard.h" /> <ClInclude Include="ScopeGuard.h" />
<ClInclude Include="SDCardUtil.h" /> <ClInclude Include="SDCardUtil.h" />
<ClInclude Include="SFMLHelper.h" />
<ClInclude Include="Semaphore.h" /> <ClInclude Include="Semaphore.h" />
<ClInclude Include="SettingsHandler.h" /> <ClInclude Include="SettingsHandler.h" />
<ClInclude Include="SPSCQueue.h" /> <ClInclude Include="SPSCQueue.h" />
@ -210,6 +211,7 @@
<ClCompile Include="QoSSession.cpp" /> <ClCompile Include="QoSSession.cpp" />
<ClCompile Include="Random.cpp" /> <ClCompile Include="Random.cpp" />
<ClCompile Include="SDCardUtil.cpp" /> <ClCompile Include="SDCardUtil.cpp" />
<ClCompile Include="SFMLHelper.cpp" />
<ClCompile Include="SettingsHandler.cpp" /> <ClCompile Include="SettingsHandler.cpp" />
<ClCompile Include="StringUtil.cpp" /> <ClCompile Include="StringUtil.cpp" />
<ClCompile Include="SymbolDB.cpp" /> <ClCompile Include="SymbolDB.cpp" />

View File

@ -68,6 +68,7 @@
<ClInclude Include="Result.h" /> <ClInclude Include="Result.h" />
<ClInclude Include="ScopeGuard.h" /> <ClInclude Include="ScopeGuard.h" />
<ClInclude Include="SDCardUtil.h" /> <ClInclude Include="SDCardUtil.h" />
<ClInclude Include="SFMLHelper.h" />
<ClInclude Include="SettingsHandler.h" /> <ClInclude Include="SettingsHandler.h" />
<ClInclude Include="SPSCQueue.h" /> <ClInclude Include="SPSCQueue.h" />
<ClInclude Include="StringUtil.h" /> <ClInclude Include="StringUtil.h" />
@ -299,6 +300,7 @@
<ClCompile Include="Profiler.cpp" /> <ClCompile Include="Profiler.cpp" />
<ClCompile Include="Random.cpp" /> <ClCompile Include="Random.cpp" />
<ClCompile Include="SDCardUtil.cpp" /> <ClCompile Include="SDCardUtil.cpp" />
<ClCompile Include="SFMLHelper.cpp" />
<ClCompile Include="SettingsHandler.cpp" /> <ClCompile Include="SettingsHandler.cpp" />
<ClCompile Include="StringUtil.cpp" /> <ClCompile Include="StringUtil.cpp" />
<ClCompile Include="SymbolDB.cpp" /> <ClCompile Include="SymbolDB.cpp" />

View File

@ -109,6 +109,7 @@
#define GC_SRAM "SRAM.raw" #define GC_SRAM "SRAM.raw"
#define GC_MEMCARDA "MemoryCardA" #define GC_MEMCARDA "MemoryCardA"
#define GC_MEMCARDB "MemoryCardB" #define GC_MEMCARDB "MemoryCardB"
#define GC_MEMCARD_NETPLAY "NetPlayTemp"
#define WII_STATE "state.dat" #define WII_STATE "state.dat"

View File

@ -0,0 +1,38 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "Common/SFMLHelper.h"
#include <SFML/Network/Packet.hpp>
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<u64>(high) << 32);
}
void PacketWriteU64(sf::Packet& packet, const u64 value)
{
packet << static_cast<u32>(value) << static_cast<u32>(value >> 32);
}
} // namespace Common

View File

@ -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 <typename value_type>
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

View File

@ -36,6 +36,12 @@ const ConfigInfo<std::string> MAIN_MEMCARD_A_PATH{{System::Main, "Core", "Memcar
const ConfigInfo<std::string> MAIN_MEMCARD_B_PATH{{System::Main, "Core", "MemcardBPath"}, ""}; const ConfigInfo<std::string> MAIN_MEMCARD_B_PATH{{System::Main, "Core", "MemcardBPath"}, ""};
const ConfigInfo<std::string> MAIN_AGP_CART_A_PATH{{System::Main, "Core", "AgpCartAPath"}, ""}; const ConfigInfo<std::string> MAIN_AGP_CART_A_PATH{{System::Main, "Core", "AgpCartAPath"}, ""};
const ConfigInfo<std::string> MAIN_AGP_CART_B_PATH{{System::Main, "Core", "AgpCartBPath"}, ""}; const ConfigInfo<std::string> MAIN_AGP_CART_B_PATH{{System::Main, "Core", "AgpCartBPath"}, ""};
const ConfigInfo<std::string> MAIN_GCI_FOLDER_A_PATH_OVERRIDE{
{System::Main, "Core", "GCIFolderAPathOverride"}, ""};
const ConfigInfo<std::string> MAIN_GCI_FOLDER_B_PATH_OVERRIDE{
{System::Main, "Core", "GCIFolderBPathOverride"}, ""};
const ConfigInfo<bool> MAIN_GCI_FOLDER_CURRENT_GAME_ONLY{
{System::Main, "Core", "GCIFolderCurrentGameOnly"}, false};
const ConfigInfo<int> MAIN_SLOT_A{{System::Main, "Core", "SlotA"}, const ConfigInfo<int> MAIN_SLOT_A{{System::Main, "Core", "SlotA"},
ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER}; ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER};
const ConfigInfo<int> MAIN_SLOT_B{{System::Main, "Core", "SlotB"}, const ConfigInfo<int> MAIN_SLOT_B{{System::Main, "Core", "SlotB"},

View File

@ -37,6 +37,9 @@ extern const ConfigInfo<std::string> MAIN_MEMCARD_A_PATH;
extern const ConfigInfo<std::string> MAIN_MEMCARD_B_PATH; extern const ConfigInfo<std::string> MAIN_MEMCARD_B_PATH;
extern const ConfigInfo<std::string> MAIN_AGP_CART_A_PATH; extern const ConfigInfo<std::string> MAIN_AGP_CART_A_PATH;
extern const ConfigInfo<std::string> MAIN_AGP_CART_B_PATH; extern const ConfigInfo<std::string> MAIN_AGP_CART_B_PATH;
extern const ConfigInfo<std::string> MAIN_GCI_FOLDER_A_PATH_OVERRIDE;
extern const ConfigInfo<std::string> MAIN_GCI_FOLDER_B_PATH_OVERRIDE;
extern const ConfigInfo<bool> MAIN_GCI_FOLDER_CURRENT_GAME_ONLY;
extern const ConfigInfo<int> MAIN_SLOT_A; extern const ConfigInfo<int> MAIN_SLOT_A;
extern const ConfigInfo<int> MAIN_SLOT_B; extern const ConfigInfo<int> MAIN_SLOT_B;
extern const ConfigInfo<int> MAIN_SERIAL_PORT_1; extern const ConfigInfo<int> MAIN_SERIAL_PORT_1;

View File

@ -6,7 +6,9 @@
#include <memory> #include <memory>
#include "Common/CommonPaths.h"
#include "Common/Config/Config.h" #include "Common/Config/Config.h"
#include "Common/FileUtil.h"
#include "Core/Config/MainSettings.h" #include "Core/Config/MainSettings.h"
#include "Core/Config/SYSCONFSettings.h" #include "Core/Config/SYSCONFSettings.h"
#include "Core/NetPlayProto.h" #include "Core/NetPlayProto.h"
@ -39,6 +41,23 @@ public:
layer->Set(Config::SYSCONF_PROGRESSIVE_SCAN, m_settings.m_ProgressiveScan); layer->Set(Config::SYSCONF_PROGRESSIVE_SCAN, m_settings.m_ProgressiveScan);
layer->Set(Config::SYSCONF_PAL60, m_settings.m_PAL60); 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 void Save(Config::Layer* layer) override

View File

@ -227,8 +227,6 @@ void SConfig::SaveCoreSettings(IniFile& ini)
core->Set("AudioLatency", iLatency); core->Set("AudioLatency", iLatency);
core->Set("AudioStretch", m_audio_stretch); core->Set("AudioStretch", m_audio_stretch);
core->Set("AudioStretchMaxLatency", m_audio_stretch_max_latency); 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("AgpCartAPath", m_strGbaCartA);
core->Set("AgpCartBPath", m_strGbaCartB); core->Set("AgpCartBPath", m_strGbaCartB);
core->Set("SlotA", m_EXIDevice[0]); core->Set("SlotA", m_EXIDevice[0]);
@ -505,8 +503,6 @@ void SConfig::LoadCoreSettings(IniFile& ini)
core->Get("AudioLatency", &iLatency, 20); core->Get("AudioLatency", &iLatency, 20);
core->Get("AudioStretch", &m_audio_stretch, false); core->Get("AudioStretch", &m_audio_stretch, false);
core->Get("AudioStretchMaxLatency", &m_audio_stretch_max_latency, 80); 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("AgpCartAPath", &m_strGbaCartA);
core->Get("AgpCartBPath", &m_strGbaCartB); core->Get("AgpCartBPath", &m_strGbaCartB);
core->Get("SlotA", (int*)&m_EXIDevice[0], ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER); core->Get("SlotA", (int*)&m_EXIDevice[0], ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER);
@ -947,62 +943,12 @@ bool SConfig::SetPathsAndGameMetadata(const BootParameters& boot)
// Set up paths // Set up paths
const std::string region_dir = GetDirectoryForRegion(ToGameCubeRegion(m_region)); 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_strSRAM = File::GetUserPath(F_GCSRAM_IDX);
m_strBootROM = GetBootROMPath(region_dir); m_strBootROM = GetBootROMPath(region_dir);
return true; 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 DiscIO::Language SConfig::GetCurrentLanguage(bool wii) const
{ {
int language_value; int language_value;

View File

@ -212,7 +212,6 @@ struct SConfig
static const char* GetDirectoryForRegion(DiscIO::Region region); static const char* GetDirectoryForRegion(DiscIO::Region region);
std::string GetBootROMPath(const std::string& region_directory) const; std::string GetBootROMPath(const std::string& region_directory) const;
bool SetPathsAndGameMetadata(const BootParameters& boot); bool SetPathsAndGameMetadata(const BootParameters& boot);
void CheckMemcardPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA);
DiscIO::Language GetCurrentLanguage(bool wii) const; DiscIO::Language GetCurrentLanguage(bool wii) const;
IniFile LoadDefaultGameIni() const; IniFile LoadDefaultGameIni() const;
@ -223,8 +222,6 @@ struct SConfig
static IniFile LoadLocalGameIni(const std::string& id, std::optional<u16> revision); static IniFile LoadLocalGameIni(const std::string& id, std::optional<u16> revision);
static IniFile LoadGameIni(const std::string& id, std::optional<u16> revision); static IniFile LoadGameIni(const std::string& id, std::optional<u16> revision);
std::string m_strMemoryCardA;
std::string m_strMemoryCardB;
std::string m_strGbaCartA; std::string m_strGbaCartA;
std::string m_strGbaCartB; std::string m_strGbaCartB;
ExpansionInterface::TEXIDevices m_EXIDevice[3]; ExpansionInterface::TEXIDevices m_EXIDevice[3];

View File

@ -448,6 +448,7 @@
<ClInclude Include="HW\WiimoteReal\WiimoteReal.h" /> <ClInclude Include="HW\WiimoteReal\WiimoteReal.h" />
<ClInclude Include="HW\WiimoteReal\WiimoteRealBase.h" /> <ClInclude Include="HW\WiimoteReal\WiimoteRealBase.h" />
<ClInclude Include="HW\WiiSave.h" /> <ClInclude Include="HW\WiiSave.h" />
<ClInclude Include="HW\WiiSaveStructs.h" />
<ClInclude Include="HW\WII_IPC.h" /> <ClInclude Include="HW\WII_IPC.h" />
<ClInclude Include="IOS\Device.h" /> <ClInclude Include="IOS\Device.h" />
<ClInclude Include="IOS\DeviceStub.h" /> <ClInclude Include="IOS\DeviceStub.h" />

View File

@ -1272,6 +1272,9 @@
<ClInclude Include="HW\WiiSave.h"> <ClInclude Include="HW\WiiSave.h">
<Filter>HW %28Flipper/Hollywood%29</Filter> <Filter>HW %28Flipper/Hollywood%29</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="HW\WiiSaveStructs.h">
<Filter>HW %28Flipper/Hollywood%29</Filter>
</ClInclude>
<ClInclude Include="DSP\DSPAssembler.h"> <ClInclude Include="DSP\DSPAssembler.h">
<Filter>DSPCore</Filter> <Filter>DSPCore</Filter>
</ClInclude> </ClInclude>

View File

@ -13,12 +13,14 @@
#include "Common/ChunkFile.h" #include "Common/ChunkFile.h"
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "Common/IniFile.h" #include "Common/IniFile.h"
#include "Common/Logging/Log.h" #include "Common/Logging/Log.h"
#include "Common/NandPaths.h" #include "Common/NandPaths.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Core/CommonTitles.h" #include "Core/CommonTitles.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h" #include "Core/ConfigManager.h"
#include "Core/CoreTiming.h" #include "Core/CoreTiming.h"
#include "Core/HW/EXI/EXI.h" #include "Core/HW/EXI/EXI.h"
@ -31,6 +33,7 @@
#include "Core/HW/Sram.h" #include "Core/HW/Sram.h"
#include "Core/HW/SystemTimers.h" #include "Core/HW/SystemTimers.h"
#include "Core/Movie.h" #include "Core/Movie.h"
#include "Core/NetPlayProto.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
namespace ExpansionInterface namespace ExpansionInterface
@ -169,24 +172,46 @@ void CEXIMemoryCard::SetupGciFolder(u16 sizeMb)
std::string strDirectoryName = File::GetUserPath(D_GCUSER_IDX); std::string strDirectoryName = File::GetUserPath(D_GCUSER_IDX);
bool migrate = true;
if (Movie::IsPlayingInput() && Movie::IsConfigSaved() && Movie::IsUsingMemcard(card_index) && if (Movie::IsPlayingInput() && Movie::IsConfigSaved() && Movie::IsUsingMemcard(card_index) &&
Movie::IsStartingFromClearSave()) Movie::IsStartingFromClearSave())
{
strDirectoryName += "Movie" DIR_SEP; strDirectoryName += "Movie" DIR_SEP;
migrate = false;
}
strDirectoryName = strDirectoryName + SConfig::GetDirectoryForRegion(region) + DIR_SEP + const std::string path_override =
StringFromFormat("Card %c", 'A' + card_index); 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); 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()) else if (!file_info.IsDirectory())
{ {
if (File::Rename(strDirectoryName, strDirectoryName + ".original")) if (File::Rename(strDirectoryName, strDirectoryName + ".original"))
{ {
PanicAlertT("%s was not a directory, moved to *.original", strDirectoryName.c_str()); 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 else // we tried but the user wants to crash
{ {
@ -204,17 +229,21 @@ void CEXIMemoryCard::SetupGciFolder(u16 sizeMb)
void CEXIMemoryCard::SetupRawMemcard(u16 sizeMb) void CEXIMemoryCard::SetupRawMemcard(u16 sizeMb)
{ {
std::string filename = (card_index == 0) ? SConfig::GetInstance().m_strMemoryCardA : const bool is_slot_a = card_index == 0;
SConfig::GetInstance().m_strMemoryCardB; 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) && if (Movie::IsPlayingInput() && Movie::IsConfigSaved() && Movie::IsUsingMemcard(card_index) &&
Movie::IsStartingFromClearSave()) Movie::IsStartingFromClearSave())
filename = File::GetUserPath(D_GCUSER_IDX) + filename =
StringFromFormat("Movie%s.raw", (card_index == 0) ? "A" : "B"); 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) if (sizeMb == MemCard251Mb)
{
filename.insert(filename.find_last_of("."), ".251"); filename.insert(filename.find_last_of("."), ".251");
}
memorycard = std::make_unique<MemoryCard>(filename, card_index, sizeMb); memorycard = std::make_unique<MemoryCard>(filename, card_index, sizeMb);
} }

View File

@ -16,6 +16,7 @@
#include "Common/Assert.h" #include "Common/Assert.h"
#include "Common/ChunkFile.h" #include "Common/ChunkFile.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/File.h" #include "Common/File.h"
#include "Common/FileSearch.h" #include "Common/FileSearch.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
@ -23,8 +24,10 @@
#include "Common/MsgHandler.h" #include "Common/MsgHandler.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Common/Thread.h" #include "Common/Thread.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h" #include "Core/ConfigManager.h"
#include "Core/Core.h" #include "Core/Core.h"
#include "Core/NetPlayProto.h"
const int NO_INDEX = -1; const int NO_INDEX = -1;
static const char* MC_HDR = "MC_SYSTEM_AREA"; 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; 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<std::string> GCMemcardDirectory::GetFileNamesForGameID(const std::string& directory,
const std::string& game_id)
{
std::vector<std::string> filenames;
u32 game_code = 0;
if (game_id.length() >= 4 && game_id != "00000000")
game_code = BE32(reinterpret_cast<const u8*>(game_id.c_str()));
std::vector<std::string> 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, GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, int slot, u16 size_mbits,
bool shift_jis, int game_id) bool shift_jis, int game_id)
: MemoryCardBase(slot, size_mbits), m_game_id(game_id), m_last_block(-1), : 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()); m_save_directory.c_str());
break; 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) if (index != NO_INDEX)
{ {
m_loaded_saves.push_back(m_saves.at(index).m_gci_header.GCI_FileName()); 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) void MigrateFromMemcardFile(const std::string& directory_name, int card_index)
{ {
File::CreateFullPath(directory_name); File::CreateFullPath(directory_name);
std::string ini_memcard = (card_index == 0) ? SConfig::GetInstance().m_strMemoryCardA : std::string ini_memcard = (card_index == 0) ? Config::Get(Config::MAIN_MEMCARD_A_PATH) :
SConfig::GetInstance().m_strMemoryCardB; Config::Get(Config::MAIN_MEMCARD_B_PATH);
if (File::Exists(ini_memcard)) if (File::Exists(ini_memcard))
{ {
GCMemcard memcard(ini_memcard.c_str()); GCMemcard memcard(ini_memcard.c_str());

View File

@ -28,6 +28,8 @@ public:
GCMemcardDirectory(GCMemcardDirectory&&) = default; GCMemcardDirectory(GCMemcardDirectory&&) = default;
GCMemcardDirectory& operator=(GCMemcardDirectory&&) = default; GCMemcardDirectory& operator=(GCMemcardDirectory&&) = default;
static std::vector<std::string> GetFileNamesForGameID(const std::string& directory,
const std::string& game_id);
void FlushToFile(); void FlushToFile();
void FlushThread(); void FlushThread();
s32 Read(u32 src_address, s32 length, u8* dest_address) override; s32 Read(u32 src_address, s32 length, u8* dest_address) override;

View File

@ -12,10 +12,12 @@
#include <thread> #include <thread>
#include "Common/ChunkFile.h" #include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/File.h" #include "Common/File.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "Common/Logging/Log.h" #include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Common/Thread.h" #include "Common/Thread.h"
#include "Core/ConfigManager.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() void MemoryCard::FlushThread()
{ {
if (!SConfig::GetInstance().bEnableMemcardSdWriting) if (!SConfig::GetInstance().bEnableMemcardSdWriting)

View File

@ -19,6 +19,7 @@ class MemoryCard : public MemoryCardBase
public: public:
MemoryCard(const std::string& filename, int card_index, u16 size_mbits = MemCard2043Mb); MemoryCard(const std::string& filename, int card_index, u16 size_mbits = MemCard2043Mb);
~MemoryCard(); ~MemoryCard();
static void CheckPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA);
void FlushThread(); void FlushThread();
void MakeDirty(); void MakeDirty();

View File

@ -32,6 +32,7 @@
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Common/Swap.h" #include "Common/Swap.h"
#include "Core/CommonTitles.h" #include "Core/CommonTitles.h"
#include "Core/HW/WiiSaveStructs.h"
#include "Core/IOS/ES/ES.h" #include "Core/IOS/ES/ES.h"
#include "Core/IOS/FS/FileSystem.h" #include "Core/IOS/FS/FileSystem.h"
#include "Core/IOS/IOS.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}}; 0x45, 0x1A, 0x57, 0x93}};
constexpr u32 s_ng_id = 0x0403AC68; 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<u64> tid;
Common::BigEndianValue<u32> banner_size; // (0x72A0 or 0xF0A0, also seen 0xBAA0)
u8 permissions;
u8 unk1; // maybe permissions is a be16
std::array<u8, 0x10> md5; // md5 of plaintext header with md5 blanker applied
Common::BigEndianValue<u16> unk2;
u8 banner[FULL_BNR_MAX];
};
static_assert(sizeof(Header) == 0xf0c0, "Header has an incorrect size");
struct BkHeader
{
Common::BigEndianValue<u32> size; // 0x00000070
// u16 magic; // 'Bk'
// u16 magic2; // or version (0x0001)
Common::BigEndianValue<u32> magic; // 0x426B0001
Common::BigEndianValue<u32> ngid;
Common::BigEndianValue<u32> number_of_files;
Common::BigEndianValue<u32> size_of_files;
Common::BigEndianValue<u32> unk1;
Common::BigEndianValue<u32> unk2;
Common::BigEndianValue<u32> total_size;
std::array<u8, 64> unk3;
Common::BigEndianValue<u64> tid;
std::array<u8, 6> mac_address;
std::array<u8, 0x12> padding;
};
static_assert(sizeof(BkHeader) == 0x80, "BkHeader has an incorrect size");
struct FileHDR
{
Common::BigEndianValue<u32> magic; // 0x03adf17e
Common::BigEndianValue<u32> size;
u8 permissions;
u8 attrib;
u8 type; // (1=file, 2=directory)
std::array<char, 0x40> name;
std::array<u8, 5> padding;
std::array<u8, 0x10> iv;
std::array<u8, 0x20> 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<std::optional<std::vector<u8>>> data;
};
virtual ~Storage() = default;
virtual bool SaveExists() { return true; }
virtual std::optional<Header> ReadHeader() = 0;
virtual std::optional<BkHeader> ReadBkHeader() = 0;
virtual std::optional<std::vector<SaveFile>> ReadFiles() = 0;
virtual bool WriteHeader(const Header& header) = 0;
virtual bool WriteBkHeader(const BkHeader& bk_header) = 0;
virtual bool WriteFiles(const std::vector<SaveFile>& files) = 0;
};
void StorageDeleter::operator()(Storage* p) const void StorageDeleter::operator()(Storage* p) const
{ {
delete p; delete p;

View File

@ -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 <segher@kernel.crashing.org>
// Licensed under the terms of the GNU GPL, version 2
// http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
#pragma once
#include <array>
#include <optional>
#include <string>
#include <vector>
#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<u64> tid;
Common::BigEndianValue<u32> banner_size; // (0x72A0 or 0xF0A0, also seen 0xBAA0)
u8 permissions;
u8 unk1; // maybe permissions is a be16
std::array<u8, 0x10> md5; // md5 of plaintext header with md5 blanker applied
Common::BigEndianValue<u16> unk2;
u8 banner[FULL_BNR_MAX];
};
static_assert(sizeof(Header) == 0xf0c0, "Header has an incorrect size");
struct BkHeader
{
Common::BigEndianValue<u32> size; // 0x00000070
// u16 magic; // 'Bk'
// u16 magic2; // or version (0x0001)
Common::BigEndianValue<u32> magic; // 0x426B0001
Common::BigEndianValue<u32> ngid;
Common::BigEndianValue<u32> number_of_files;
Common::BigEndianValue<u32> size_of_files;
Common::BigEndianValue<u32> unk1;
Common::BigEndianValue<u32> unk2;
Common::BigEndianValue<u32> total_size;
std::array<u8, 64> unk3;
Common::BigEndianValue<u64> tid;
std::array<u8, 6> mac_address;
std::array<u8, 0x12> padding;
};
static_assert(sizeof(BkHeader) == 0x80, "BkHeader has an incorrect size");
struct FileHDR
{
Common::BigEndianValue<u32> magic; // 0x03adf17e
Common::BigEndianValue<u32> size;
u8 permissions;
u8 attrib;
u8 type; // (1=file, 2=directory)
std::array<char, 0x40> name;
std::array<u8, 5> padding;
std::array<u8, 0x10> iv;
std::array<u8, 0x20> 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<std::optional<std::vector<u8>>> data;
};
virtual ~Storage() = default;
virtual bool SaveExists() { return true; }
virtual std::optional<Header> ReadHeader() = 0;
virtual std::optional<BkHeader> ReadBkHeader() = 0;
virtual std::optional<std::vector<SaveFile>> ReadFiles() = 0;
virtual bool WriteHeader(const Header& header) = 0;
virtual bool WriteBkHeader(const BkHeader& bk_header) = 0;
virtual bool WriteFiles(const std::vector<SaveFile>& files) = 0;
};
} // namespace WiiSave

View File

@ -1398,7 +1398,7 @@ void GetSettings()
} }
else else
{ {
s_bClearSave = !File::Exists(SConfig::GetInstance().m_strMemoryCardA); s_bClearSave = !File::Exists(Config::Get(Config::MAIN_MEMCARD_A_PATH));
} }
s_memcards |= s_memcards |=
(SConfig::GetInstance().m_EXIDevice[0] == ExpansionInterface::EXIDEVICE_MEMORYCARD || (SConfig::GetInstance().m_EXIDevice[0] == ExpansionInterface::EXIDEVICE_MEMORYCARD ||
@ -1491,4 +1491,4 @@ void Shutdown()
s_currentInputCount = s_totalInputCount = s_totalFrames = s_tickCountAtLastInput = 0; s_currentInputCount = s_totalInputCount = s_totalFrames = s_tickCountAtLastInput = 0;
s_temp_input.clear(); s_temp_input.clear();
} }
}; } // namespace Movie

View File

@ -13,18 +13,23 @@
#include <sstream> #include <sstream>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
#include <vector>
#include <lzo/lzo1x.h>
#include <mbedtls/md5.h> #include <mbedtls/md5.h>
#include "Common/Assert.h" #include "Common/Assert.h"
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/ENetUtil.h" #include "Common/ENetUtil.h"
#include "Common/File.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "Common/Logging/Log.h" #include "Common/Logging/Log.h"
#include "Common/MD5.h" #include "Common/MD5.h"
#include "Common/MsgHandler.h" #include "Common/MsgHandler.h"
#include "Common/NandPaths.h"
#include "Common/QoSSession.h" #include "Common/QoSSession.h"
#include "Common/SFMLHelper.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Common/Timer.h" #include "Common/Timer.h"
#include "Common/Version.h" #include "Common/Version.h"
@ -34,12 +39,19 @@
#include "Core/HW/SI/SI.h" #include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_DeviceGCController.h" #include "Core/HW/SI/SI_DeviceGCController.h"
#include "Core/HW/Sram.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/WiimoteEmu/WiimoteEmu.h"
#include "Core/HW/WiimoteReal/WiimoteReal.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/USB/Bluetooth/BTEmu.h"
#include "Core/IOS/Uids.h"
#include "Core/Movie.h" #include "Core/Movie.h"
#include "Core/PowerPC/PowerPC.h" #include "Core/PowerPC/PowerPC.h"
#include "Core/WiiRoot.h"
#include "InputCommon/GCAdapter.h" #include "InputCommon/GCAdapter.h"
#include "UICommon/GameFile.h"
#include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoConfig.h" #include "VideoCommon/VideoConfig.h"
@ -47,6 +59,7 @@ namespace NetPlay
{ {
static std::mutex crit_netplay_client; static std::mutex crit_netplay_client;
static NetPlayClient* netplay_client = nullptr; static NetPlayClient* netplay_client = nullptr;
static std::unique_ptr<IOS::HLE::FS::FileSystem> s_wii_sync_fs;
// called from ---GUI--- thread // called from ---GUI--- thread
NetPlayClient::~NetPlayClient() NetPlayClient::~NetPlayClient()
@ -467,10 +480,11 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
packet >> tmp; packet >> tmp;
m_net_settings.m_EXIDevice[1] = static_cast<ExpansionInterface::TEXIDevices>(tmp); m_net_settings.m_EXIDevice[1] = static_cast<ExpansionInterface::TEXIDevices>(tmp);
u32 time_low, time_high; g_netplay_initial_rtc = Common::PacketReadU64(packet);
packet >> time_low;
packet >> time_high; packet >> m_net_settings.m_SyncSaveData;
g_netplay_initial_rtc = time_low | ((u64)time_high << 32); packet >> m_net_settings.m_SaveDataRegion;
m_net_settings.m_IsHosting = m_dialog->IsHosting();
} }
m_dialog->OnMsgStartGame(); m_dialog->OnMsgStartGame();
@ -553,6 +567,194 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
} }
break; 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<IOS::HLE::FS::HostFileSystem>(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<WiiSave::Storage::SaveFile> 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<WiiSave::Storage::SaveFile::Type>(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: case NP_MSG_COMPUTE_MD5:
{ {
std::string file_identifier; std::string file_identifier;
@ -895,6 +1097,120 @@ bool NetPlayClient::StartGame(const std::string& path)
return true; 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<MessageId>(NP_MSG_SYNC_SAVE_DATA);
response_packet << static_cast<MessageId>(SYNC_SAVE_DATA_SUCCESS);
Send(response_packet);
}
}
else
{
sf::Packet response_packet;
response_packet << static_cast<MessageId>(NP_MSG_SYNC_SAVE_DATA);
response_packet << static_cast<MessageId>(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<u8> in_buffer(NETPLAY_LZO_OUT_LEN);
std::vector<u8> 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<std::vector<u8>> NetPlayClient::DecompressPacketIntoBuffer(sf::Packet& packet)
{
u64 size = Common::PacketReadU64(packet);
;
std::vector<u8> out_buffer(size);
if (size == 0)
return out_buffer;
std::vector<u8> 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 // called from ---GUI--- thread
bool NetPlayClient::ChangeGame(const std::string&) bool NetPlayClient::ChangeGame(const std::string&)
{ {
@ -1178,6 +1494,8 @@ bool NetPlayClient::StopGame()
// stop game // stop game
m_dialog->StopGame(); m_dialog->StopGame();
ClearWiiSyncFS();
return true; return true;
} }
@ -1278,8 +1596,7 @@ void NetPlayClient::SendTimeBase()
sf::Packet packet; sf::Packet packet;
packet << static_cast<MessageId>(NP_MSG_TIMEBASE); packet << static_cast<MessageId>(NP_MSG_TIMEBASE);
packet << static_cast<u32>(timebase); Common::PacketWriteU64(packet, timebase);
packet << static_cast<u32>(timebase << 32);
packet << netplay_client->m_timebase_frame; packet << netplay_client->m_timebase_frame;
netplay_client->SendAsync(std::move(packet)); netplay_client->SendAsync(std::move(packet));
@ -1358,6 +1675,26 @@ const NetSettings& GetNetSettings()
return netplay_client->GetNetSettings(); return netplay_client->GetNetSettings();
} }
IOS::HLE::FS::FileSystem* GetWiiSyncFS()
{
return s_wii_sync_fs.get();
}
void SetWiiSyncFS(std::unique_ptr<IOS::HLE::FS::FileSystem> 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) void NetPlay_Enable(NetPlayClient* const np)
{ {
std::lock_guard<std::mutex> lk(crit_netplay_client); std::lock_guard<std::mutex> lk(crit_netplay_client);

View File

@ -7,10 +7,13 @@
#include <SFML/Network/Packet.hpp> #include <SFML/Network/Packet.hpp>
#include <array> #include <array>
#include <map> #include <map>
#include <memory>
#include <mutex> #include <mutex>
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/Event.h" #include "Common/Event.h"
#include "Common/SPSCQueue.h" #include "Common/SPSCQueue.h"
@ -18,6 +21,11 @@
#include "Core/NetPlayProto.h" #include "Core/NetPlayProto.h"
#include "InputCommon/GCPadStatus.h" #include "InputCommon/GCPadStatus.h"
namespace UICommon
{
class GameFile;
}
namespace NetPlay namespace NetPlay
{ {
class NetPlayUI class NetPlayUI
@ -26,6 +34,7 @@ public:
virtual ~NetPlayUI() {} virtual ~NetPlayUI() {}
virtual void BootGame(const std::string& filename) = 0; virtual void BootGame(const std::string& filename) = 0;
virtual void StopGame() = 0; virtual void StopGame() = 0;
virtual bool IsHosting() const = 0;
virtual void Update() = 0; virtual void Update() = 0;
virtual void AppendChat(const std::string& msg) = 0; virtual void AppendChat(const std::string& msg) = 0;
@ -38,8 +47,11 @@ public:
virtual void OnConnectionLost() = 0; virtual void OnConnectionLost() = 0;
virtual void OnConnectionError(const std::string& message) = 0; virtual void OnConnectionError(const std::string& message) = 0;
virtual void OnTraversalError(TraversalClient::FailureReason error) = 0; virtual void OnTraversalError(TraversalClient::FailureReason error) = 0;
virtual void OnSaveDataSyncFailure() = 0;
virtual bool IsRecording() = 0; virtual bool IsRecording() = 0;
virtual std::string FindGame(const std::string& game) = 0; virtual std::string FindGame(const std::string& game) = 0;
virtual std::shared_ptr<const UICommon::GameFile> FindGameFile(const std::string& game) = 0;
virtual void ShowMD5Dialog(const std::string& file_identifier) = 0; virtual void ShowMD5Dialog(const std::string& file_identifier) = 0;
virtual void SetMD5Progress(int pid, int progress) = 0; virtual void SetMD5Progress(int pid, int progress) = 0;
virtual void SetMD5Result(int pid, const std::string& result) = 0; virtual void SetMD5Result(int pid, const std::string& result) = 0;
@ -159,6 +171,10 @@ private:
void SendStartGamePacket(); void SendStartGamePacket();
void SendStopGamePacket(); void SendStopGamePacket();
void SyncSaveDataResponse(bool success);
bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path);
std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet);
void UpdateDevices(); void UpdateDevices();
void AddPadStateToPacket(int in_game_pad, const GCPadStatus& np, sf::Packet& packet); void AddPadStateToPacket(int in_game_pad, const GCPadStatus& np, sf::Packet& packet);
void SendWiimoteState(int in_game_pad, const NetWiimote& nw); void SendWiimoteState(int in_game_pad, const NetWiimote& nw);
@ -184,6 +200,8 @@ private:
bool m_should_compute_MD5 = false; bool m_should_compute_MD5 = false;
Common::Event m_gc_pad_event; Common::Event m_gc_pad_event;
Common::Event m_wii_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; u32 m_timebase_frame = 0;
}; };

View File

@ -9,6 +9,10 @@
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Core/HW/EXI/EXI_Device.h" #include "Core/HW/EXI/EXI_Device.h"
namespace IOS::HLE::FS
{
class FileSystem;
}
namespace PowerPC namespace PowerPC
{ {
enum class CPUCore; enum class CPUCore;
@ -33,6 +37,9 @@ struct NetSettings
bool m_OCEnable; bool m_OCEnable;
float m_OCFactor; float m_OCFactor;
ExpansionInterface::TEXIDevices m_EXIDevice[2]; ExpansionInterface::TEXIDevices m_EXIDevice[2];
bool m_SyncSaveData;
std::string m_SaveDataRegion;
bool m_IsHosting;
}; };
struct NetTraversalConfig struct NetTraversalConfig
@ -94,6 +101,7 @@ enum
NP_MSG_PLAYER_PING_DATA = 0xE2, NP_MSG_PLAYER_PING_DATA = 0xE2,
NP_MSG_SYNC_GC_SRAM = 0xF0, NP_MSG_SYNC_GC_SRAM = 0xF0,
NP_MSG_SYNC_SAVE_DATA = 0xF1,
}; };
enum enum
@ -103,6 +111,19 @@ enum
CON_ERR_VERSION_MISMATCH = 3 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<u8>; using NetWiimote = std::vector<u8>;
using MessageId = u8; using MessageId = u8;
using PlayerId = u8; using PlayerId = u8;
@ -111,8 +132,10 @@ using PadMapping = s8;
using PadMappingArray = std::array<PadMapping, 4>; using PadMappingArray = std::array<PadMapping, 4>;
bool IsNetPlayRunning(); bool IsNetPlayRunning();
// Precondition: A netplay client instance must be present. In other words, // Precondition: A netplay client instance must be present. In other words,
// IsNetPlayRunning() must be true before calling this. // IsNetPlayRunning() must be true before calling this.
const NetSettings& GetNetSettings(); const NetSettings& GetNetSettings();
IOS::HLE::FS::FileSystem* GetWiiSyncFS();
void SetWiiSyncFS(std::unique_ptr<IOS::HLE::FS::FileSystem> fs);
void ClearWiiSyncFS();
} // namespace NetPlay } // namespace NetPlay

View File

@ -9,24 +9,38 @@
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#include <lzo/lzo1x.h>
#include "Common/CommonPaths.h"
#include "Common/ENetUtil.h" #include "Common/ENetUtil.h"
#include "Common/File.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "Common/Logging/Log.h" #include "Common/Logging/Log.h"
#include "Common/MsgHandler.h" #include "Common/MsgHandler.h"
#include "Common/SFMLHelper.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Common/UPnP.h" #include "Common/UPnP.h"
#include "Common/Version.h" #include "Common/Version.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/NetplaySettings.h" #include "Core/Config/NetplaySettings.h"
#include "Core/ConfigManager.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/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 "Core/NetPlayClient.h" //for NetPlayUI
#include "DiscIO/Enums.h"
#include "InputCommon/GCPadStatus.h" #include "InputCommon/GCPadStatus.h"
#include "UICommon/GameFile.h"
#if !defined(_WIN32) #if !defined(_WIN32)
#include <sys/socket.h> #include <sys/socket.h>
@ -650,15 +664,13 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
case NP_MSG_TIMEBASE: case NP_MSG_TIMEBASE:
{ {
u32 x, y, frame; u64 timebase = Common::PacketReadU64(packet);
packet >> x; u32 frame;
packet >> y;
packet >> frame; packet >> frame;
if (m_desync_detected) if (m_desync_detected)
break; break;
u64 timebase = x | ((u64)y << 32);
std::vector<std::pair<PlayerId, u64>>& timebases = m_timebase_by_frame[frame]; std::vector<std::pair<PlayerId, u64>>& timebases = m_timebase_by_frame[frame];
timebases.emplace_back(player.pid, timebase); timebases.emplace_back(player.pid, timebase);
if (timebases.size() >= m_players.size()) if (timebases.size() >= m_players.size())
@ -737,12 +749,50 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
} }
break; 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: default:
PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid, PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid,
player.pid); player.pid);
// unknown message, kick the client // unknown message, kick the client
return 1; return 1;
break;
} }
return 0; return 0;
@ -812,6 +862,27 @@ void NetPlayServer::SetNetSettings(const NetSettings& settings)
} }
// called from ---GUI--- thread // 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() bool NetPlayServer::StartGame()
{ {
m_timebase_by_frame.clear(); m_timebase_by_frame.clear();
@ -827,6 +898,9 @@ bool NetPlayServer::StartGame()
else else
g_netplay_initial_rtc = Common::Timer::GetLocalTimeSinceJan1970(); 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 // tell clients to start game
sf::Packet spac; sf::Packet spac;
spac << static_cast<MessageId>(NP_MSG_START_GAME); spac << static_cast<MessageId>(NP_MSG_START_GAME);
@ -847,16 +921,321 @@ bool NetPlayServer::StartGame()
spac << m_settings.m_ReducePollingRate; spac << m_settings.m_ReducePollingRate;
spac << m_settings.m_EXIDevice[0]; spac << m_settings.m_EXIDevice[0];
spac << m_settings.m_EXIDevice[1]; spac << m_settings.m_EXIDevice[1];
spac << static_cast<u32>(g_netplay_initial_rtc); Common::PacketWriteU64(spac, g_netplay_initial_rtc);
spac << static_cast<u32>(g_netplay_initial_rtc >> 32); spac << m_settings.m_SyncSaveData;
spac << region;
SendAsyncToClients(std::move(spac)); SendAsyncToClients(std::move(spac));
m_start_pending = false;
m_is_running = true; m_is_running = true;
return 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<MessageId>(NP_MSG_SYNC_SAVE_DATA);
pac << static_cast<MessageId>(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<MessageId>(NP_MSG_SYNC_SAVE_DATA);
pac << static_cast<MessageId>(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<MessageId>(NP_MSG_SYNC_SAVE_DATA);
pac << static_cast<MessageId>(SYNC_SAVE_DATA_GCI);
pac << is_slot_a;
if (File::IsDirectory(path))
{
std::vector<std::string> files =
GCMemcardDirectory::GetFileNamesForGameID(path + DIR_SEP, game->GetGameID());
pac << static_cast<u8>(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<u8>(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<MessageId>(NP_MSG_SYNC_SAVE_DATA);
pac << static_cast<MessageId>(SYNC_SAVE_DATA_WII);
if (save->SaveExists())
{
const std::optional<WiiSave::Header> header = save->ReadHeader();
const std::optional<WiiSave::BkHeader> bk_header = save->ReadBkHeader();
const std::optional<std::vector<WiiSave::Storage::SaveFile>> 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<u8>(file.type) << file.path;
if (file.type == WiiSave::Storage::SaveFile::Type::File)
{
const std::optional<std::vector<u8>>& 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<u8> in_buffer(NETPLAY_LZO_IN_LEN);
std::vector<u8> out_buffer(NETPLAY_LZO_OUT_LEN);
std::vector<u8> 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<lzo_uint32>(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<u32>(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<u32>(0);
return true;
}
bool NetPlayServer::CompressBufferIntoPacket(const std::vector<u8>& in_buffer, sf::Packet& packet)
{
const u64 size = in_buffer.size();
Common::PacketWriteU64(packet, size);
if (size == 0)
return true;
std::vector<u8> out_buffer(NETPLAY_LZO_OUT_LEN);
std::vector<u8> 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<lzo_uint32>(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<u32>(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<u32>(0);
return true;
}
// called from multiple threads // called from multiple threads
void NetPlayServer::SendToClients(const sf::Packet& packet, const PlayerId skip_pid) void NetPlayServer::SendToClients(const sf::Packet& packet, const PlayerId skip_pid)
{ {

View File

@ -41,6 +41,7 @@ public:
void SetNetSettings(const NetSettings& settings); void SetNetSettings(const NetSettings& settings);
bool StartGame(); bool StartGame();
bool RequestStartGame();
PadMappingArray GetPadMapping() const; PadMappingArray GetPadMapping() const;
void SetPadMapping(const PadMappingArray& mappings); void SetPadMapping(const PadMappingArray& mappings);
@ -78,6 +79,10 @@ private:
bool operator==(const Client& other) const { return this == &other; } 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<u8>& in_buffer, sf::Packet& packet);
void SendToClients(const sf::Packet& packet, const PlayerId skip_pid = 0); void SendToClients(const sf::Packet& packet, const PlayerId skip_pid = 0);
void Send(ENetPeer* socket, const sf::Packet& packet); void Send(ENetPeer* socket, const sf::Packet& packet);
unsigned int OnConnect(ENetPeer* socket); unsigned int OnConnect(ENetPeer* socket);
@ -102,6 +107,8 @@ private:
unsigned int m_target_buffer_size = 0; unsigned int m_target_buffer_size = 0;
PadMappingArray m_pad_map; PadMappingArray m_pad_map;
PadMappingArray m_wiimote_map; PadMappingArray m_wiimote_map;
unsigned int m_save_data_synced_players = 0;
bool m_start_pending = false;
std::map<PlayerId, Client> m_players; std::map<PlayerId, Client> m_players;

View File

@ -26,10 +26,10 @@
namespace Core namespace Core
{ {
static std::string s_temp_wii_root;
namespace FS = IOS::HLE::FS; namespace FS = IOS::HLE::FS;
static std::string s_temp_wii_root;
static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs) static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs)
{ {
const u64 title_id = SConfig::GetInstance().GetTitleID(); const u64 title_id = SConfig::GetInstance().GetTitleID();
@ -52,7 +52,9 @@ static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs)
(Movie::IsMovieActive() && !Movie::IsStartingFromClearSave())) (Movie::IsMovieActive() && !Movie::IsStartingFromClearSave()))
{ {
// Copy the current user's save to the Blank NAND // 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); const auto session_save = WiiSave::MakeNandStorage(session_fs, title_id);
WiiSave::Copy(user_save.get(), session_save.get()); WiiSave::Copy(user_save.get(), session_save.get());
} }
@ -62,13 +64,26 @@ void InitializeWiiRoot(bool use_temporary)
{ {
if (use_temporary) if (use_temporary)
{ {
s_temp_wii_root = File::CreateTempDir(); s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP;
if (s_temp_wii_root.empty())
{
ERROR_LOG(IOS_FS, "Could not create temporary directory");
return;
}
WARN_LOG(IOS_FS, "Using temporary directory %s for minimal Wii FS", s_temp_wii_root.c_str()); 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); File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root);
} }
else else
@ -148,8 +163,11 @@ void InitializeWiiFileSystemContents()
void CleanUpWiiFileSystemContents() void CleanUpWiiFileSystemContents()
{ {
if (s_temp_wii_root.empty() || !SConfig::GetInstance().bEnableMemcardSdWriting) if (s_temp_wii_root.empty() || !SConfig::GetInstance().bEnableMemcardSdWriting ||
NetPlay::GetWiiSyncFS())
{
return; return;
}
const u64 title_id = SConfig::GetInstance().GetTitleID(); 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 session_save = WiiSave::MakeNandStorage(ios->GetFS().get(), title_id);
const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured); 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 auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id);
const std::string backup_path = const std::string backup_path =

View File

@ -12,4 +12,4 @@ void ShutdownWiiRoot();
// Initialize or clean up the filesystem contents. // Initialize or clean up the filesystem contents.
void InitializeWiiFileSystemContents(); void InitializeWiiFileSystemContents();
void CleanUpWiiFileSystemContents(); void CleanUpWiiFileSystemContents();
} } // namespace Core

View File

@ -1100,6 +1100,9 @@ bool MainWindow::NetPlayJoin()
return false; return false;
} }
if (Settings::Instance().GetNetPlayServer() != nullptr)
Settings::Instance().GetNetPlayServer()->SetNetPlayUI(m_netplay_dialog);
m_netplay_setup_dialog->close(); m_netplay_setup_dialog->close();
m_netplay_dialog->show(nickname, is_traversal); m_netplay_dialog->show(nickname, is_traversal);

View File

@ -86,6 +86,7 @@ void NetPlayDialog::CreateMainLayout()
m_buffer_size_box = new QSpinBox; m_buffer_size_box = new QSpinBox;
m_save_sd_box = new QCheckBox(tr("Write save/SD data")); m_save_sd_box = new QCheckBox(tr("Write save/SD data"));
m_load_wii_box = new QCheckBox(tr("Load Wii Save")); 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_record_input_box = new QCheckBox(tr("Record inputs"));
m_reduce_polling_rate_box = new QCheckBox(tr("Reduce Polling Rate")); m_reduce_polling_rate_box = new QCheckBox(tr("Reduce Polling Rate"));
m_buffer_label = new QLabel(tr("Buffer:")); m_buffer_label = new QLabel(tr("Buffer:"));
@ -95,6 +96,8 @@ void NetPlayDialog::CreateMainLayout()
m_game_button->setDefault(false); m_game_button->setDefault(false);
m_game_button->setAutoDefault(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* default_button = new QAction(tr("Calculate MD5 hash"), m_md5_button);
auto* menu = new QMenu(this); auto* menu = new QMenu(this);
@ -140,6 +143,7 @@ void NetPlayDialog::CreateMainLayout()
options_widget->addWidget(m_buffer_size_box); options_widget->addWidget(m_buffer_size_box);
options_widget->addWidget(m_save_sd_box); options_widget->addWidget(m_save_sd_box);
options_widget->addWidget(m_load_wii_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_record_input_box);
options_widget->addWidget(m_reduce_polling_rate_box); options_widget->addWidget(m_reduce_polling_rate_box);
options_widget->addWidget(m_quit_button); options_widget->addWidget(m_quit_button);
@ -305,9 +309,11 @@ void NetPlayDialog::OnStart()
settings.m_ReducePollingRate = m_reduce_polling_rate_box->isChecked(); settings.m_ReducePollingRate = m_reduce_polling_rate_box->isChecked();
settings.m_EXIDevice[0] = instance.m_EXIDevice[0]; settings.m_EXIDevice[0] = instance.m_EXIDevice[0];
settings.m_EXIDevice[1] = instance.m_EXIDevice[1]; 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()->SetNetSettings(settings);
Settings::Instance().GetNetPlayServer()->StartGame(); if (Settings::Instance().GetNetPlayServer()->RequestStartGame())
SetOptionsEnabled(false);
} }
void NetPlayDialog::reject() void NetPlayDialog::reject()
@ -346,6 +352,7 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal)
m_start_button->setHidden(!is_hosting); m_start_button->setHidden(!is_hosting);
m_save_sd_box->setHidden(!is_hosting); m_save_sd_box->setHidden(!is_hosting);
m_load_wii_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_reduce_polling_rate_box->setHidden(!is_hosting);
m_buffer_size_box->setHidden(!is_hosting); m_buffer_size_box->setHidden(!is_hosting);
m_buffer_label->setHidden(!is_hosting); m_buffer_label->setHidden(!is_hosting);
@ -374,7 +381,6 @@ void NetPlayDialog::UpdateGUI()
m_player_count = static_cast<int>(players.size()); m_player_count = static_cast<int>(players.size());
int selection_pid = m_players_list->currentItem() ? int selection_pid = m_players_list->currentItem() ?
m_players_list->currentItem()->data(Qt::UserRole).toInt() : m_players_list->currentItem()->data(Qt::UserRole).toInt() :
-1; -1;
@ -487,6 +493,11 @@ void NetPlayDialog::StopGame()
emit Stop(); emit Stop();
} }
bool NetPlayDialog::IsHosting() const
{
return Settings::Instance().GetNetPlayServer() != nullptr;
}
void NetPlayDialog::Update() void NetPlayDialog::Update()
{ {
QueueOnObject(this, &NetPlayDialog::UpdateGUI); QueueOnObject(this, &NetPlayDialog::UpdateGUI);
@ -539,19 +550,23 @@ void NetPlayDialog::GameStatusChanged(bool running)
if (!running && !m_got_stop_request) if (!running && !m_got_stop_request)
Settings::Instance().GetNetPlayClient()->RequestStopGame(); Settings::Instance().GetNetPlayClient()->RequestStopGame();
QueueOnObject(this, [this, running] { QueueOnObject(this, [this, running] { SetOptionsEnabled(!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);
}
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() void NetPlayDialog::OnMsgStartGame()
@ -618,6 +633,11 @@ void NetPlayDialog::OnTraversalError(TraversalClient::FailureReason error)
}); });
} }
void NetPlayDialog::OnSaveDataSyncFailure()
{
QueueOnObject(this, [this] { SetOptionsEnabled(true); });
}
bool NetPlayDialog::IsRecording() bool NetPlayDialog::IsRecording()
{ {
std::optional<bool> is_recording = RunOnObject(m_record_input_box, &QCheckBox::isChecked); std::optional<bool> 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::string NetPlayDialog::FindGame(const std::string& game)
{ {
std::optional<std::string> path = RunOnObject(this, [this, game] { std::optional<std::string> path = RunOnObject(this, [this, &game] {
for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++)
{ {
if (m_game_list_model->GetUniqueIdentifier(i).toStdString() == game) if (m_game_list_model->GetUniqueIdentifier(i).toStdString() == game)
@ -641,6 +661,22 @@ std::string NetPlayDialog::FindGame(const std::string& game)
return std::string(""); return std::string("");
} }
std::shared_ptr<const UICommon::GameFile> NetPlayDialog::FindGameFile(const std::string& game)
{
std::optional<std::shared_ptr<const UICommon::GameFile>> 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<std::shared_ptr<const UICommon::GameFile>>(nullptr);
});
if (game_file)
return *game_file;
return nullptr;
}
void NetPlayDialog::ShowMD5Dialog(const std::string& file_identifier) void NetPlayDialog::ShowMD5Dialog(const std::string& file_identifier)
{ {
QueueOnObject(this, [this, file_identifier] { QueueOnObject(this, [this, file_identifier] {

View File

@ -38,6 +38,7 @@ public:
// NetPlayUI methods // NetPlayUI methods
void BootGame(const std::string& filename) override; void BootGame(const std::string& filename) override;
void StopGame() override; void StopGame() override;
bool IsHosting() const override;
void Update() override; void Update() override;
void AppendChat(const std::string& msg) override; void AppendChat(const std::string& msg) override;
@ -50,8 +51,11 @@ public:
void OnConnectionLost() override; void OnConnectionLost() override;
void OnConnectionError(const std::string& message) override; void OnConnectionError(const std::string& message) override;
void OnTraversalError(TraversalClient::FailureReason error) override; void OnTraversalError(TraversalClient::FailureReason error) override;
void OnSaveDataSyncFailure() override;
bool IsRecording() override; bool IsRecording() override;
std::string FindGame(const std::string& game) override; std::string FindGame(const std::string& game) override;
std::shared_ptr<const UICommon::GameFile> FindGameFile(const std::string& game) override;
void ShowMD5Dialog(const std::string& file_identifier) override; void ShowMD5Dialog(const std::string& file_identifier) override;
void SetMD5Progress(int pid, int progress) override; void SetMD5Progress(int pid, int progress) override;
void SetMD5Result(int pid, const std::string& result) override; void SetMD5Result(int pid, const std::string& result) override;
@ -71,6 +75,7 @@ private:
int duration = OSD::Duration::NORMAL); int duration = OSD::Duration::NORMAL);
void UpdateGUI(); void UpdateGUI();
void GameStatusChanged(bool running); void GameStatusChanged(bool running);
void SetOptionsEnabled(bool enabled);
void SetGame(const QString& game_path); void SetGame(const QString& game_path);
@ -97,6 +102,7 @@ private:
QSpinBox* m_buffer_size_box; QSpinBox* m_buffer_size_box;
QCheckBox* m_save_sd_box; QCheckBox* m_save_sd_box;
QCheckBox* m_load_wii_box; QCheckBox* m_load_wii_box;
QCheckBox* m_sync_save_data_box;
QCheckBox* m_record_input_box; QCheckBox* m_record_input_box;
QCheckBox* m_reduce_polling_rate_box; QCheckBox* m_reduce_polling_rate_box;
QPushButton* m_quit_button; QPushButton* m_quit_button;

View File

@ -16,8 +16,10 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
#include "Common/Config/Config.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h" #include "Core/ConfigManager.h"
#include "Core/Core.h" #include "Core/Core.h"
#include "Core/HW/EXI/EXI.h" #include "Core/HW/EXI/EXI.h"
@ -200,8 +202,8 @@ void GameCubePane::OnConfigPressed(int slot)
if (other_slot_memcard) if (other_slot_memcard)
{ {
QString path_b = QString path_b =
QFileInfo(QString::fromStdString(slot == 0 ? SConfig::GetInstance().m_strMemoryCardB : QFileInfo(QString::fromStdString(slot == 0 ? Config::Get(Config::MAIN_MEMCARD_B_PATH) :
SConfig::GetInstance().m_strMemoryCardA)) Config::Get(Config::MAIN_MEMCARD_A_PATH)))
.absoluteFilePath(); .absoluteFilePath();
if (path_abs == path_b) if (path_abs == path_b)
@ -216,8 +218,8 @@ void GameCubePane::OnConfigPressed(int slot)
if (memcard) if (memcard)
{ {
path_old = path_old =
QFileInfo(QString::fromStdString(slot == 0 ? SConfig::GetInstance().m_strMemoryCardA : QFileInfo(QString::fromStdString(slot == 0 ? Config::Get(Config::MAIN_MEMCARD_A_PATH) :
SConfig::GetInstance().m_strMemoryCardB)) Config::Get(Config::MAIN_MEMCARD_B_PATH)))
.absoluteFilePath(); .absoluteFilePath();
} }
else else
@ -231,11 +233,11 @@ void GameCubePane::OnConfigPressed(int slot)
{ {
if (slot == SLOT_A_INDEX) if (slot == SLOT_A_INDEX)
{ {
SConfig::GetInstance().m_strMemoryCardA = path_abs.toStdString(); Config::SetBase(Config::MAIN_MEMCARD_A_PATH, path_abs.toStdString());
} }
else else
{ {
SConfig::GetInstance().m_strMemoryCardB = path_abs.toStdString(); Config::SetBase(Config::MAIN_MEMCARD_B_PATH, path_abs.toStdString());
} }
} }
else else