Merge pull request #7222 from Techjar/netplay-sync-saves

NetPlay save data synchronization
This commit is contained in:
spycrab 2018-07-21 02:09:02 +02:00 committed by GitHub
commit c141511c87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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