Merge pull request #8861 from JosJuice/netplay-hash

Make netplay's "same game" check more robust
This commit is contained in:
JMC47 2020-09-06 17:14:08 -04:00 committed by GitHub
commit e7e5175606
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 523 additions and 157 deletions

View File

@ -37,6 +37,7 @@ add_library(core
PatchEngine.h PatchEngine.h
State.cpp State.cpp
State.h State.h
SyncIdentifier.h
SysConf.cpp SysConf.cpp
SysConf.h SysConf.h
TitleDatabase.cpp TitleDatabase.cpp

View File

@ -673,6 +673,7 @@
<ClInclude Include="PowerPC\PPCTables.h" /> <ClInclude Include="PowerPC\PPCTables.h" />
<ClInclude Include="PowerPC\Profiler.h" /> <ClInclude Include="PowerPC\Profiler.h" />
<ClInclude Include="State.h" /> <ClInclude Include="State.h" />
<ClInclude Include="SyncIdentifier.h" />
<ClInclude Include="SysConf.h" /> <ClInclude Include="SysConf.h" />
<ClInclude Include="Titles.h" /> <ClInclude Include="Titles.h" />
<ClInclude Include="TitleDatabase.h" /> <ClInclude Include="TitleDatabase.h" />

View File

@ -1757,7 +1757,8 @@
</ClInclude> </ClInclude>
<ClInclude Include="HW\EXI\BBA\TAP_Win32.h"> <ClInclude Include="HW\EXI\BBA\TAP_Win32.h">
<Filter>HW %28Flipper/Hollywood%29\EXI - Expansion Interface\BBA</Filter> <Filter>HW %28Flipper/Hollywood%29\EXI - Expansion Interface\BBA</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="SyncIdentifier.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Text Include="CMakeLists.txt" /> <Text Include="CMakeLists.txt" />

View File

@ -53,9 +53,11 @@
#include "Core/IOS/Uids.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/SyncIdentifier.h"
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h" #include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
#include "InputCommon/GCAdapter.h" #include "InputCommon/GCAdapter.h"
#include "InputCommon/InputConfig.h" #include "InputCommon/InputConfig.h"
#include "UICommon/GameFile.h"
#include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoConfig.h" #include "VideoCommon/VideoConfig.h"
@ -284,6 +286,22 @@ bool NetPlayClient::Connect()
} }
} }
static void ReceiveSyncIdentifier(sf::Packet& spac, SyncIdentifier& sync_identifier)
{
// We use a temporary variable here due to a potential long vs long long mismatch
sf::Uint64 dol_elf_size;
spac >> dol_elf_size;
sync_identifier.dol_elf_size = dol_elf_size;
spac >> sync_identifier.game_id;
spac >> sync_identifier.revision;
spac >> sync_identifier.disc_number;
spac >> sync_identifier.is_datel;
for (u8& x : sync_identifier.sync_hash)
spac >> x;
}
// called from ---NETPLAY--- thread // called from ---NETPLAY--- thread
unsigned int NetPlayClient::OnData(sf::Packet& packet) unsigned int NetPlayClient::OnData(sf::Packet& packet)
{ {
@ -572,24 +590,25 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
case NP_MSG_CHANGE_GAME: case NP_MSG_CHANGE_GAME:
{ {
std::string netplay_name;
{ {
std::lock_guard<std::recursive_mutex> lkg(m_crit.game); std::lock_guard<std::recursive_mutex> lkg(m_crit.game);
packet >> m_selected_game; ReceiveSyncIdentifier(packet, m_selected_game);
packet >> netplay_name;
} }
INFO_LOG(NETPLAY, "Game changed to %s", m_selected_game.c_str()); INFO_LOG(NETPLAY, "Game changed to %s", netplay_name.c_str());
// update gui // update gui
m_dialog->OnMsgChangeGame(m_selected_game); m_dialog->OnMsgChangeGame(m_selected_game, netplay_name);
sf::Packet game_status_packet; sf::Packet game_status_packet;
game_status_packet << static_cast<MessageId>(NP_MSG_GAME_STATUS); game_status_packet << static_cast<MessageId>(NP_MSG_GAME_STATUS);
PlayerGameStatus status = m_dialog->FindGame(m_selected_game).empty() ? SyncIdentifierComparison result;
PlayerGameStatus::NotFound : m_dialog->FindGameFile(m_selected_game, &result);
PlayerGameStatus::Ok;
game_status_packet << static_cast<u32>(status); game_status_packet << static_cast<u32>(result);
Send(game_status_packet); Send(game_status_packet);
sf::Packet ipl_status_packet; sf::Packet ipl_status_packet;
@ -609,7 +628,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
Player& player = m_players[pid]; Player& player = m_players[pid];
u32 status; u32 status;
packet >> status; packet >> status;
player.game_status = static_cast<PlayerGameStatus>(status); player.game_status = static_cast<SyncIdentifierComparison>(status);
} }
m_dialog->Update(); m_dialog->Update();
@ -623,7 +642,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
packet >> m_current_game; packet >> m_current_game;
packet >> m_net_settings.m_CPUthread; packet >> m_net_settings.m_CPUthread;
INFO_LOG(NETPLAY, "Start of game %s", m_selected_game.c_str()); INFO_LOG(NETPLAY, "Start of game %s", m_selected_game.game_id.c_str());
{ {
std::underlying_type_t<PowerPC::CPUCore> core; std::underlying_type_t<PowerPC::CPUCore> core;
@ -1172,10 +1191,10 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
case NP_MSG_COMPUTE_MD5: case NP_MSG_COMPUTE_MD5:
{ {
std::string file_identifier; SyncIdentifier sync_identifier;
packet >> file_identifier; ReceiveSyncIdentifier(packet, sync_identifier);
ComputeMD5(file_identifier); ComputeMD5(sync_identifier);
} }
break; break;
@ -1382,11 +1401,15 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector<int>& pid_list)
switch (player.game_status) switch (player.game_status)
{ {
case PlayerGameStatus::Ok: case SyncIdentifierComparison::SameGame:
ss << "ready"; ss << "ready";
break; break;
case PlayerGameStatus::NotFound: case SyncIdentifierComparison::DifferentVersion:
ss << "wrong game version";
break;
case SyncIdentifierComparison::DifferentGame:
ss << "game missing"; ss << "game missing";
break; break;
@ -2286,23 +2309,24 @@ bool NetPlayClient::DoAllPlayersHaveGame()
{ {
std::lock_guard<std::recursive_mutex> lkp(m_crit.players); std::lock_guard<std::recursive_mutex> lkp(m_crit.players);
return std::all_of(std::begin(m_players), std::end(m_players), return std::all_of(std::begin(m_players), std::end(m_players), [](auto entry) {
[](auto entry) { return entry.second.game_status == PlayerGameStatus::Ok; }); return entry.second.game_status == SyncIdentifierComparison::SameGame;
});
} }
void NetPlayClient::ComputeMD5(const std::string& file_identifier) void NetPlayClient::ComputeMD5(const SyncIdentifier& sync_identifier)
{ {
if (m_should_compute_MD5) if (m_should_compute_MD5)
return; return;
m_dialog->ShowMD5Dialog(file_identifier); m_dialog->ShowMD5Dialog(sync_identifier.game_id);
m_should_compute_MD5 = true; m_should_compute_MD5 = true;
std::string file; std::string file;
if (file_identifier == WII_SDCARD) if (sync_identifier == GetSDCardIdentifier())
file = File::GetUserPath(F_WIISDCARD_IDX); file = File::GetUserPath(F_WIISDCARD_IDX);
else else if (auto game = m_dialog->FindGameFile(sync_identifier))
file = m_dialog->FindGame(file_identifier); file = game->GetFilePath();
if (file.empty() || !File::Exists(file)) if (file.empty() || !File::Exists(file))
{ {
@ -2348,6 +2372,11 @@ void NetPlayClient::AdjustPadBufferSize(const unsigned int size)
m_dialog->OnPadBufferChanged(size); m_dialog->OnPadBufferChanged(size);
} }
SyncIdentifier NetPlayClient::GetSDCardIdentifier()
{
return SyncIdentifier{{}, "sd", {}, {}, {}, {}};
}
bool IsNetPlayRunning() bool IsNetPlayRunning()
{ {
return netplay_client != nullptr; return netplay_client != nullptr;

View File

@ -22,6 +22,7 @@
#include "Common/SPSCQueue.h" #include "Common/SPSCQueue.h"
#include "Common/TraversalClient.h" #include "Common/TraversalClient.h"
#include "Core/NetPlayProto.h" #include "Core/NetPlayProto.h"
#include "Core/SyncIdentifier.h"
#include "InputCommon/GCPadStatus.h" #include "InputCommon/GCPadStatus.h"
namespace UICommon namespace UICommon
@ -42,7 +43,8 @@ public:
virtual void Update() = 0; virtual void Update() = 0;
virtual void AppendChat(const std::string& msg) = 0; virtual void AppendChat(const std::string& msg) = 0;
virtual void OnMsgChangeGame(const std::string& filename) = 0; virtual void OnMsgChangeGame(const SyncIdentifier& sync_identifier,
const std::string& netplay_name) = 0;
virtual void OnMsgStartGame() = 0; virtual void OnMsgStartGame() = 0;
virtual void OnMsgStopGame() = 0; virtual void OnMsgStopGame() = 0;
virtual void OnMsgPowerButton() = 0; virtual void OnMsgPowerButton() = 0;
@ -59,9 +61,10 @@ public:
virtual void OnGolferChanged(bool is_golfer, const std::string& golfer_name) = 0; virtual void OnGolferChanged(bool is_golfer, const std::string& golfer_name) = 0;
virtual bool IsRecording() = 0; virtual bool IsRecording() = 0;
virtual std::string FindGame(const std::string& game) = 0; virtual std::shared_ptr<const UICommon::GameFile>
virtual std::shared_ptr<const UICommon::GameFile> FindGameFile(const std::string& game) = 0; FindGameFile(const SyncIdentifier& sync_identifier,
virtual void ShowMD5Dialog(const std::string& file_identifier) = 0; SyncIdentifierComparison* found = nullptr) = 0;
virtual void ShowMD5Dialog(const std::string& title) = 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;
virtual void AbortMD5() = 0; virtual void AbortMD5() = 0;
@ -75,13 +78,6 @@ public:
virtual void SetChunkedProgress(int pid, u64 progress) = 0; virtual void SetChunkedProgress(int pid, u64 progress) = 0;
}; };
enum class PlayerGameStatus
{
Unknown,
Ok,
NotFound
};
class Player class Player
{ {
public: public:
@ -89,7 +85,7 @@ public:
std::string name; std::string name;
std::string revision; std::string revision;
u32 ping; u32 ping;
PlayerGameStatus game_status; SyncIdentifierComparison game_status;
bool IsHost() const { return pid == 1; } bool IsHost() const { return pid == 1; }
}; };
@ -149,6 +145,8 @@ public:
void AdjustPadBufferSize(unsigned int size); void AdjustPadBufferSize(unsigned int size);
static SyncIdentifier GetSDCardIdentifier();
protected: protected:
struct AsyncQueueEntry struct AsyncQueueEntry
{ {
@ -182,7 +180,7 @@ protected:
ENetPeer* m_server = nullptr; ENetPeer* m_server = nullptr;
std::thread m_thread; std::thread m_thread;
std::string m_selected_game; SyncIdentifier m_selected_game;
Common::Flag m_is_running{false}; Common::Flag m_is_running{false};
Common::Flag m_do_loop{true}; Common::Flag m_do_loop{true};
@ -237,7 +235,7 @@ private:
void Send(const sf::Packet& packet, u8 channel_id = DEFAULT_CHANNEL); void Send(const sf::Packet& packet, u8 channel_id = DEFAULT_CHANNEL);
void Disconnect(); void Disconnect();
bool Connect(); bool Connect();
void ComputeMD5(const std::string& file_identifier); void ComputeMD5(const SyncIdentifier& sync_identifier);
void DisplayPlayersPing(); void DisplayPlayersPing();
u32 GetPlayersMaxPing() const; u32 GetPlayersMaxPing() const;

View File

@ -51,6 +51,7 @@
#include "Core/IOS/IOS.h" #include "Core/IOS/IOS.h"
#include "Core/IOS/Uids.h" #include "Core/IOS/Uids.h"
#include "Core/NetPlayClient.h" //for NetPlayUI #include "Core/NetPlayClient.h" //for NetPlayUI
#include "Core/SyncIdentifier.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h" #include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
#include "InputCommon/GCPadStatus.h" #include "InputCommon/GCPadStatus.h"
@ -182,7 +183,7 @@ void NetPlayServer::SetupIndex()
session.region = Config::Get(Config::NETPLAY_INDEX_REGION); session.region = Config::Get(Config::NETPLAY_INDEX_REGION);
session.has_password = !Config::Get(Config::NETPLAY_INDEX_PASSWORD).empty(); session.has_password = !Config::Get(Config::NETPLAY_INDEX_PASSWORD).empty();
session.method = m_traversal_client ? "traversal" : "direct"; session.method = m_traversal_client ? "traversal" : "direct";
session.game_id = m_selected_game.empty() ? "UNKNOWN" : m_selected_game; session.game_id = m_selected_game_name.empty() ? "UNKNOWN" : m_selected_game_name;
session.player_count = static_cast<int>(m_players.size()); session.player_count = static_cast<int>(m_players.size());
session.in_game = m_is_running; session.in_game = m_is_running;
session.port = GetPort(); session.port = GetPort();
@ -238,7 +239,7 @@ void NetPlayServer::ThreadFunc()
SendToClients(spac); SendToClients(spac);
m_index.SetPlayerCount(static_cast<int>(m_players.size())); m_index.SetPlayerCount(static_cast<int>(m_players.size()));
m_index.SetGame(m_selected_game); m_index.SetGame(m_selected_game_name);
m_index.SetInGame(m_is_running); m_index.SetInGame(m_is_running);
m_update_pings = false; m_update_pings = false;
@ -348,6 +349,20 @@ void NetPlayServer::ThreadFunc()
} }
} // namespace NetPlay } // namespace NetPlay
static void SendSyncIdentifier(sf::Packet& spac, const SyncIdentifier& sync_identifier)
{
// We cast here due to a potential long vs long long mismatch
spac << static_cast<sf::Uint64>(sync_identifier.dol_elf_size);
spac << sync_identifier.game_id;
spac << sync_identifier.revision;
spac << sync_identifier.disc_number;
spac << sync_identifier.is_datel;
for (const u8& x : sync_identifier.sync_hash)
spac << x;
}
// called from ---NETPLAY--- thread // called from ---NETPLAY--- thread
unsigned int NetPlayServer::OnConnect(ENetPeer* socket, sf::Packet& rpac) unsigned int NetPlayServer::OnConnect(ENetPeer* socket, sf::Packet& rpac)
{ {
@ -413,11 +428,12 @@ unsigned int NetPlayServer::OnConnect(ENetPeer* socket, sf::Packet& rpac)
Send(player.socket, spac); Send(player.socket, spac);
// send new client the selected game // send new client the selected game
if (!m_selected_game.empty()) if (!m_selected_game_name.empty())
{ {
spac.clear(); spac.clear();
spac << static_cast<MessageId>(NP_MSG_CHANGE_GAME); spac << static_cast<MessageId>(NP_MSG_CHANGE_GAME);
spac << m_selected_game; SendSyncIdentifier(spac, m_selected_game_identifier);
spac << m_selected_game_name;
Send(player.socket, spac); Send(player.socket, spac);
} }
@ -913,7 +929,7 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
u32 status; u32 status;
packet >> status; packet >> status;
m_players[player.pid].game_status = static_cast<PlayerGameStatus>(status); m_players[player.pid].game_status = static_cast<SyncIdentifierComparison>(status);
// send msg to other clients // send msg to other clients
sf::Packet spac; sf::Packet spac;
@ -1153,16 +1169,19 @@ void NetPlayServer::SendChatMessage(const std::string& msg)
} }
// called from ---GUI--- thread // called from ---GUI--- thread
bool NetPlayServer::ChangeGame(const std::string& game) bool NetPlayServer::ChangeGame(const SyncIdentifier& sync_identifier,
const std::string& netplay_name)
{ {
std::lock_guard<std::recursive_mutex> lkg(m_crit.game); std::lock_guard<std::recursive_mutex> lkg(m_crit.game);
m_selected_game = game; m_selected_game_identifier = sync_identifier;
m_selected_game_name = netplay_name;
// send changed game to clients // send changed game to clients
sf::Packet spac; sf::Packet spac;
spac << static_cast<MessageId>(NP_MSG_CHANGE_GAME); spac << static_cast<MessageId>(NP_MSG_CHANGE_GAME);
spac << game; SendSyncIdentifier(spac, m_selected_game_identifier);
spac << m_selected_game_name;
SendAsyncToClients(std::move(spac)); SendAsyncToClients(std::move(spac));
@ -1170,11 +1189,11 @@ bool NetPlayServer::ChangeGame(const std::string& game)
} }
// called from ---GUI--- thread // called from ---GUI--- thread
bool NetPlayServer::ComputeMD5(const std::string& file_identifier) bool NetPlayServer::ComputeMD5(const SyncIdentifier& sync_identifier)
{ {
sf::Packet spac; sf::Packet spac;
spac << static_cast<MessageId>(NP_MSG_COMPUTE_MD5); spac << static_cast<MessageId>(NP_MSG_COMPUTE_MD5);
spac << file_identifier; SendSyncIdentifier(spac, sync_identifier);
SendAsyncToClients(std::move(spac)); SendAsyncToClients(std::move(spac));
@ -1260,7 +1279,7 @@ bool NetPlayServer::StartGame()
const sf::Uint64 initial_rtc = GetInitialNetPlayRTC(); const sf::Uint64 initial_rtc = GetInitialNetPlayRTC();
const std::string region = SConfig::GetDirectoryForRegion( const std::string region = SConfig::GetDirectoryForRegion(
SConfig::ToGameCubeRegion(m_dialog->FindGameFile(m_selected_game)->GetRegion())); SConfig::ToGameCubeRegion(m_dialog->FindGameFile(m_selected_game_identifier)->GetRegion()));
// sync GC SRAM with clients // sync GC SRAM with clients
if (!g_SRAM_netplay_initialized) if (!g_SRAM_netplay_initialized)
@ -1395,7 +1414,7 @@ bool NetPlayServer::SyncSaveData()
} }
} }
const auto game = m_dialog->FindGameFile(m_selected_game); const auto game = m_dialog->FindGameFile(m_selected_game_identifier);
if (game == nullptr) if (game == nullptr)
{ {
PanicAlertT("Selected game doesn't exist in game list!"); PanicAlertT("Selected game doesn't exist in game list!");
@ -1618,7 +1637,7 @@ bool NetPlayServer::SyncCodes()
m_codes_synced = false; m_codes_synced = false;
// Get Game Path // Get Game Path
const auto game = m_dialog->FindGameFile(m_selected_game); const auto game = m_dialog->FindGameFile(m_selected_game_identifier);
if (game == nullptr) if (game == nullptr)
{ {
PanicAlertT("Selected game doesn't exist in game list!"); PanicAlertT("Selected game doesn't exist in game list!");

View File

@ -5,6 +5,7 @@
#pragma once #pragma once
#include <SFML/Network/Packet.hpp> #include <SFML/Network/Packet.hpp>
#include <map> #include <map>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@ -14,19 +15,20 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
#include "Common/Event.h" #include "Common/Event.h"
#include "Common/QoSSession.h" #include "Common/QoSSession.h"
#include "Common/SPSCQueue.h" #include "Common/SPSCQueue.h"
#include "Common/Timer.h" #include "Common/Timer.h"
#include "Common/TraversalClient.h" #include "Common/TraversalClient.h"
#include "Core/NetPlayProto.h" #include "Core/NetPlayProto.h"
#include "Core/SyncIdentifier.h"
#include "InputCommon/GCPadStatus.h" #include "InputCommon/GCPadStatus.h"
#include "UICommon/NetPlayIndex.h" #include "UICommon/NetPlayIndex.h"
namespace NetPlay namespace NetPlay
{ {
class NetPlayUI; class NetPlayUI;
enum class PlayerGameStatus;
class NetPlayServer : public TraversalClientClient class NetPlayServer : public TraversalClientClient
{ {
@ -43,8 +45,8 @@ public:
const NetTraversalConfig& traversal_config); const NetTraversalConfig& traversal_config);
~NetPlayServer(); ~NetPlayServer();
bool ChangeGame(const std::string& game); bool ChangeGame(const SyncIdentifier& sync_identifier, const std::string& netplay_name);
bool ComputeMD5(const std::string& file_identifier); bool ComputeMD5(const SyncIdentifier& sync_identifier);
bool AbortMD5(); bool AbortMD5();
void SendChatMessage(const std::string& msg); void SendChatMessage(const std::string& msg);
@ -80,7 +82,7 @@ private:
PlayerId pid; PlayerId pid;
std::string name; std::string name;
std::string revision; std::string revision;
PlayerGameStatus game_status; SyncIdentifierComparison game_status;
bool has_ipl_dump; bool has_ipl_dump;
ENetPeer* socket; ENetPeer* socket;
@ -180,7 +182,8 @@ private:
Common::SPSCQueue<AsyncQueueEntry, false> m_async_queue; Common::SPSCQueue<AsyncQueueEntry, false> m_async_queue;
Common::SPSCQueue<ChunkedDataQueueEntry, false> m_chunked_data_queue; Common::SPSCQueue<ChunkedDataQueueEntry, false> m_chunked_data_queue;
std::string m_selected_game; SyncIdentifier m_selected_game_identifier;
std::string m_selected_game_name;
std::thread m_thread; std::thread m_thread;
Common::Event m_chunked_data_event; Common::Event m_chunked_data_event;
Common::Event m_chunked_data_complete_event; Common::Event m_chunked_data_complete_event;

View File

@ -0,0 +1,46 @@
// Copyright 2020 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <string>
#include <tuple>
#include "Common/CommonTypes.h"
namespace NetPlay
{
struct SyncIdentifier
{
u64 dol_elf_size;
std::string game_id;
u16 revision;
u8 disc_number;
bool is_datel;
// This hash is intended to be (but is not guaranteed to be):
// 1. Identical for discs with no differences that affect netplay/TAS sync
// 2. Different for discs with differences that affect netplay/TAS sync
// 3. Much faster than hashing the entire disc
// The way the hash is calculated may change with updates to Dolphin.
std::array<u8, 20> sync_hash;
bool operator==(const SyncIdentifier& s) const
{
return std::tie(dol_elf_size, game_id, revision, disc_number, is_datel, sync_hash) ==
std::tie(s.dol_elf_size, s.game_id, s.revision, s.disc_number, s.is_datel, s.sync_hash);
}
bool operator!=(const SyncIdentifier& s) const { return !operator==(s); }
};
enum class SyncIdentifierComparison
{
SameGame,
DifferentVersion,
DifferentGame,
Unknown,
};
} // namespace NetPlay

View File

@ -245,19 +245,26 @@ bool ExportBI2Data(const Volume& volume, const Partition& partition,
return ExportData(volume, partition, 0x440, 0x2000, export_filename); return ExportData(volume, partition, 0x440, 0x2000, export_filename);
} }
std::optional<u64> GetApploaderSize(const Volume& volume, const Partition& partition)
{
constexpr u64 header_size = 0x20;
const std::optional<u32> apploader_size = volume.ReadSwapped<u32>(0x2440 + 0x14, partition);
const std::optional<u32> trailer_size = volume.ReadSwapped<u32>(0x2440 + 0x18, partition);
if (!apploader_size || !trailer_size)
return std::nullopt;
return header_size + *apploader_size + *trailer_size;
}
bool ExportApploader(const Volume& volume, const Partition& partition, bool ExportApploader(const Volume& volume, const Partition& partition,
const std::string& export_filename) const std::string& export_filename)
{ {
if (!IsDisc(volume.GetVolumeType())) if (!IsDisc(volume.GetVolumeType()))
return false; return false;
std::optional<u32> apploader_size = volume.ReadSwapped<u32>(0x2440 + 0x14, partition); const std::optional<u64> apploader_size = GetApploaderSize(volume, partition);
const std::optional<u32> trailer_size = volume.ReadSwapped<u32>(0x2440 + 0x18, partition); if (!apploader_size)
constexpr u32 header_size = 0x20;
if (!apploader_size || !trailer_size)
return false; return false;
*apploader_size += *trailer_size + header_size;
DEBUG_LOG(DISCIO, "Apploader size -> %x", *apploader_size);
return ExportData(volume, partition, 0x2440, *apploader_size, export_filename); return ExportData(volume, partition, 0x2440, *apploader_size, export_filename);
} }

View File

@ -61,6 +61,7 @@ bool ExportHeader(const Volume& volume, const Partition& partition,
const std::string& export_filename); const std::string& export_filename);
bool ExportBI2Data(const Volume& volume, const Partition& partition, bool ExportBI2Data(const Volume& volume, const Partition& partition,
const std::string& export_filename); const std::string& export_filename);
std::optional<u64> GetApploaderSize(const Volume& volume, const Partition& partition);
bool ExportApploader(const Volume& volume, const Partition& partition, bool ExportApploader(const Volume& volume, const Partition& partition,
const std::string& export_filename); const std::string& export_filename);
std::optional<u64> GetBootDOLOffset(const Volume& volume, const Partition& partition); std::optional<u64> GetBootDOLOffset(const Volume& volume, const Partition& partition);

View File

@ -9,12 +9,16 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <type_traits>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <mbedtls/sha1.h>
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Core/IOS/ES/Formats.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
#include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeDisc.h"
@ -28,6 +32,43 @@ const IOS::ES::TicketReader Volume::INVALID_TICKET{};
const IOS::ES::TMDReader Volume::INVALID_TMD{}; const IOS::ES::TMDReader Volume::INVALID_TMD{};
const std::vector<u8> Volume::INVALID_CERT_CHAIN{}; const std::vector<u8> Volume::INVALID_CERT_CHAIN{};
template <typename T>
static void AddToSyncHash(mbedtls_sha1_context* context, const T& data)
{
static_assert(std::is_trivially_copyable_v<T>);
mbedtls_sha1_update_ret(context, reinterpret_cast<const u8*>(&data), sizeof(data));
}
void Volume::ReadAndAddToSyncHash(mbedtls_sha1_context* context, u64 offset, u64 length,
const Partition& partition) const
{
std::vector<u8> buffer(length);
if (Read(offset, length, buffer.data(), partition))
mbedtls_sha1_update_ret(context, buffer.data(), buffer.size());
}
void Volume::AddTMDToSyncHash(mbedtls_sha1_context* context, const Partition& partition) const
{
// We want to hash some important parts of the TMD, but nothing that changes when fakesigning.
// (Fakesigned WADs are very popular, and we don't want people with properly signed WADs to
// unnecessarily be at a disadvantage due to most netplay partners having fakesigned WADs.)
const IOS::ES::TMDReader& tmd = GetTMD(partition);
if (!tmd.IsValid())
return;
AddToSyncHash(context, tmd.GetIOSId());
AddToSyncHash(context, tmd.GetTitleId());
AddToSyncHash(context, tmd.GetTitleFlags());
AddToSyncHash(context, tmd.GetGroupId());
AddToSyncHash(context, tmd.GetRegion());
AddToSyncHash(context, tmd.GetTitleVersion());
AddToSyncHash(context, tmd.GetBootIndex());
for (const IOS::ES::Content& content : tmd.GetContents())
AddToSyncHash(context, content);
}
std::map<Language, std::string> Volume::ReadWiiNames(const std::vector<char16_t>& data) std::map<Language, std::string> Volume::ReadWiiNames(const std::vector<char16_t>& data)
{ {
std::map<Language, std::string> names; std::map<Language, std::string> names;

View File

@ -12,6 +12,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <mbedtls/sha1.h>
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/StringUtil.h" #include "Common/StringUtil.h"
#include "Common/Swap.h" #include "Common/Swap.h"
@ -143,6 +145,13 @@ public:
virtual u64 GetRawSize() const = 0; virtual u64 GetRawSize() const = 0;
virtual const BlobReader& GetBlobReader() const = 0; virtual const BlobReader& GetBlobReader() const = 0;
// This hash is intended to be (but is not guaranteed to be):
// 1. Identical for discs with no differences that affect netplay/TAS sync
// 2. Different for discs with differences that affect netplay/TAS sync
// 3. Much faster than hashing the entire disc
// The way the hash is calculated may change with updates to Dolphin.
virtual std::array<u8, 20> GetSyncHash() const = 0;
protected: protected:
template <u32 N> template <u32 N>
std::string DecodeString(const char (&data)[N]) const std::string DecodeString(const char (&data)[N]) const
@ -156,6 +165,10 @@ protected:
return CP1252ToUTF8(string); return CP1252ToUTF8(string);
} }
void ReadAndAddToSyncHash(mbedtls_sha1_context* context, u64 offset, u64 length,
const Partition& partition) const;
void AddTMDToSyncHash(mbedtls_sha1_context* context, const Partition& partition) const;
virtual u32 GetOffsetShift() const { return 0; } virtual u32 GetOffsetShift() const { return 0; }
static std::map<Language, std::string> ReadWiiNames(const std::vector<char16_t>& data); static std::map<Language, std::string> ReadWiiNames(const std::vector<char16_t>& data);

View File

@ -4,11 +4,17 @@
#include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeDisc.h"
#include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
#include <mbedtls/sha1.h>
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "DiscIO/DiscExtractor.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
#include "DiscIO/Filesystem.h"
namespace DiscIO namespace DiscIO
{ {
@ -90,4 +96,35 @@ bool VolumeDisc::IsNKit() const
return ReadSwapped<u32>(0x200, PARTITION_NONE) == NKIT_MAGIC; return ReadSwapped<u32>(0x200, PARTITION_NONE) == NKIT_MAGIC;
} }
void VolumeDisc::AddGamePartitionToSyncHash(mbedtls_sha1_context* context) const
{
const Partition partition = GetGamePartition();
// All headers at the beginning of the partition, plus the apploader
ReadAndAddToSyncHash(context, 0, 0x2440 + GetApploaderSize(*this, partition).value_or(0),
partition);
// Boot DOL (may be missing if this is a Datel disc)
const std::optional<u64> dol_offset = GetBootDOLOffset(*this, partition);
if (dol_offset)
{
ReadAndAddToSyncHash(context, *dol_offset,
GetBootDOLSize(*this, partition, *dol_offset).value_or(0), partition);
}
// File system
const std::optional<u64> fst_offset = GetFSTOffset(*this, partition);
if (fst_offset)
ReadAndAddToSyncHash(context, *fst_offset, GetFSTSize(*this, partition).value_or(0), partition);
// opening.bnr (name and banner)
const FileSystem* file_system = GetFileSystem(partition);
if (file_system)
{
std::unique_ptr<FileInfo> file_info = file_system->FindFileInfo("opening.bnr");
if (file_info && !file_info->IsDirectory())
ReadAndAddToSyncHash(context, file_info->GetOffset(), file_info->GetSize(), partition);
}
}
} // namespace DiscIO } // namespace DiscIO

View File

@ -7,6 +7,8 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <mbedtls/sha1.h>
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "DiscIO/Volume.h" #include "DiscIO/Volume.h"
@ -26,6 +28,7 @@ public:
protected: protected:
Region RegionCodeToRegion(std::optional<u32> region_code) const; Region RegionCodeToRegion(std::optional<u32> region_code) const;
void AddGamePartitionToSyncHash(mbedtls_sha1_context* context) const;
}; };
} // namespace DiscIO } // namespace DiscIO

View File

@ -11,6 +11,8 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <mbedtls/sha1.h>
#include "Common/Assert.h" #include "Common/Assert.h"
#include "Common/ColorUtil.h" #include "Common/ColorUtil.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
@ -137,6 +139,19 @@ bool VolumeGC::IsDatelDisc() const
return !GetBootDOLOffset(*this, PARTITION_NONE).has_value(); return !GetBootDOLOffset(*this, PARTITION_NONE).has_value();
} }
std::array<u8, 20> VolumeGC::GetSyncHash() const
{
mbedtls_sha1_context context;
mbedtls_sha1_init(&context);
mbedtls_sha1_starts_ret(&context);
AddGamePartitionToSyncHash(&context);
std::array<u8, 20> hash;
mbedtls_sha1_finish_ret(&context, hash.data());
return hash;
}
VolumeGC::ConvertedGCBanner VolumeGC::LoadBannerFile() const VolumeGC::ConvertedGCBanner VolumeGC::LoadBannerFile() const
{ {
GCBanner banner_file; GCBanner banner_file;

View File

@ -51,6 +51,8 @@ public:
u64 GetRawSize() const override; u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override; const BlobReader& GetBlobReader() const override;
std::array<u8, 20> GetSyncHash() const override;
private: private:
static const u32 GC_BANNER_WIDTH = 96; static const u32 GC_BANNER_WIDTH = 96;
static const u32 GC_BANNER_HEIGHT = 32; static const u32 GC_BANNER_HEIGHT = 32;

View File

@ -479,8 +479,8 @@ std::vector<Partition> VolumeVerifier::CheckPartitions()
AddProblem(Severity::Low, AddProblem(Severity::Low,
Common::GetStringT( Common::GetStringT(
"The data partition is not at its normal position. This will affect the " "The data partition is not at its normal position. This will affect the "
"emulated loading times. When using NetPlay or sending input recordings to " "emulated loading times. You will be unable to share input recordings and use "
"other people, you will experience desyncs if anyone is using a good dump.")); "NetPlay with anyone who is using a good dump."));
} }
} }
@ -783,10 +783,10 @@ void VolumeVerifier::CheckDiscSize(const std::vector<Partition>& partitions)
{ {
AddProblem( AddProblem(
Severity::Low, Severity::Low,
Common::GetStringT("This disc image has an unusual size. This will likely make the " Common::GetStringT(
"emulated loading times longer. When using NetPlay or sending " "This disc image has an unusual size. This will likely make the emulated "
"input recordings to other people, you will likely experience " "loading times longer. You will likely be unable to share input recordings "
"desyncs if anyone is using a good dump.")); "and use NetPlay with anyone who is using a good dump."));
} }
else else
{ {

View File

@ -40,6 +40,7 @@ VolumeWAD::VolumeWAD(std::unique_ptr<BlobReader> reader) : m_reader(std::move(re
m_ticket_size = m_reader->ReadSwapped<u32>(0x10).value_or(0); m_ticket_size = m_reader->ReadSwapped<u32>(0x10).value_or(0);
m_tmd_size = m_reader->ReadSwapped<u32>(0x14).value_or(0); m_tmd_size = m_reader->ReadSwapped<u32>(0x14).value_or(0);
m_data_size = m_reader->ReadSwapped<u32>(0x18).value_or(0); m_data_size = m_reader->ReadSwapped<u32>(0x18).value_or(0);
m_opening_bnr_size = m_reader->ReadSwapped<u32>(0x1C).value_or(0);
m_cert_chain_offset = Common::AlignUp(m_hdr_size, 0x40); m_cert_chain_offset = Common::AlignUp(m_hdr_size, 0x40);
m_ticket_offset = m_cert_chain_offset + Common::AlignUp(m_cert_chain_size, 0x40); m_ticket_offset = m_cert_chain_offset + Common::AlignUp(m_cert_chain_size, 0x40);
@ -342,4 +343,22 @@ const BlobReader& VolumeWAD::GetBlobReader() const
return *m_reader; return *m_reader;
} }
std::array<u8, 20> VolumeWAD::GetSyncHash() const
{
// We can skip hashing the contents since the TMD contains hashes of the contents.
// We specifically don't hash the ticket, since its console ID can differ without any problems.
mbedtls_sha1_context context;
mbedtls_sha1_init(&context);
mbedtls_sha1_starts_ret(&context);
AddTMDToSyncHash(&context, PARTITION_NONE);
ReadAndAddToSyncHash(&context, m_opening_bnr_offset, m_opening_bnr_size, PARTITION_NONE);
std::array<u8, 20> hash;
mbedtls_sha1_finish_ret(&context, hash.data());
return hash;
}
} // namespace DiscIO } // namespace DiscIO

View File

@ -70,6 +70,8 @@ public:
u64 GetRawSize() const override; u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override; const BlobReader& GetBlobReader() const override;
std::array<u8, 20> GetSyncHash() const override;
private: private:
std::unique_ptr<BlobReader> m_reader; std::unique_ptr<BlobReader> m_reader;
IOS::ES::TicketReader m_ticket; IOS::ES::TicketReader m_ticket;
@ -85,6 +87,7 @@ private:
u32 m_ticket_size = 0; u32 m_ticket_size = 0;
u32 m_tmd_size = 0; u32 m_tmd_size = 0;
u32 m_data_size = 0; u32 m_data_size = 0;
u32 m_opening_bnr_size = 0;
}; };
} // namespace DiscIO } // namespace DiscIO

View File

@ -362,6 +362,33 @@ const BlobReader& VolumeWii::GetBlobReader() const
return *m_reader; return *m_reader;
} }
std::array<u8, 20> VolumeWii::GetSyncHash() const
{
mbedtls_sha1_context context;
mbedtls_sha1_init(&context);
mbedtls_sha1_starts_ret(&context);
// Disc header
ReadAndAddToSyncHash(&context, 0, 0x80, PARTITION_NONE);
// Region code
ReadAndAddToSyncHash(&context, 0x4E000, 4, PARTITION_NONE);
// The data offset of the game partition - an important factor for disc drive timings
const u64 data_offset = PartitionOffsetToRawOffset(0, GetGamePartition());
mbedtls_sha1_update_ret(&context, reinterpret_cast<const u8*>(&data_offset), sizeof(data_offset));
// TMD
AddTMDToSyncHash(&context, GetGamePartition());
// Game partition contents
AddGamePartitionToSyncHash(&context);
std::array<u8, 20> hash;
mbedtls_sha1_finish_ret(&context, hash.data());
return hash;
}
bool VolumeWii::CheckH3TableIntegrity(const Partition& partition) const bool VolumeWii::CheckH3TableIntegrity(const Partition& partition) const
{ {
auto it = m_partitions.find(partition); auto it = m_partitions.find(partition);

View File

@ -92,6 +92,7 @@ public:
bool IsSizeAccurate() const override; bool IsSizeAccurate() const override;
u64 GetRawSize() const override; u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override; const BlobReader& GetBlobReader() const override;
std::array<u8, 20> GetSyncHash() const override;
// The in parameter can either contain all the data to begin with, // The in parameter can either contain all the data to begin with,
// or read_function can write data into the in parameter when called. // or read_function can write data into the in parameter when called.

View File

@ -398,9 +398,7 @@ void GameList::ShowContextMenu(const QPoint&)
QAction* netplay_host = new QAction(tr("Host with NetPlay"), menu); QAction* netplay_host = new QAction(tr("Host with NetPlay"), menu);
connect(netplay_host, &QAction::triggered, [this, game] { connect(netplay_host, &QAction::triggered, [this, game] { emit NetPlayHost(*game); });
emit NetPlayHost(QString::fromStdString(game->GetUniqueIdentifier()));
});
connect(&Settings::Instance(), &Settings::EmulationStateChanged, menu, [=](Core::State state) { connect(&Settings::Instance(), &Settings::EmulationStateChanged, menu, [=](Core::State state) {
netplay_host->setEnabled(state == Core::State::Uninitialized); netplay_host->setEnabled(state == Core::State::Uninitialized);
@ -740,6 +738,11 @@ GameList::FindSecondDisc(const UICommon::GameFile& game) const
return m_model->FindSecondDisc(game); return m_model->FindSecondDisc(game);
} }
std::string GameList::GetNetPlayName(const UICommon::GameFile& game) const
{
return m_model->GetNetPlayName(game);
}
void GameList::SetViewColumn(int col, bool view) void GameList::SetViewColumn(int col, bool view)
{ {
m_list->setColumnHidden(col, !view); m_list->setColumnHidden(col, !view);

View File

@ -32,6 +32,7 @@ public:
bool HasMultipleSelected() const; bool HasMultipleSelected() const;
std::shared_ptr<const UICommon::GameFile> FindGame(const std::string& path) const; std::shared_ptr<const UICommon::GameFile> FindGame(const std::string& path) const;
std::shared_ptr<const UICommon::GameFile> FindSecondDisc(const UICommon::GameFile& game) const; std::shared_ptr<const UICommon::GameFile> FindSecondDisc(const UICommon::GameFile& game) const;
std::string GetNetPlayName(const UICommon::GameFile& game) const;
void SetListView() { SetPreferredView(true); } void SetListView() { SetPreferredView(true); }
void SetGridView() { SetPreferredView(false); } void SetGridView() { SetPreferredView(false); }
@ -47,7 +48,7 @@ public:
signals: signals:
void GameSelected(); void GameSelected();
void NetPlayHost(const QString& game_id); void NetPlayHost(const UICommon::GameFile& game);
void SelectionChanged(std::shared_ptr<const UICommon::GameFile> game_file); void SelectionChanged(std::shared_ptr<const UICommon::GameFile> game_file);
void OpenGeneralSettings(); void OpenGeneralSettings();

View File

@ -313,14 +313,9 @@ std::shared_ptr<const UICommon::GameFile> GameListModel::GetGameFile(int index)
return m_games[index]; return m_games[index];
} }
QString GameListModel::GetPath(int index) const std::string GameListModel::GetNetPlayName(const UICommon::GameFile& game) const
{ {
return QString::fromStdString(m_games[index]->GetFilePath()); return game.GetNetPlayName(m_title_database);
}
QString GameListModel::GetUniqueIdentifier(int index) const
{
return QString::fromStdString(m_games[index]->GetUniqueIdentifier());
} }
void GameListModel::AddGame(const std::shared_ptr<const UICommon::GameFile>& game) void GameListModel::AddGame(const std::shared_ptr<const UICommon::GameFile>& game)

View File

@ -37,10 +37,7 @@ public:
int columnCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override;
std::shared_ptr<const UICommon::GameFile> GetGameFile(int index) const; std::shared_ptr<const UICommon::GameFile> GetGameFile(int index) const;
// Path of the game at the specified index. std::string GetNetPlayName(const UICommon::GameFile& game) const;
QString GetPath(int index) const;
// Unique identifier of the game at the specified index.
QString GetUniqueIdentifier(int index) const;
bool ShouldDisplayGameListItem(int index) const; bool ShouldDisplayGameListItem(int index) const;
void SetSearchTerm(const QString& term); void SetSearchTerm(const QString& term);

View File

@ -1374,7 +1374,7 @@ bool MainWindow::NetPlayJoin()
return true; return true;
} }
bool MainWindow::NetPlayHost(const QString& game_id) bool MainWindow::NetPlayHost(const UICommon::GameFile& game)
{ {
if (Core::IsRunning()) if (Core::IsRunning())
{ {
@ -1419,7 +1419,8 @@ bool MainWindow::NetPlayHost(const QString& game_id)
return false; return false;
} }
Settings::Instance().GetNetPlayServer()->ChangeGame(game_id.toStdString()); Settings::Instance().GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(),
m_game_list->GetNetPlayName(game));
// Join our local server // Join our local server
return NetPlayJoin(); return NetPlayJoin();

View File

@ -158,7 +158,7 @@ private:
void NetPlayInit(); void NetPlayInit();
bool NetPlayJoin(); bool NetPlayJoin();
bool NetPlayHost(const QString& game_id); bool NetPlayHost(const UICommon::GameFile& game);
void NetPlayQuit(); void NetPlayQuit();
void OnBootGameCubeIPL(DiscIO::Region region); void OnBootGameCubeIPL(DiscIO::Region region);

View File

@ -4,12 +4,15 @@
#include "DolphinQt/NetPlay/GameListDialog.h" #include "DolphinQt/NetPlay/GameListDialog.h"
#include <memory>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QListWidget> #include <QListWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "DolphinQt/GameList/GameListModel.h" #include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/Settings.h" #include "DolphinQt/Settings.h"
#include "UICommon/GameFile.h"
GameListDialog::GameListDialog(QWidget* parent) : QDialog(parent) GameListDialog::GameListDialog(QWidget* parent) : QDialog(parent)
{ {
@ -35,12 +38,8 @@ void GameListDialog::CreateWidgets()
void GameListDialog::ConnectWidgets() void GameListDialog::ConnectWidgets()
{ {
connect(m_game_list, &QListWidget::itemSelectionChanged, [this] { connect(m_game_list, &QListWidget::itemSelectionChanged,
int row = m_game_list->currentRow(); [this] { m_button_box->setEnabled(m_game_list->currentRow() != -1); });
m_button_box->setEnabled(row != -1);
m_game_id = m_game_list->currentItem()->text();
});
connect(m_game_list, &QListWidget::itemDoubleClicked, this, &GameListDialog::accept); connect(m_game_list, &QListWidget::itemDoubleClicked, this, &GameListDialog::accept);
connect(m_button_box, &QDialogButtonBox::accepted, this, &GameListDialog::accept); connect(m_button_box, &QDialogButtonBox::accepted, this, &GameListDialog::accept);
@ -54,16 +53,21 @@ void GameListDialog::PopulateGameList()
for (int i = 0; i < game_list_model->rowCount(QModelIndex()); i++) for (int i = 0; i < game_list_model->rowCount(QModelIndex()); i++)
{ {
auto* item = new QListWidgetItem(game_list_model->GetUniqueIdentifier(i)); std::shared_ptr<const UICommon::GameFile> game = game_list_model->GetGameFile(i);
auto* item =
new QListWidgetItem(QString::fromStdString(game_list_model->GetNetPlayName(*game)));
item->setData(Qt::UserRole, QVariant::fromValue(std::move(game)));
m_game_list->addItem(item); m_game_list->addItem(item);
} }
m_game_list->sortItems(); m_game_list->sortItems();
} }
const QString& GameListDialog::GetSelectedUniqueID() const const UICommon::GameFile& GameListDialog::GetSelectedGame() const
{ {
return m_game_id; auto items = m_game_list->selectedItems();
return *items[0]->data(Qt::UserRole).value<std::shared_ptr<const UICommon::GameFile>>();
} }
int GameListDialog::exec() int GameListDialog::exec()

View File

@ -11,6 +11,11 @@ class QVBoxLayout;
class QListWidget; class QListWidget;
class QDialogButtonBox; class QDialogButtonBox;
namespace UICommon
{
class GameFile;
}
class GameListDialog : public QDialog class GameListDialog : public QDialog
{ {
Q_OBJECT Q_OBJECT
@ -18,7 +23,7 @@ public:
explicit GameListDialog(QWidget* parent); explicit GameListDialog(QWidget* parent);
int exec() override; int exec() override;
const QString& GetSelectedUniqueID() const; const UICommon::GameFile& GetSelectedGame() const;
private: private:
void CreateWidgets(); void CreateWidgets();
@ -28,5 +33,4 @@ private:
QVBoxLayout* m_main_layout; QVBoxLayout* m_main_layout;
QListWidget* m_game_list; QListWidget* m_game_list;
QDialogButtonBox* m_button_box; QDialogButtonBox* m_button_box;
QString m_game_id;
}; };

View File

@ -21,6 +21,7 @@
#include <QTableWidget> #include <QTableWidget>
#include <QTextBrowser> #include <QTextBrowser>
#include <algorithm>
#include <sstream> #include <sstream>
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
@ -37,6 +38,7 @@
#include "Core/ConfigManager.h" #include "Core/ConfigManager.h"
#include "Core/Core.h" #include "Core/Core.h"
#include "Core/NetPlayServer.h" #include "Core/NetPlayServer.h"
#include "Core/SyncIdentifier.h"
#include "DolphinQt/GameList/GameListModel.h" #include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/NetPlay/ChunkedProgressDialog.h" #include "DolphinQt/NetPlay/ChunkedProgressDialog.h"
@ -153,17 +155,19 @@ void NetPlayDialog::CreateMainLayout()
m_md5_menu = m_menu_bar->addMenu(tr("Checksum")); m_md5_menu = m_menu_bar->addMenu(tr("Checksum"));
m_md5_menu->addAction(tr("Current game"), this, [this] { m_md5_menu->addAction(tr("Current game"), this, [this] {
Settings::Instance().GetNetPlayServer()->ComputeMD5(m_current_game); Settings::Instance().GetNetPlayServer()->ComputeMD5(m_current_game_identifier);
}); });
m_md5_menu->addAction(tr("Other game..."), this, [this] { m_md5_menu->addAction(tr("Other game..."), this, [this] {
GameListDialog gld(this); GameListDialog gld(this);
if (gld.exec() != QDialog::Accepted) if (gld.exec() != QDialog::Accepted)
return; return;
Settings::Instance().GetNetPlayServer()->ComputeMD5(gld.GetSelectedUniqueID().toStdString()); Settings::Instance().GetNetPlayServer()->ComputeMD5(gld.GetSelectedGame().GetSyncIdentifier());
});
m_md5_menu->addAction(tr("SD Card"), this, [] {
Settings::Instance().GetNetPlayServer()->ComputeMD5(
NetPlay::NetPlayClient::GetSDCardIdentifier());
}); });
m_md5_menu->addAction(tr("SD Card"), this,
[] { Settings::Instance().GetNetPlayServer()->ComputeMD5(WII_SDCARD); });
m_other_menu = m_menu_bar->addMenu(tr("Other")); m_other_menu = m_menu_bar->addMenu(tr("Other"));
m_record_input_action = m_other_menu->addAction(tr("Record Inputs")); m_record_input_action = m_other_menu->addAction(tr("Record Inputs"));
@ -321,9 +325,14 @@ void NetPlayDialog::ConnectWidgets()
GameListDialog gld(this); GameListDialog gld(this);
if (gld.exec() == QDialog::Accepted) if (gld.exec() == QDialog::Accepted)
{ {
auto unique_id = gld.GetSelectedUniqueID(); Settings& settings = Settings::Instance();
Settings::Instance().GetNetPlayServer()->ChangeGame(unique_id.toStdString());
Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"), unique_id); const UICommon::GameFile& game = gld.GetSelectedGame();
const std::string netplay_name = settings.GetGameListModel()->GetNetPlayName(game);
settings.GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(), netplay_name);
Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"),
QString::fromStdString(netplay_name));
} }
}); });
@ -416,7 +425,7 @@ void NetPlayDialog::OnStart()
return; return;
} }
const auto game = FindGameFile(m_current_game); const auto game = FindGameFile(m_current_game_identifier);
if (!game) if (!game)
{ {
PanicAlertT("Selected game doesn't exist in game list!"); PanicAlertT("Selected game doesn't exist in game list!");
@ -583,11 +592,12 @@ void NetPlayDialog::UpdateDiscordPresence()
{ {
#ifdef USE_DISCORD_PRESENCE #ifdef USE_DISCORD_PRESENCE
// both m_current_game and m_player_count need to be set for the status to be displayed correctly // both m_current_game and m_player_count need to be set for the status to be displayed correctly
if (m_player_count == 0 || m_current_game.empty()) if (m_player_count == 0 || m_current_game_name.empty())
return; return;
const auto use_default = [this]() { const auto use_default = [this]() {
Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::Empty, "", m_current_game); Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::Empty, "",
m_current_game_name);
}; };
if (Core::IsRunning()) if (Core::IsRunning())
@ -602,7 +612,8 @@ void NetPlayDialog::UpdateDiscordPresence()
return use_default(); return use_default();
Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::RoomID, Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::RoomID,
std::string(host_id.begin(), host_id.end()), m_current_game); std::string(host_id.begin(), host_id.end()),
m_current_game_name);
} }
else else
{ {
@ -612,7 +623,7 @@ void NetPlayDialog::UpdateDiscordPresence()
Discord::UpdateDiscordPresence( Discord::UpdateDiscordPresence(
m_player_count, Discord::SecretType::IPAddress, m_player_count, Discord::SecretType::IPAddress,
Discord::CreateSecretFromIPAddress(*m_external_ip_address, port), m_current_game); Discord::CreateSecretFromIPAddress(*m_external_ip_address, port), m_current_game_name);
} }
} }
else else
@ -660,9 +671,10 @@ void NetPlayDialog::UpdateGUI()
return '|' + str + '|'; return '|' + str + '|';
}; };
static const std::map<NetPlay::PlayerGameStatus, QString> player_status{ static const std::map<NetPlay::SyncIdentifierComparison, QString> player_status{
{NetPlay::PlayerGameStatus::Ok, tr("OK")}, {NetPlay::SyncIdentifierComparison::SameGame, tr("OK")},
{NetPlay::PlayerGameStatus::NotFound, tr("Not Found")}, {NetPlay::SyncIdentifierComparison::DifferentVersion, tr("Wrong Version")},
{NetPlay::SyncIdentifierComparison::DifferentGame, tr("Not Found")},
}; };
for (int i = 0; i < m_player_count; i++) for (int i = 0; i < m_player_count; i++)
@ -805,15 +817,17 @@ void NetPlayDialog::AppendChat(const std::string& msg)
QApplication::alert(this); QApplication::alert(this);
} }
void NetPlayDialog::OnMsgChangeGame(const std::string& title) void NetPlayDialog::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier,
const std::string& netplay_name)
{ {
QString qtitle = QString::fromStdString(title); QString qname = QString::fromStdString(netplay_name);
QueueOnObject(this, [this, qtitle, title] { QueueOnObject(this, [this, qname, netplay_name, &sync_identifier] {
m_game_button->setText(qtitle); m_game_button->setText(qname);
m_current_game = title; m_current_game_identifier = sync_identifier;
m_current_game_name = netplay_name;
UpdateDiscordPresence(); UpdateDiscordPresence();
}); });
DisplayMessage(tr("Game changed to \"%1\"").arg(qtitle), "magenta"); DisplayMessage(tr("Game changed to \"%1\"").arg(qname), "magenta");
} }
void NetPlayDialog::GameStatusChanged(bool running) void NetPlayDialog::GameStatusChanged(bool running)
@ -859,7 +873,12 @@ void NetPlayDialog::OnMsgStartGame()
auto client = Settings::Instance().GetNetPlayClient(); auto client = Settings::Instance().GetNetPlayClient();
if (client) if (client)
client->StartGame(FindGame(m_current_game)); {
if (auto game = FindGameFile(m_current_game_identifier))
client->StartGame(game->GetFilePath());
else
PanicAlertT("Selected game doesn't exist in game list!");
}
UpdateDiscordPresence(); UpdateDiscordPresence();
}); });
} }
@ -1017,29 +1036,24 @@ bool NetPlayDialog::IsRecording()
return false; return false;
} }
std::string NetPlayDialog::FindGame(const std::string& game) std::shared_ptr<const UICommon::GameFile>
NetPlayDialog::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier,
NetPlay::SyncIdentifierComparison* found)
{ {
std::optional<std::string> path = RunOnObject(this, [this, &game] { NetPlay::SyncIdentifierComparison temp;
for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) if (!found)
{ found = &temp;
if (m_game_list_model->GetUniqueIdentifier(i).toStdString() == game)
return m_game_list_model->GetPath(i).toStdString(); *found = NetPlay::SyncIdentifierComparison::DifferentGame;
}
return std::string("");
});
if (path)
return *path;
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 = std::optional<std::shared_ptr<const UICommon::GameFile>> game_file =
RunOnObject(this, [this, &game] { RunOnObject(this, [this, &sync_identifier, found] {
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) auto game_file = m_game_list_model->GetGameFile(i);
return m_game_list_model->GetGameFile(i); *found = std::min(*found, game_file->CompareSyncIdentifier(sync_identifier));
if (*found == NetPlay::SyncIdentifierComparison::SameGame)
return game_file;
} }
return static_cast<std::shared_ptr<const UICommon::GameFile>>(nullptr); return static_cast<std::shared_ptr<const UICommon::GameFile>>(nullptr);
}); });
@ -1126,15 +1140,15 @@ void NetPlayDialog::SaveSettings()
Config::SetBase(Config::NETPLAY_NETWORK_MODE, network_mode); Config::SetBase(Config::NETPLAY_NETWORK_MODE, network_mode);
} }
void NetPlayDialog::ShowMD5Dialog(const std::string& file_identifier) void NetPlayDialog::ShowMD5Dialog(const std::string& title)
{ {
QueueOnObject(this, [this, file_identifier] { QueueOnObject(this, [this, title] {
m_md5_menu->setEnabled(false); m_md5_menu->setEnabled(false);
if (m_md5_dialog->isVisible()) if (m_md5_dialog->isVisible())
m_md5_dialog->close(); m_md5_dialog->close();
m_md5_dialog->show(QString::fromStdString(file_identifier)); m_md5_dialog->show(QString::fromStdString(title));
}); });
} }

View File

@ -45,7 +45,8 @@ public:
void Update() override; void Update() override;
void AppendChat(const std::string& msg) override; void AppendChat(const std::string& msg) override;
void OnMsgChangeGame(const std::string& filename) override; void OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier,
const std::string& netplay_name) override;
void OnMsgStartGame() override; void OnMsgStartGame() override;
void OnMsgStopGame() override; void OnMsgStopGame() override;
void OnMsgPowerButton() override; void OnMsgPowerButton() override;
@ -65,13 +66,14 @@ public:
void OnIndexRefreshFailed(const std::string error) override; void OnIndexRefreshFailed(const std::string error) override;
bool IsRecording() override; bool IsRecording() override;
std::string FindGame(const std::string& game) override; std::shared_ptr<const UICommon::GameFile>
std::shared_ptr<const UICommon::GameFile> FindGameFile(const std::string& game) override; FindGameFile(const NetPlay::SyncIdentifier& sync_identifier,
NetPlay::SyncIdentifierComparison* found = nullptr) override;
void LoadSettings(); void LoadSettings();
void SaveSettings(); void SaveSettings();
void ShowMD5Dialog(const std::string& file_identifier) override; void ShowMD5Dialog(const std::string& title) 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;
void AbortMD5() override; void AbortMD5() override;
@ -145,7 +147,8 @@ private:
MD5Dialog* m_md5_dialog; MD5Dialog* m_md5_dialog;
ChunkedProgressDialog* m_chunked_progress_dialog; ChunkedProgressDialog* m_chunked_progress_dialog;
PadMappingDialog* m_pad_mapping; PadMappingDialog* m_pad_mapping;
std::string m_current_game; NetPlay::SyncIdentifier m_current_game_identifier;
std::string m_current_game_name;
Common::Lazy<std::string> m_external_ip_address; Common::Lazy<std::string> m_external_ip_address;
std::string m_nickname; std::string m_nickname;
GameListModel* m_game_list_model = nullptr; GameListModel* m_game_list_model = nullptr;

View File

@ -4,6 +4,8 @@
#include "DolphinQt/NetPlay/NetPlaySetupDialog.h" #include "DolphinQt/NetPlay/NetPlaySetupDialog.h"
#include <memory>
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QDialogButtonBox> #include <QDialogButtonBox>
@ -24,6 +26,7 @@
#include "DolphinQt/QtUtils/UTF8CodePointCountValidator.h" #include "DolphinQt/QtUtils/UTF8CodePointCountValidator.h"
#include "DolphinQt/Settings.h" #include "DolphinQt/Settings.h"
#include "UICommon/GameFile.h"
#include "UICommon/NetPlayIndex.h" #include "UICommon/NetPlayIndex.h"
NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent) NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent)
@ -347,7 +350,7 @@ void NetPlaySetupDialog::accept()
return; return;
} }
emit Host(items[0]->text()); emit Host(*items[0]->data(Qt::UserRole).value<std::shared_ptr<const UICommon::GameFile>>());
} }
} }
@ -358,11 +361,11 @@ void NetPlaySetupDialog::PopulateGameList()
m_host_games->clear(); m_host_games->clear();
for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++)
{ {
auto title = m_game_list_model->GetUniqueIdentifier(i); std::shared_ptr<const UICommon::GameFile> game = m_game_list_model->GetGameFile(i);
auto path = m_game_list_model->GetPath(i);
auto* item = new QListWidgetItem(title); auto* item =
item->setData(Qt::UserRole, path); new QListWidgetItem(QString::fromStdString(m_game_list_model->GetNetPlayName(*game)));
item->setData(Qt::UserRole, QVariant::fromValue(std::move(game)));
m_host_games->addItem(item); m_host_games->addItem(item);
} }

View File

@ -18,6 +18,11 @@ class QPushButton;
class QSpinBox; class QSpinBox;
class QTabWidget; class QTabWidget;
namespace UICommon
{
class GameFile;
}
class NetPlaySetupDialog : public QDialog class NetPlaySetupDialog : public QDialog
{ {
Q_OBJECT Q_OBJECT
@ -29,7 +34,7 @@ public:
signals: signals:
bool Join(); bool Join();
bool Host(const QString& game_identifier); bool Host(const UICommon::GameFile& game);
private: private:
void CreateMainLayout(); void CreateMainLayout();

View File

@ -5,6 +5,7 @@
#include "UICommon/GameFile.h" #include "UICommon/GameFile.h"
#include <algorithm> #include <algorithm>
#include <array>
#include <cinttypes> #include <cinttypes>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@ -13,11 +14,13 @@
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view>
#include <tuple> #include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <fmt/format.h> #include <fmt/format.h>
#include <mbedtls/sha1.h>
#include <pugixml.hpp> #include <pugixml.hpp>
#include "Common/ChunkFile.h" #include "Common/ChunkFile.h"
@ -532,7 +535,7 @@ std::vector<DiscIO::Language> GameFile::GetLanguages() const
return languages; return languages;
} }
std::string GameFile::GetUniqueIdentifier() const std::string GameFile::GetNetPlayName(const Core::TitleDatabase& title_database) const
{ {
std::vector<std::string> info; std::vector<std::string> info;
if (!GetGameID().empty()) if (!GetGameID().empty())
@ -540,12 +543,7 @@ std::string GameFile::GetUniqueIdentifier() const
if (GetRevision() != 0) if (GetRevision() != 0)
info.push_back("Revision " + std::to_string(GetRevision())); info.push_back("Revision " + std::to_string(GetRevision()));
std::string name = GetLongName(DiscIO::Language::English); const std::string name = GetName(title_database);
if (name.empty())
{
// Use the file name as a fallback. Not necessarily consistent, but it's the best we have
name = m_file_name;
}
int disc_number = GetDiscNumber() + 1; int disc_number = GetDiscNumber() + 1;
@ -566,6 +564,66 @@ std::string GameFile::GetUniqueIdentifier() const
return name + " (" + ss.str() + ")"; return name + " (" + ss.str() + ")";
} }
std::array<u8, 20> GameFile::GetSyncHash() const
{
std::array<u8, 20> hash{};
if (m_platform == DiscIO::Platform::ELFOrDOL)
{
std::string buffer;
if (File::ReadFileToString(m_file_path, buffer))
mbedtls_sha1_ret(reinterpret_cast<unsigned char*>(buffer.data()), buffer.size(), hash.data());
}
else
{
if (std::unique_ptr<DiscIO::Volume> volume = DiscIO::CreateVolume(m_file_path))
hash = volume->GetSyncHash();
}
return hash;
}
NetPlay::SyncIdentifier GameFile::GetSyncIdentifier() const
{
const u64 dol_elf_size = m_platform == DiscIO::Platform::ELFOrDOL ? m_file_size : 0;
return NetPlay::SyncIdentifier{dol_elf_size, m_game_id, m_revision,
m_disc_number, m_is_datel_disc, GetSyncHash()};
}
NetPlay::SyncIdentifierComparison
GameFile::CompareSyncIdentifier(const NetPlay::SyncIdentifier& sync_identifier) const
{
const bool is_elf_or_dol = m_platform == DiscIO::Platform::ELFOrDOL;
if ((is_elf_or_dol ? m_file_size : 0) != sync_identifier.dol_elf_size)
return NetPlay::SyncIdentifierComparison::DifferentGame;
const auto trim = [](const std::string& str, size_t n) {
return std::string_view(str.data(), std::min(n, str.size()));
};
if (trim(m_game_id, 3) != trim(sync_identifier.game_id, 3))
return NetPlay::SyncIdentifierComparison::DifferentGame;
if (m_disc_number != sync_identifier.disc_number || m_is_datel_disc != sync_identifier.is_datel)
return NetPlay::SyncIdentifierComparison::DifferentGame;
const NetPlay::SyncIdentifierComparison mismatch_result =
is_elf_or_dol || m_is_datel_disc ? NetPlay::SyncIdentifierComparison::DifferentGame :
NetPlay::SyncIdentifierComparison::DifferentVersion;
if (m_game_id != sync_identifier.game_id)
{
const bool game_id_is_title_id = m_game_id.size() > 6 || sync_identifier.game_id.size() > 6;
return game_id_is_title_id ? NetPlay::SyncIdentifierComparison::DifferentGame : mismatch_result;
}
if (m_revision != sync_identifier.revision)
return mismatch_result;
return GetSyncHash() == sync_identifier.sync_hash ? NetPlay::SyncIdentifierComparison::SameGame :
mismatch_result;
}
std::string GameFile::GetWiiFSPath() const std::string GameFile::GetWiiFSPath() const
{ {
ASSERT(DiscIO::IsWii(m_platform)); ASSERT(DiscIO::IsWii(m_platform));

View File

@ -4,11 +4,13 @@
#pragma once #pragma once
#include <array>
#include <map> #include <map>
#include <string> #include <string>
#include <vector> #include <vector>
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Core/SyncIdentifier.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
@ -80,7 +82,16 @@ public:
u16 GetRevision() const { return m_revision; } u16 GetRevision() const { return m_revision; }
// 0 is the first disc, 1 is the second disc // 0 is the first disc, 1 is the second disc
u8 GetDiscNumber() const { return m_disc_number; } u8 GetDiscNumber() const { return m_disc_number; }
std::string GetUniqueIdentifier() const; std::string GetNetPlayName(const Core::TitleDatabase& title_database) const;
// This function is slow
std::array<u8, 20> GetSyncHash() const;
// This function is slow
NetPlay::SyncIdentifier GetSyncIdentifier() const;
// This function is slow if all of game_id, revision, disc_number, is_datel are identical
NetPlay::SyncIdentifierComparison
CompareSyncIdentifier(const NetPlay::SyncIdentifier& sync_identifier) const;
std::string GetWiiFSPath() const; std::string GetWiiFSPath() const;
DiscIO::Region GetRegion() const { return m_region; } DiscIO::Region GetRegion() const { return m_region; }
DiscIO::Country GetCountry() const { return m_country; } DiscIO::Country GetCountry() const { return m_country; }