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
Random.cpp
SDCardUtil.cpp
SFMLHelper.cpp
SettingsHandler.cpp
StringUtil.cpp
SymbolDB.cpp

View File

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

View File

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

View File

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

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_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_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"},
ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER};
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_AGP_CART_A_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_B;
extern const ConfigInfo<int> MAIN_SERIAL_PORT_1;

View File

@ -6,7 +6,9 @@
#include <memory>
#include "Common/CommonPaths.h"
#include "Common/Config/Config.h"
#include "Common/FileUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/SYSCONFSettings.h"
#include "Core/NetPlayProto.h"
@ -39,6 +41,23 @@ public:
layer->Set(Config::SYSCONF_PROGRESSIVE_SCAN, m_settings.m_ProgressiveScan);
layer->Set(Config::SYSCONF_PAL60, m_settings.m_PAL60);
if (m_settings.m_SyncSaveData)
{
if (!m_settings.m_IsHosting)
{
const std::string path = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY DIR_SEP;
layer->Set(Config::MAIN_GCI_FOLDER_A_PATH_OVERRIDE, path + "Card A");
layer->Set(Config::MAIN_GCI_FOLDER_B_PATH_OVERRIDE, path + "Card B");
const std::string file = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY + "%c." +
m_settings.m_SaveDataRegion + ".raw";
layer->Set(Config::MAIN_MEMCARD_A_PATH, StringFromFormat(file.c_str(), 'A'));
layer->Set(Config::MAIN_MEMCARD_B_PATH, StringFromFormat(file.c_str(), 'B'));
}
layer->Set(Config::MAIN_GCI_FOLDER_CURRENT_GAME_ONLY, true);
}
}
void Save(Config::Layer* layer) override

View File

@ -227,8 +227,6 @@ void SConfig::SaveCoreSettings(IniFile& ini)
core->Set("AudioLatency", iLatency);
core->Set("AudioStretch", m_audio_stretch);
core->Set("AudioStretchMaxLatency", m_audio_stretch_max_latency);
core->Set("MemcardAPath", m_strMemoryCardA);
core->Set("MemcardBPath", m_strMemoryCardB);
core->Set("AgpCartAPath", m_strGbaCartA);
core->Set("AgpCartBPath", m_strGbaCartB);
core->Set("SlotA", m_EXIDevice[0]);
@ -505,8 +503,6 @@ void SConfig::LoadCoreSettings(IniFile& ini)
core->Get("AudioLatency", &iLatency, 20);
core->Get("AudioStretch", &m_audio_stretch, false);
core->Get("AudioStretchMaxLatency", &m_audio_stretch_max_latency, 80);
core->Get("MemcardAPath", &m_strMemoryCardA);
core->Get("MemcardBPath", &m_strMemoryCardB);
core->Get("AgpCartAPath", &m_strGbaCartA);
core->Get("AgpCartBPath", &m_strGbaCartB);
core->Get("SlotA", (int*)&m_EXIDevice[0], ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER);
@ -947,62 +943,12 @@ bool SConfig::SetPathsAndGameMetadata(const BootParameters& boot)
// Set up paths
const std::string region_dir = GetDirectoryForRegion(ToGameCubeRegion(m_region));
CheckMemcardPath(SConfig::GetInstance().m_strMemoryCardA, region_dir, true);
CheckMemcardPath(SConfig::GetInstance().m_strMemoryCardB, region_dir, false);
m_strSRAM = File::GetUserPath(F_GCSRAM_IDX);
m_strBootROM = GetBootROMPath(region_dir);
return true;
}
void SConfig::CheckMemcardPath(std::string& memcardPath, const std::string& gameRegion,
bool isSlotA)
{
std::string ext("." + gameRegion + ".raw");
if (memcardPath.empty())
{
// Use default memcard path if there is no user defined name
std::string defaultFilename = isSlotA ? GC_MEMCARDA : GC_MEMCARDB;
memcardPath = File::GetUserPath(D_GCUSER_IDX) + defaultFilename + ext;
}
else
{
std::string filename = memcardPath;
std::string region = filename.substr(filename.size() - 7, 3);
bool hasregion = false;
hasregion |= region.compare(USA_DIR) == 0;
hasregion |= region.compare(JAP_DIR) == 0;
hasregion |= region.compare(EUR_DIR) == 0;
if (!hasregion)
{
// filename doesn't have region in the extension
if (File::Exists(filename))
{
// If the old file exists we are polite and ask if we should copy it
std::string oldFilename = filename;
filename.replace(filename.size() - 4, 4, ext);
if (PanicYesNoT("Memory Card filename in Slot %c is incorrect\n"
"Region not specified\n\n"
"Slot %c path was changed to\n"
"%s\n"
"Would you like to copy the old file to this new location?\n",
isSlotA ? 'A' : 'B', isSlotA ? 'A' : 'B', filename.c_str()))
{
if (!File::Copy(oldFilename, filename))
PanicAlertT("Copy failed");
}
}
memcardPath = filename; // Always correct the path!
}
else if (region.compare(gameRegion) != 0)
{
// filename has region, but it's not == gameRegion
// Just set the correct filename, the EXI Device will create it if it doesn't exist
memcardPath = filename.replace(filename.size() - ext.size(), ext.size(), ext);
}
}
}
DiscIO::Language SConfig::GetCurrentLanguage(bool wii) const
{
int language_value;

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
#include "Common/Assert.h"
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/File.h"
#include "Common/FileSearch.h"
#include "Common/FileUtil.h"
@ -23,8 +24,10 @@
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Common/Thread.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/NetPlayProto.h"
const int NO_INDEX = -1;
static const char* MC_HDR = "MC_SYSTEM_AREA";
@ -121,6 +124,58 @@ int GCMemcardDirectory::LoadGCI(const std::string& file_name, bool current_game_
return NO_INDEX;
}
// This is only used by NetPlay but it made sense to put it here to keep the relevant code together
std::vector<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,
bool shift_jis, int game_id)
: MemoryCardBase(slot, size_mbits), m_game_id(game_id), m_last_block(-1),
@ -151,7 +206,8 @@ GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, int slot, u
m_save_directory.c_str());
break;
}
int index = LoadGCI(gci_file, m_saves.size() > 112);
int index = LoadGCI(gci_file, m_saves.size() > 112 ||
Config::Get(Config::MAIN_GCI_FOLDER_CURRENT_GAME_ONLY));
if (index != NO_INDEX)
{
m_loaded_saves.push_back(m_saves.at(index).m_gci_header.GCI_FileName());
@ -687,8 +743,8 @@ void GCIFile::DoState(PointerWrap& p)
void MigrateFromMemcardFile(const std::string& directory_name, int card_index)
{
File::CreateFullPath(directory_name);
std::string ini_memcard = (card_index == 0) ? SConfig::GetInstance().m_strMemoryCardA :
SConfig::GetInstance().m_strMemoryCardB;
std::string ini_memcard = (card_index == 0) ? Config::Get(Config::MAIN_MEMCARD_A_PATH) :
Config::Get(Config::MAIN_MEMCARD_B_PATH);
if (File::Exists(ini_memcard))
{
GCMemcard memcard(ini_memcard.c_str());

View File

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

View File

@ -12,10 +12,12 @@
#include <thread>
#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Common/Thread.h"
#include "Core/ConfigManager.h"
@ -71,6 +73,53 @@ MemoryCard::~MemoryCard()
}
}
void MemoryCard::CheckPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA)
{
std::string ext("." + gameRegion + ".raw");
if (memcardPath.empty())
{
// Use default memcard path if there is no user defined name
std::string defaultFilename = isSlotA ? GC_MEMCARDA : GC_MEMCARDB;
memcardPath = File::GetUserPath(D_GCUSER_IDX) + defaultFilename + ext;
}
else
{
std::string filename = memcardPath;
std::string region = filename.substr(filename.size() - 7, 3);
bool hasregion = false;
hasregion |= region.compare(USA_DIR) == 0;
hasregion |= region.compare(JAP_DIR) == 0;
hasregion |= region.compare(EUR_DIR) == 0;
if (!hasregion)
{
// filename doesn't have region in the extension
if (File::Exists(filename))
{
// If the old file exists we are polite and ask if we should copy it
std::string oldFilename = filename;
filename.replace(filename.size() - 4, 4, ext);
if (PanicYesNoT("Memory Card filename in Slot %c is incorrect\n"
"Region not specified\n\n"
"Slot %c path was changed to\n"
"%s\n"
"Would you like to copy the old file to this new location?\n",
isSlotA ? 'A' : 'B', isSlotA ? 'A' : 'B', filename.c_str()))
{
if (!File::Copy(oldFilename, filename))
PanicAlertT("Copy failed");
}
}
memcardPath = filename; // Always correct the path!
}
else if (region.compare(gameRegion) != 0)
{
// filename has region, but it's not == gameRegion
// Just set the correct filename, the EXI Device will create it if it doesn't exist
memcardPath = filename.replace(filename.size() - ext.size(), ext.size(), ext);
}
}
}
void MemoryCard::FlushThread()
{
if (!SConfig::GetInstance().bEnableMemcardSdWriting)

View File

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

View File

@ -32,6 +32,7 @@
#include "Common/StringUtil.h"
#include "Common/Swap.h"
#include "Core/CommonTitles.h"
#include "Core/HW/WiiSaveStructs.h"
#include "Core/IOS/ES/ES.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/IOS/IOS.h"
@ -48,96 +49,6 @@ constexpr Md5 s_md5_blanker{{0x0E, 0x65, 0x37, 0x81, 0x99, 0xBE, 0x45, 0x17, 0xA
0x45, 0x1A, 0x57, 0x93}};
constexpr u32 s_ng_id = 0x0403AC68;
enum
{
BLOCK_SZ = 0x40,
ICON_SZ = 0x1200,
BNR_SZ = 0x60a0,
FULL_BNR_MIN = 0x72a0, // BNR_SZ + 1*ICON_SZ
FULL_BNR_MAX = 0xF0A0, // BNR_SZ + 8*ICON_SZ
BK_LISTED_SZ = 0x70, // Size before rounding to nearest block
SIG_SZ = 0x40,
FULL_CERT_SZ = 0x3C0, // SIG_SZ + NG_CERT_SZ + AP_CERT_SZ + 0x80?
BK_HDR_MAGIC = 0x426B0001,
FILE_HDR_MAGIC = 0x03adf17e
};
#pragma pack(push, 1)
struct Header
{
Common::BigEndianValue<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
{
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
{
s_bClearSave = !File::Exists(SConfig::GetInstance().m_strMemoryCardA);
s_bClearSave = !File::Exists(Config::Get(Config::MAIN_MEMCARD_A_PATH));
}
s_memcards |=
(SConfig::GetInstance().m_EXIDevice[0] == ExpansionInterface::EXIDEVICE_MEMORYCARD ||
@ -1491,4 +1491,4 @@ void Shutdown()
s_currentInputCount = s_totalInputCount = s_totalFrames = s_tickCountAtLastInput = 0;
s_temp_input.clear();
}
};
} // namespace Movie

View File

@ -13,18 +13,23 @@
#include <sstream>
#include <thread>
#include <type_traits>
#include <vector>
#include <lzo/lzo1x.h>
#include <mbedtls/md5.h>
#include "Common/Assert.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/ENetUtil.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MD5.h"
#include "Common/MsgHandler.h"
#include "Common/NandPaths.h"
#include "Common/QoSSession.h"
#include "Common/SFMLHelper.h"
#include "Common/StringUtil.h"
#include "Common/Timer.h"
#include "Common/Version.h"
@ -34,12 +39,19 @@
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_DeviceGCController.h"
#include "Core/HW/Sram.h"
#include "Core/HW/WiiSave.h"
#include "Core/HW/WiiSaveStructs.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/IOS/FS/HostBackend/FS.h"
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
#include "Core/IOS/Uids.h"
#include "Core/Movie.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/WiiRoot.h"
#include "InputCommon/GCAdapter.h"
#include "UICommon/GameFile.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoConfig.h"
@ -47,6 +59,7 @@ namespace NetPlay
{
static std::mutex crit_netplay_client;
static NetPlayClient* netplay_client = nullptr;
static std::unique_ptr<IOS::HLE::FS::FileSystem> s_wii_sync_fs;
// called from ---GUI--- thread
NetPlayClient::~NetPlayClient()
@ -467,10 +480,11 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
packet >> tmp;
m_net_settings.m_EXIDevice[1] = static_cast<ExpansionInterface::TEXIDevices>(tmp);
u32 time_low, time_high;
packet >> time_low;
packet >> time_high;
g_netplay_initial_rtc = time_low | ((u64)time_high << 32);
g_netplay_initial_rtc = Common::PacketReadU64(packet);
packet >> m_net_settings.m_SyncSaveData;
packet >> m_net_settings.m_SaveDataRegion;
m_net_settings.m_IsHosting = m_dialog->IsHosting();
}
m_dialog->OnMsgStartGame();
@ -553,6 +567,194 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
}
break;
case NP_MSG_SYNC_SAVE_DATA:
{
MessageId sub_id;
packet >> sub_id;
switch (sub_id)
{
case SYNC_SAVE_DATA_NOTIFY:
{
packet >> m_sync_save_data_count;
m_sync_save_data_success_count = 0;
if (m_sync_save_data_count == 0)
SyncSaveDataResponse(true);
else
m_dialog->AppendChat(GetStringT("Synchronizing save data..."));
}
break;
case SYNC_SAVE_DATA_RAW:
{
if (m_dialog->IsHosting())
return 0;
bool is_slot_a;
std::string region;
bool mc251;
packet >> is_slot_a >> region >> mc251;
const std::string path = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY +
(is_slot_a ? "A." : "B.") + region + (mc251 ? ".251" : "") + ".raw";
if (File::Exists(path) && !File::Delete(path))
{
PanicAlertT("Failed to delete NetPlay memory card. Verify your write permissions.");
SyncSaveDataResponse(false);
return 0;
}
const bool success = DecompressPacketIntoFile(packet, path);
SyncSaveDataResponse(success);
}
break;
case SYNC_SAVE_DATA_GCI:
{
if (m_dialog->IsHosting())
return 0;
bool is_slot_a;
u8 file_count;
packet >> is_slot_a >> file_count;
const std::string path = File::GetUserPath(D_GCUSER_IDX) + GC_MEMCARD_NETPLAY DIR_SEP +
StringFromFormat("Card %c", is_slot_a ? 'A' : 'B');
if ((File::Exists(path) && !File::DeleteDirRecursively(path + DIR_SEP)) ||
!File::CreateFullPath(path + DIR_SEP))
{
PanicAlertT("Failed to reset NetPlay GCI folder. Verify your write permissions.");
SyncSaveDataResponse(false);
return 0;
}
for (u8 i = 0; i < file_count; i++)
{
std::string file_name;
packet >> file_name;
if (!DecompressPacketIntoFile(packet, path + DIR_SEP + file_name))
{
SyncSaveDataResponse(false);
return 0;
}
}
SyncSaveDataResponse(true);
}
break;
case SYNC_SAVE_DATA_WII:
{
if (m_dialog->IsHosting())
return 0;
const auto game = m_dialog->FindGameFile(m_selected_game);
if (game == nullptr)
{
SyncSaveDataResponse(true); // whatever, we won't be booting anyways
return 0;
}
const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP;
if (File::Exists(path) && !File::DeleteDirRecursively(path))
{
PanicAlertT("Failed to reset NetPlay NAND folder. Verify your write permissions.");
SyncSaveDataResponse(false);
return 0;
}
auto temp_fs = std::make_unique<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:
{
std::string file_identifier;
@ -895,6 +1097,120 @@ bool NetPlayClient::StartGame(const std::string& path)
return true;
}
void NetPlayClient::SyncSaveDataResponse(const bool success)
{
m_dialog->AppendChat(success ? GetStringT("Data received!") :
GetStringT("Error processing data."));
if (success)
{
if (++m_sync_save_data_success_count >= m_sync_save_data_count)
{
sf::Packet response_packet;
response_packet << static_cast<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
bool NetPlayClient::ChangeGame(const std::string&)
{
@ -1178,6 +1494,8 @@ bool NetPlayClient::StopGame()
// stop game
m_dialog->StopGame();
ClearWiiSyncFS();
return true;
}
@ -1278,8 +1596,7 @@ void NetPlayClient::SendTimeBase()
sf::Packet packet;
packet << static_cast<MessageId>(NP_MSG_TIMEBASE);
packet << static_cast<u32>(timebase);
packet << static_cast<u32>(timebase << 32);
Common::PacketWriteU64(packet, timebase);
packet << netplay_client->m_timebase_frame;
netplay_client->SendAsync(std::move(packet));
@ -1358,6 +1675,26 @@ const NetSettings& GetNetSettings()
return netplay_client->GetNetSettings();
}
IOS::HLE::FS::FileSystem* GetWiiSyncFS()
{
return s_wii_sync_fs.get();
}
void SetWiiSyncFS(std::unique_ptr<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)
{
std::lock_guard<std::mutex> lk(crit_netplay_client);

View File

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

View File

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

View File

@ -9,24 +9,38 @@
#include <cstdio>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <type_traits>
#include <unordered_set>
#include <vector>
#include <lzo/lzo1x.h>
#include "Common/CommonPaths.h"
#include "Common/ENetUtil.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/SFMLHelper.h"
#include "Common/StringUtil.h"
#include "Common/UPnP.h"
#include "Common/Version.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/NetplaySettings.h"
#include "Core/ConfigManager.h"
#include "Core/HW/GCMemcard/GCMemcardDirectory.h"
#include "Core/HW/GCMemcard/GCMemcardRaw.h"
#include "Core/HW/Sram.h"
#include "Core/HW/WiiSave.h"
#include "Core/HW/WiiSaveStructs.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/NetPlayClient.h" //for NetPlayUI
#include "DiscIO/Enums.h"
#include "InputCommon/GCPadStatus.h"
#include "UICommon/GameFile.h"
#if !defined(_WIN32)
#include <sys/socket.h>
@ -650,15 +664,13 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
case NP_MSG_TIMEBASE:
{
u32 x, y, frame;
packet >> x;
packet >> y;
u64 timebase = Common::PacketReadU64(packet);
u32 frame;
packet >> frame;
if (m_desync_detected)
break;
u64 timebase = x | ((u64)y << 32);
std::vector<std::pair<PlayerId, u64>>& timebases = m_timebase_by_frame[frame];
timebases.emplace_back(player.pid, timebase);
if (timebases.size() >= m_players.size())
@ -737,12 +749,50 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
}
break;
case NP_MSG_SYNC_SAVE_DATA:
{
MessageId sub_id;
packet >> sub_id;
switch (sub_id)
{
case SYNC_SAVE_DATA_SUCCESS:
{
if (m_start_pending)
{
m_save_data_synced_players++;
if (m_save_data_synced_players >= m_players.size() - 1)
{
m_dialog->AppendChat(GetStringT("All players synchronized."));
StartGame();
}
}
}
break;
case SYNC_SAVE_DATA_FAILURE:
{
m_dialog->AppendChat(
StringFromFormat(GetStringT("%s failed to synchronize.").c_str(), player.name.c_str()));
m_dialog->OnSaveDataSyncFailure();
m_start_pending = false;
}
break;
default:
PanicAlertT(
"Unknown SYNC_SAVE_DATA message with id:%d received from player:%d Kicking player!",
sub_id, player.pid);
return 1;
}
}
break;
default:
PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid,
player.pid);
// unknown message, kick the client
return 1;
break;
}
return 0;
@ -812,6 +862,27 @@ void NetPlayServer::SetNetSettings(const NetSettings& settings)
}
// called from ---GUI--- thread
bool NetPlayServer::RequestStartGame()
{
if (m_settings.m_SyncSaveData && m_players.size() > 1)
{
if (!SyncSaveData())
{
PanicAlertT("Error synchronizing save data!");
return false;
}
m_start_pending = true;
}
else
{
return StartGame();
}
return true;
}
// called from multiple threads
bool NetPlayServer::StartGame()
{
m_timebase_by_frame.clear();
@ -827,6 +898,9 @@ bool NetPlayServer::StartGame()
else
g_netplay_initial_rtc = Common::Timer::GetLocalTimeSinceJan1970();
const std::string region = SConfig::GetDirectoryForRegion(
SConfig::ToGameCubeRegion(m_dialog->FindGameFile(m_selected_game)->GetRegion()));
// tell clients to start game
sf::Packet spac;
spac << static_cast<MessageId>(NP_MSG_START_GAME);
@ -847,16 +921,321 @@ bool NetPlayServer::StartGame()
spac << m_settings.m_ReducePollingRate;
spac << m_settings.m_EXIDevice[0];
spac << m_settings.m_EXIDevice[1];
spac << static_cast<u32>(g_netplay_initial_rtc);
spac << static_cast<u32>(g_netplay_initial_rtc >> 32);
Common::PacketWriteU64(spac, g_netplay_initial_rtc);
spac << m_settings.m_SyncSaveData;
spac << region;
SendAsyncToClients(std::move(spac));
m_start_pending = false;
m_is_running = true;
return true;
}
// called from ---GUI--- thread
bool NetPlayServer::SyncSaveData()
{
m_save_data_synced_players = 0;
u8 save_count = 0;
constexpr size_t exi_device_count = 2;
for (size_t i = 0; i < exi_device_count; i++)
{
if (m_settings.m_EXIDevice[i] == ExpansionInterface::EXIDEVICE_MEMORYCARD ||
SConfig::GetInstance().m_EXIDevice[i] == ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER)
{
save_count++;
}
}
const auto game = m_dialog->FindGameFile(m_selected_game);
if (game == nullptr)
{
PanicAlertT("Selected game doesn't exist in game list!");
return false;
}
bool wii_save = false;
if (m_settings.m_CopyWiiSave && (game->GetPlatform() == DiscIO::Platform::WiiDisc ||
game->GetPlatform() == DiscIO::Platform::WiiWAD))
{
wii_save = true;
save_count++;
}
{
sf::Packet pac;
pac << static_cast<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
void NetPlayServer::SendToClients(const sf::Packet& packet, const PlayerId skip_pid)
{

View File

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

View File

@ -26,10 +26,10 @@
namespace Core
{
static std::string s_temp_wii_root;
namespace FS = IOS::HLE::FS;
static std::string s_temp_wii_root;
static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs)
{
const u64 title_id = SConfig::GetInstance().GetTitleID();
@ -52,7 +52,9 @@ static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs)
(Movie::IsMovieActive() && !Movie::IsStartingFromClearSave()))
{
// Copy the current user's save to the Blank NAND
const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id);
auto* sync_fs = NetPlay::GetWiiSyncFS();
const auto user_save =
WiiSave::MakeNandStorage(sync_fs ? sync_fs : configured_fs.get(), title_id);
const auto session_save = WiiSave::MakeNandStorage(session_fs, title_id);
WiiSave::Copy(user_save.get(), session_save.get());
}
@ -62,13 +64,26 @@ void InitializeWiiRoot(bool use_temporary)
{
if (use_temporary)
{
s_temp_wii_root = File::CreateTempDir();
if (s_temp_wii_root.empty())
{
ERROR_LOG(IOS_FS, "Could not create temporary directory");
return;
}
s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP;
WARN_LOG(IOS_FS, "Using temporary directory %s for minimal Wii FS", s_temp_wii_root.c_str());
// If directory exists, make a backup
if (File::Exists(s_temp_wii_root))
{
const std::string backup_path =
s_temp_wii_root.substr(0, s_temp_wii_root.size() - 1) + ".backup" DIR_SEP;
WARN_LOG(IOS_FS, "Temporary Wii FS directory exists, moving to backup...");
// If backup exists, delete it as we don't want a mess
if (File::Exists(backup_path))
{
WARN_LOG(IOS_FS, "Temporary Wii FS backup directory exists, deleting...");
File::DeleteDirRecursively(backup_path);
}
File::CopyDir(s_temp_wii_root, backup_path, true);
}
File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root);
}
else
@ -148,8 +163,11 @@ void InitializeWiiFileSystemContents()
void CleanUpWiiFileSystemContents()
{
if (s_temp_wii_root.empty() || !SConfig::GetInstance().bEnableMemcardSdWriting)
if (s_temp_wii_root.empty() || !SConfig::GetInstance().bEnableMemcardSdWriting ||
NetPlay::GetWiiSyncFS())
{
return;
}
const u64 title_id = SConfig::GetInstance().GetTitleID();
@ -157,6 +175,16 @@ void CleanUpWiiFileSystemContents()
const auto session_save = WiiSave::MakeNandStorage(ios->GetFS().get(), title_id);
const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured);
// FS won't write the save if the directory doesn't exist
const std::string title_path = Common::GetTitleDataPath(title_id);
if (!configured_fs->GetMetadata(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path))
{
configured_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path, 0,
{IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite,
IOS::HLE::FS::Mode::ReadWrite});
}
const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id);
const std::string backup_path =

View File

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

View File

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

View File

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

View File

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

View File

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