From a41166bb376820389d9863ce23cf6fab20cb3b5f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 7 Jun 2020 22:58:03 +0200 Subject: [PATCH] Make netplay's "same game" check more robust Instead of comparing the game ID, revision, disc number and name, we can compare a hash of important parts of the disc including all the aforementioned data but also additional data such as the FST. The primary reason why I'm making this change is to let us catch more desyncs before they happen, but this should also fix https://bugs.dolphin-emu.org/issues/12115. As a bonus, the UI can now distinguish the case where a client doesn't have the game at all from the case where a client has the wrong version of the game. --- Source/Core/Core/CMakeLists.txt | 1 + Source/Core/Core/Core.vcxproj | 1 + Source/Core/Core/Core.vcxproj.filters | 3 +- Source/Core/Core/NetPlayClient.cpp | 71 ++++++++++---- Source/Core/Core/NetPlayClient.h | 26 +++-- Source/Core/Core/NetPlayServer.cpp | 45 ++++++--- Source/Core/Core/NetPlayServer.h | 13 ++- Source/Core/Core/SyncIdentifier.h | 46 +++++++++ Source/Core/DiscIO/DiscExtractor.cpp | 19 ++-- Source/Core/DiscIO/DiscExtractor.h | 1 + Source/Core/DiscIO/Volume.cpp | 41 ++++++++ Source/Core/DiscIO/Volume.h | 13 +++ Source/Core/DiscIO/VolumeDisc.cpp | 37 +++++++ Source/Core/DiscIO/VolumeDisc.h | 3 + Source/Core/DiscIO/VolumeGC.cpp | 15 +++ Source/Core/DiscIO/VolumeGC.h | 2 + Source/Core/DiscIO/VolumeWad.cpp | 19 ++++ Source/Core/DiscIO/VolumeWad.h | 3 + Source/Core/DiscIO/VolumeWii.cpp | 27 ++++++ Source/Core/DiscIO/VolumeWii.h | 1 + Source/Core/DolphinQt/GameList/GameList.cpp | 4 +- Source/Core/DolphinQt/GameList/GameList.h | 2 +- .../Core/DolphinQt/GameList/GameListModel.cpp | 10 -- .../Core/DolphinQt/GameList/GameListModel.h | 4 - Source/Core/DolphinQt/MainWindow.cpp | 5 +- Source/Core/DolphinQt/MainWindow.h | 2 +- .../Core/DolphinQt/NetPlay/GameListDialog.cpp | 21 ++-- .../Core/DolphinQt/NetPlay/GameListDialog.h | 8 +- .../Core/DolphinQt/NetPlay/NetPlayDialog.cpp | 97 +++++++++++-------- Source/Core/DolphinQt/NetPlay/NetPlayDialog.h | 13 ++- .../DolphinQt/NetPlay/NetPlaySetupDialog.cpp | 12 ++- .../DolphinQt/NetPlay/NetPlaySetupDialog.h | 7 +- Source/Core/UICommon/GameFile.cpp | 65 ++++++++++++- Source/Core/UICommon/GameFile.h | 13 ++- 34 files changed, 502 insertions(+), 148 deletions(-) create mode 100644 Source/Core/Core/SyncIdentifier.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 37a1991cdb..a3f3f4dc93 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -37,6 +37,7 @@ add_library(core PatchEngine.h State.cpp State.h + SyncIdentifier.h SysConf.cpp SysConf.h TitleDatabase.cpp diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj index ceefd749c9..dfe82922d6 100644 --- a/Source/Core/Core/Core.vcxproj +++ b/Source/Core/Core/Core.vcxproj @@ -709,6 +709,7 @@ + diff --git a/Source/Core/Core/Core.vcxproj.filters b/Source/Core/Core/Core.vcxproj.filters index cb729a6f4a..91553ac3af 100644 --- a/Source/Core/Core/Core.vcxproj.filters +++ b/Source/Core/Core/Core.vcxproj.filters @@ -1757,7 +1757,8 @@ HW %28Flipper/Hollywood%29\EXI - Expansion Interface\BBA - + + diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index e6efce1799..4b1c730a7d 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -53,9 +53,11 @@ #include "Core/IOS/Uids.h" #include "Core/Movie.h" #include "Core/PowerPC/PowerPC.h" +#include "Core/SyncIdentifier.h" #include "InputCommon/ControllerEmu/ControlGroup/Attachments.h" #include "InputCommon/GCAdapter.h" #include "InputCommon/InputConfig.h" +#include "UICommon/GameFile.h" #include "VideoCommon/OnScreenDisplay.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 unsigned int NetPlayClient::OnData(sf::Packet& packet) { @@ -572,24 +590,25 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_CHANGE_GAME: { + std::string netplay_name; { std::lock_guard 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 - m_dialog->OnMsgChangeGame(m_selected_game); + m_dialog->OnMsgChangeGame(m_selected_game, netplay_name); sf::Packet game_status_packet; game_status_packet << static_cast(NP_MSG_GAME_STATUS); - PlayerGameStatus status = m_dialog->FindGame(m_selected_game).empty() ? - PlayerGameStatus::NotFound : - PlayerGameStatus::Ok; + SyncIdentifierComparison result; + m_dialog->FindGameFile(m_selected_game, &result); - game_status_packet << static_cast(status); + game_status_packet << static_cast(result); Send(game_status_packet); sf::Packet ipl_status_packet; @@ -609,7 +628,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) Player& player = m_players[pid]; u32 status; packet >> status; - player.game_status = static_cast(status); + player.game_status = static_cast(status); } m_dialog->Update(); @@ -623,7 +642,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) packet >> m_current_game; 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 core; @@ -1172,10 +1191,10 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_COMPUTE_MD5: { - std::string file_identifier; - packet >> file_identifier; + SyncIdentifier sync_identifier; + ReceiveSyncIdentifier(packet, sync_identifier); - ComputeMD5(file_identifier); + ComputeMD5(sync_identifier); } break; @@ -1382,11 +1401,15 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) switch (player.game_status) { - case PlayerGameStatus::Ok: + case SyncIdentifierComparison::SameGame: ss << "ready"; break; - case PlayerGameStatus::NotFound: + case SyncIdentifierComparison::DifferentVersion: + ss << "wrong game version"; + break; + + case SyncIdentifierComparison::DifferentGame: ss << "game missing"; break; @@ -2286,23 +2309,24 @@ bool NetPlayClient::DoAllPlayersHaveGame() { std::lock_guard lkp(m_crit.players); - return std::all_of(std::begin(m_players), std::end(m_players), - [](auto entry) { return entry.second.game_status == PlayerGameStatus::Ok; }); + return std::all_of(std::begin(m_players), std::end(m_players), [](auto entry) { + 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) return; - m_dialog->ShowMD5Dialog(file_identifier); + m_dialog->ShowMD5Dialog(sync_identifier.game_id); m_should_compute_MD5 = true; std::string file; - if (file_identifier == WII_SDCARD) + if (sync_identifier == GetSDCardIdentifier()) file = File::GetUserPath(F_WIISDCARD_IDX); - else - file = m_dialog->FindGame(file_identifier); + else if (auto game = m_dialog->FindGameFile(sync_identifier)) + file = game->GetFilePath(); if (file.empty() || !File::Exists(file)) { @@ -2348,6 +2372,11 @@ void NetPlayClient::AdjustPadBufferSize(const unsigned int size) m_dialog->OnPadBufferChanged(size); } +SyncIdentifier NetPlayClient::GetSDCardIdentifier() +{ + return SyncIdentifier{{}, "sd", {}, {}, {}, {}}; +} + bool IsNetPlayRunning() { return netplay_client != nullptr; diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index 2e6acf4518..6af81e8d60 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -22,6 +22,7 @@ #include "Common/SPSCQueue.h" #include "Common/TraversalClient.h" #include "Core/NetPlayProto.h" +#include "Core/SyncIdentifier.h" #include "InputCommon/GCPadStatus.h" namespace UICommon @@ -42,7 +43,8 @@ public: virtual void Update() = 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 OnMsgStopGame() = 0; virtual void OnMsgPowerButton() = 0; @@ -59,9 +61,10 @@ public: virtual void OnGolferChanged(bool is_golfer, const std::string& golfer_name) = 0; virtual bool IsRecording() = 0; - virtual std::string FindGame(const std::string& game) = 0; - virtual std::shared_ptr FindGameFile(const std::string& game) = 0; - virtual void ShowMD5Dialog(const std::string& file_identifier) = 0; + virtual std::shared_ptr + FindGameFile(const SyncIdentifier& sync_identifier, + SyncIdentifierComparison* found = nullptr) = 0; + virtual void ShowMD5Dialog(const std::string& title) = 0; virtual void SetMD5Progress(int pid, int progress) = 0; virtual void SetMD5Result(int pid, const std::string& result) = 0; virtual void AbortMD5() = 0; @@ -75,13 +78,6 @@ public: virtual void SetChunkedProgress(int pid, u64 progress) = 0; }; -enum class PlayerGameStatus -{ - Unknown, - Ok, - NotFound -}; - class Player { public: @@ -89,7 +85,7 @@ public: std::string name; std::string revision; u32 ping; - PlayerGameStatus game_status; + SyncIdentifierComparison game_status; bool IsHost() const { return pid == 1; } }; @@ -149,6 +145,8 @@ public: void AdjustPadBufferSize(unsigned int size); + static SyncIdentifier GetSDCardIdentifier(); + protected: struct AsyncQueueEntry { @@ -182,7 +180,7 @@ protected: ENetPeer* m_server = nullptr; std::thread m_thread; - std::string m_selected_game; + SyncIdentifier m_selected_game; Common::Flag m_is_running{false}; Common::Flag m_do_loop{true}; @@ -237,7 +235,7 @@ private: void Send(const sf::Packet& packet, u8 channel_id = DEFAULT_CHANNEL); void Disconnect(); bool Connect(); - void ComputeMD5(const std::string& file_identifier); + void ComputeMD5(const SyncIdentifier& sync_identifier); void DisplayPlayersPing(); u32 GetPlayersMaxPing() const; diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index e76d362285..aac046ed44 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -51,6 +51,7 @@ #include "Core/IOS/IOS.h" #include "Core/IOS/Uids.h" #include "Core/NetPlayClient.h" //for NetPlayUI +#include "Core/SyncIdentifier.h" #include "DiscIO/Enums.h" #include "InputCommon/ControllerEmu/ControlGroup/Attachments.h" #include "InputCommon/GCPadStatus.h" @@ -182,7 +183,7 @@ void NetPlayServer::SetupIndex() session.region = Config::Get(Config::NETPLAY_INDEX_REGION); session.has_password = !Config::Get(Config::NETPLAY_INDEX_PASSWORD).empty(); 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(m_players.size()); session.in_game = m_is_running; session.port = GetPort(); @@ -238,7 +239,7 @@ void NetPlayServer::ThreadFunc() SendToClients(spac); m_index.SetPlayerCount(static_cast(m_players.size())); - m_index.SetGame(m_selected_game); + m_index.SetGame(m_selected_game_name); m_index.SetInGame(m_is_running); m_update_pings = false; @@ -348,6 +349,20 @@ void NetPlayServer::ThreadFunc() } } // 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(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 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 new client the selected game - if (!m_selected_game.empty()) + if (!m_selected_game_name.empty()) { spac.clear(); spac << static_cast(NP_MSG_CHANGE_GAME); - spac << m_selected_game; + SendSyncIdentifier(spac, m_selected_game_identifier); + spac << m_selected_game_name; Send(player.socket, spac); } @@ -913,7 +929,7 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player) u32 status; packet >> status; - m_players[player.pid].game_status = static_cast(status); + m_players[player.pid].game_status = static_cast(status); // send msg to other clients sf::Packet spac; @@ -1153,16 +1169,19 @@ void NetPlayServer::SendChatMessage(const std::string& msg) } // 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 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 sf::Packet spac; spac << static_cast(NP_MSG_CHANGE_GAME); - spac << game; + SendSyncIdentifier(spac, m_selected_game_identifier); + spac << m_selected_game_name; SendAsyncToClients(std::move(spac)); @@ -1170,11 +1189,11 @@ bool NetPlayServer::ChangeGame(const std::string& game) } // called from ---GUI--- thread -bool NetPlayServer::ComputeMD5(const std::string& file_identifier) +bool NetPlayServer::ComputeMD5(const SyncIdentifier& sync_identifier) { sf::Packet spac; spac << static_cast(NP_MSG_COMPUTE_MD5); - spac << file_identifier; + SendSyncIdentifier(spac, sync_identifier); SendAsyncToClients(std::move(spac)); @@ -1260,7 +1279,7 @@ bool NetPlayServer::StartGame() const sf::Uint64 initial_rtc = GetInitialNetPlayRTC(); 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 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) { PanicAlertT("Selected game doesn't exist in game list!"); @@ -1618,7 +1637,7 @@ bool NetPlayServer::SyncCodes() m_codes_synced = false; // 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) { PanicAlertT("Selected game doesn't exist in game list!"); diff --git a/Source/Core/Core/NetPlayServer.h b/Source/Core/Core/NetPlayServer.h index 62b20f7744..16034074af 100644 --- a/Source/Core/Core/NetPlayServer.h +++ b/Source/Core/Core/NetPlayServer.h @@ -5,6 +5,7 @@ #pragma once #include + #include #include #include @@ -14,19 +15,20 @@ #include #include #include + #include "Common/Event.h" #include "Common/QoSSession.h" #include "Common/SPSCQueue.h" #include "Common/Timer.h" #include "Common/TraversalClient.h" #include "Core/NetPlayProto.h" +#include "Core/SyncIdentifier.h" #include "InputCommon/GCPadStatus.h" #include "UICommon/NetPlayIndex.h" namespace NetPlay { class NetPlayUI; -enum class PlayerGameStatus; class NetPlayServer : public TraversalClientClient { @@ -43,8 +45,8 @@ public: const NetTraversalConfig& traversal_config); ~NetPlayServer(); - bool ChangeGame(const std::string& game); - bool ComputeMD5(const std::string& file_identifier); + bool ChangeGame(const SyncIdentifier& sync_identifier, const std::string& netplay_name); + bool ComputeMD5(const SyncIdentifier& sync_identifier); bool AbortMD5(); void SendChatMessage(const std::string& msg); @@ -80,7 +82,7 @@ private: PlayerId pid; std::string name; std::string revision; - PlayerGameStatus game_status; + SyncIdentifierComparison game_status; bool has_ipl_dump; ENetPeer* socket; @@ -180,7 +182,8 @@ private: Common::SPSCQueue m_async_queue; Common::SPSCQueue m_chunked_data_queue; - std::string m_selected_game; + SyncIdentifier m_selected_game_identifier; + std::string m_selected_game_name; std::thread m_thread; Common::Event m_chunked_data_event; Common::Event m_chunked_data_complete_event; diff --git a/Source/Core/Core/SyncIdentifier.h b/Source/Core/Core/SyncIdentifier.h new file mode 100644 index 0000000000..19af15bd1b --- /dev/null +++ b/Source/Core/Core/SyncIdentifier.h @@ -0,0 +1,46 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#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 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 diff --git a/Source/Core/DiscIO/DiscExtractor.cpp b/Source/Core/DiscIO/DiscExtractor.cpp index 3c2a67568d..b12b56b5c2 100644 --- a/Source/Core/DiscIO/DiscExtractor.cpp +++ b/Source/Core/DiscIO/DiscExtractor.cpp @@ -245,19 +245,26 @@ bool ExportBI2Data(const Volume& volume, const Partition& partition, return ExportData(volume, partition, 0x440, 0x2000, export_filename); } +std::optional GetApploaderSize(const Volume& volume, const Partition& partition) +{ + constexpr u64 header_size = 0x20; + const std::optional apploader_size = volume.ReadSwapped(0x2440 + 0x14, partition); + const std::optional trailer_size = volume.ReadSwapped(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, const std::string& export_filename) { if (!IsDisc(volume.GetVolumeType())) return false; - std::optional apploader_size = volume.ReadSwapped(0x2440 + 0x14, partition); - const std::optional trailer_size = volume.ReadSwapped(0x2440 + 0x18, partition); - constexpr u32 header_size = 0x20; - if (!apploader_size || !trailer_size) + const std::optional apploader_size = GetApploaderSize(volume, partition); + if (!apploader_size) 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); } diff --git a/Source/Core/DiscIO/DiscExtractor.h b/Source/Core/DiscIO/DiscExtractor.h index 0ca1954ee0..c7e18f83d6 100644 --- a/Source/Core/DiscIO/DiscExtractor.h +++ b/Source/Core/DiscIO/DiscExtractor.h @@ -61,6 +61,7 @@ bool ExportHeader(const Volume& volume, const Partition& partition, const std::string& export_filename); bool ExportBI2Data(const Volume& volume, const Partition& partition, const std::string& export_filename); +std::optional GetApploaderSize(const Volume& volume, const Partition& partition); bool ExportApploader(const Volume& volume, const Partition& partition, const std::string& export_filename); std::optional GetBootDOLOffset(const Volume& volume, const Partition& partition); diff --git a/Source/Core/DiscIO/Volume.cpp b/Source/Core/DiscIO/Volume.cpp index 23df2afc5c..ab4b0e562b 100644 --- a/Source/Core/DiscIO/Volume.cpp +++ b/Source/Core/DiscIO/Volume.cpp @@ -9,12 +9,16 @@ #include #include #include +#include #include #include +#include + #include "Common/CommonTypes.h" #include "Common/StringUtil.h" +#include "Core/IOS/ES/Formats.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" #include "DiscIO/VolumeDisc.h" @@ -28,6 +32,43 @@ const IOS::ES::TicketReader Volume::INVALID_TICKET{}; const IOS::ES::TMDReader Volume::INVALID_TMD{}; const std::vector Volume::INVALID_CERT_CHAIN{}; +template +static void AddToSyncHash(mbedtls_sha1_context* context, const T& data) +{ + static_assert(std::is_trivially_copyable_v); + mbedtls_sha1_update_ret(context, reinterpret_cast(&data), sizeof(data)); +} + +void Volume::ReadAndAddToSyncHash(mbedtls_sha1_context* context, u64 offset, u64 length, + const Partition& partition) const +{ + std::vector 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 Volume::ReadWiiNames(const std::vector& data) { std::map names; diff --git a/Source/Core/DiscIO/Volume.h b/Source/Core/DiscIO/Volume.h index e176c5d72a..e580e4b109 100644 --- a/Source/Core/DiscIO/Volume.h +++ b/Source/Core/DiscIO/Volume.h @@ -12,6 +12,8 @@ #include #include +#include + #include "Common/CommonTypes.h" #include "Common/StringUtil.h" #include "Common/Swap.h" @@ -138,6 +140,13 @@ public: virtual u64 GetRawSize() 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 GetSyncHash() const = 0; + protected: template std::string DecodeString(const char (&data)[N]) const @@ -151,6 +160,10 @@ protected: 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; } static std::map ReadWiiNames(const std::vector& data); diff --git a/Source/Core/DiscIO/VolumeDisc.cpp b/Source/Core/DiscIO/VolumeDisc.cpp index fed82a8067..3ccea4b62f 100644 --- a/Source/Core/DiscIO/VolumeDisc.cpp +++ b/Source/Core/DiscIO/VolumeDisc.cpp @@ -4,11 +4,17 @@ #include "DiscIO/VolumeDisc.h" +#include #include #include +#include + +#include #include "Common/CommonTypes.h" +#include "DiscIO/DiscExtractor.h" #include "DiscIO/Enums.h" +#include "DiscIO/Filesystem.h" namespace DiscIO { @@ -90,4 +96,35 @@ bool VolumeDisc::IsNKit() const return ReadSwapped(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 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 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 file_info = file_system->FindFileInfo("opening.bnr"); + if (file_info && !file_info->IsDirectory()) + ReadAndAddToSyncHash(context, file_info->GetOffset(), file_info->GetSize(), partition); + } +} + } // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeDisc.h b/Source/Core/DiscIO/VolumeDisc.h index 680bbf83b0..e12ec310ae 100644 --- a/Source/Core/DiscIO/VolumeDisc.h +++ b/Source/Core/DiscIO/VolumeDisc.h @@ -7,6 +7,8 @@ #include #include +#include + #include "Common/CommonTypes.h" #include "DiscIO/Volume.h" @@ -26,6 +28,7 @@ public: protected: Region RegionCodeToRegion(std::optional region_code) const; + void AddGamePartitionToSyncHash(mbedtls_sha1_context* context) const; }; } // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeGC.cpp b/Source/Core/DiscIO/VolumeGC.cpp index 15d71c74ca..3489965395 100644 --- a/Source/Core/DiscIO/VolumeGC.cpp +++ b/Source/Core/DiscIO/VolumeGC.cpp @@ -11,6 +11,8 @@ #include #include +#include + #include "Common/Assert.h" #include "Common/ColorUtil.h" #include "Common/CommonTypes.h" @@ -137,6 +139,19 @@ bool VolumeGC::IsDatelDisc() const return !GetBootDOLOffset(*this, PARTITION_NONE).has_value(); } +std::array VolumeGC::GetSyncHash() const +{ + mbedtls_sha1_context context; + mbedtls_sha1_init(&context); + mbedtls_sha1_starts_ret(&context); + + AddGamePartitionToSyncHash(&context); + + std::array hash; + mbedtls_sha1_finish_ret(&context, hash.data()); + return hash; +} + VolumeGC::ConvertedGCBanner VolumeGC::LoadBannerFile() const { GCBanner banner_file; diff --git a/Source/Core/DiscIO/VolumeGC.h b/Source/Core/DiscIO/VolumeGC.h index 56e2356877..f4b2dbc6fc 100644 --- a/Source/Core/DiscIO/VolumeGC.h +++ b/Source/Core/DiscIO/VolumeGC.h @@ -51,6 +51,8 @@ public: u64 GetRawSize() const override; const BlobReader& GetBlobReader() const override; + std::array GetSyncHash() const override; + private: static const u32 GC_BANNER_WIDTH = 96; static const u32 GC_BANNER_HEIGHT = 32; diff --git a/Source/Core/DiscIO/VolumeWad.cpp b/Source/Core/DiscIO/VolumeWad.cpp index b7b8fa35cd..86aebb20b6 100644 --- a/Source/Core/DiscIO/VolumeWad.cpp +++ b/Source/Core/DiscIO/VolumeWad.cpp @@ -40,6 +40,7 @@ VolumeWAD::VolumeWAD(std::unique_ptr reader) : m_reader(std::move(re m_ticket_size = m_reader->ReadSwapped(0x10).value_or(0); m_tmd_size = m_reader->ReadSwapped(0x14).value_or(0); m_data_size = m_reader->ReadSwapped(0x18).value_or(0); + m_opening_bnr_size = m_reader->ReadSwapped(0x1C).value_or(0); m_cert_chain_offset = Common::AlignUp(m_hdr_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; } +std::array 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 hash; + mbedtls_sha1_finish_ret(&context, hash.data()); + return hash; +} + } // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeWad.h b/Source/Core/DiscIO/VolumeWad.h index a4c311da78..8dc0cf39ef 100644 --- a/Source/Core/DiscIO/VolumeWad.h +++ b/Source/Core/DiscIO/VolumeWad.h @@ -70,6 +70,8 @@ public: u64 GetRawSize() const override; const BlobReader& GetBlobReader() const override; + std::array GetSyncHash() const override; + private: std::unique_ptr m_reader; IOS::ES::TicketReader m_ticket; @@ -85,6 +87,7 @@ private: u32 m_ticket_size = 0; u32 m_tmd_size = 0; u32 m_data_size = 0; + u32 m_opening_bnr_size = 0; }; } // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeWii.cpp b/Source/Core/DiscIO/VolumeWii.cpp index 73797fe1e3..f3a9bf5e12 100644 --- a/Source/Core/DiscIO/VolumeWii.cpp +++ b/Source/Core/DiscIO/VolumeWii.cpp @@ -364,6 +364,33 @@ const BlobReader& VolumeWii::GetBlobReader() const return *m_reader; } +std::array 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(&data_offset), sizeof(data_offset)); + + // TMD + AddTMDToSyncHash(&context, GetGamePartition()); + + // Game partition contents + AddGamePartitionToSyncHash(&context); + + std::array hash; + mbedtls_sha1_finish_ret(&context, hash.data()); + return hash; +} + bool VolumeWii::CheckH3TableIntegrity(const Partition& partition) const { auto it = m_partitions.find(partition); diff --git a/Source/Core/DiscIO/VolumeWii.h b/Source/Core/DiscIO/VolumeWii.h index 3af5884d3e..107c8175b4 100644 --- a/Source/Core/DiscIO/VolumeWii.h +++ b/Source/Core/DiscIO/VolumeWii.h @@ -92,6 +92,7 @@ public: bool IsSizeAccurate() const override; u64 GetRawSize() const override; const BlobReader& GetBlobReader() const override; + std::array GetSyncHash() const override; // The in parameter can either contain all the data to begin with, // or read_function can write data into the in parameter when called. diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index 0b1d480375..722bdb6912 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -403,9 +403,7 @@ void GameList::ShowContextMenu(const QPoint&) QAction* netplay_host = new QAction(tr("Host with NetPlay"), menu); - connect(netplay_host, &QAction::triggered, [this, game] { - emit NetPlayHost(QString::fromStdString(game->GetUniqueIdentifier())); - }); + connect(netplay_host, &QAction::triggered, [this, game] { emit NetPlayHost(*game); }); connect(&Settings::Instance(), &Settings::EmulationStateChanged, menu, [=](Core::State state) { netplay_host->setEnabled(state == Core::State::Uninitialized); diff --git a/Source/Core/DolphinQt/GameList/GameList.h b/Source/Core/DolphinQt/GameList/GameList.h index f64ddd61f0..429f8796d7 100644 --- a/Source/Core/DolphinQt/GameList/GameList.h +++ b/Source/Core/DolphinQt/GameList/GameList.h @@ -47,7 +47,7 @@ public: signals: void GameSelected(); - void NetPlayHost(const QString& game_id); + void NetPlayHost(const UICommon::GameFile& game); void SelectionChanged(std::shared_ptr game_file); void OpenGeneralSettings(); diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index 99cd631e92..c184c2ef60 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -313,16 +313,6 @@ std::shared_ptr GameListModel::GetGameFile(int index) return m_games[index]; } -QString GameListModel::GetPath(int index) const -{ - return QString::fromStdString(m_games[index]->GetFilePath()); -} - -QString GameListModel::GetUniqueIdentifier(int index) const -{ - return QString::fromStdString(m_games[index]->GetUniqueIdentifier()); -} - void GameListModel::AddGame(const std::shared_ptr& game) { beginInsertRows(QModelIndex(), m_games.size(), m_games.size()); diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h index 28e116c64f..a476ebae82 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.h +++ b/Source/Core/DolphinQt/GameList/GameListModel.h @@ -37,10 +37,6 @@ public: int columnCount(const QModelIndex& parent) const override; std::shared_ptr GetGameFile(int index) const; - // Path of the game at the specified index. - QString GetPath(int index) const; - // Unique identifier of the game at the specified index. - QString GetUniqueIdentifier(int index) const; bool ShouldDisplayGameListItem(int index) const; void SetSearchTerm(const QString& term); diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index bff3a6422c..e579db3dea 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -1374,7 +1374,7 @@ bool MainWindow::NetPlayJoin() return true; } -bool MainWindow::NetPlayHost(const QString& game_id) +bool MainWindow::NetPlayHost(const UICommon::GameFile& game) { if (Core::IsRunning()) { @@ -1419,7 +1419,8 @@ bool MainWindow::NetPlayHost(const QString& game_id) return false; } - Settings::Instance().GetNetPlayServer()->ChangeGame(game_id.toStdString()); + Settings::Instance().GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(), + game.GetNetPlayName()); // Join our local server return NetPlayJoin(); diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index 63141ec2d3..8c15941551 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -157,7 +157,7 @@ private: void NetPlayInit(); bool NetPlayJoin(); - bool NetPlayHost(const QString& game_id); + bool NetPlayHost(const UICommon::GameFile& game); void NetPlayQuit(); void OnBootGameCubeIPL(DiscIO::Region region); diff --git a/Source/Core/DolphinQt/NetPlay/GameListDialog.cpp b/Source/Core/DolphinQt/NetPlay/GameListDialog.cpp index 9fa14e3b30..4ada80cc73 100644 --- a/Source/Core/DolphinQt/NetPlay/GameListDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/GameListDialog.cpp @@ -4,12 +4,15 @@ #include "DolphinQt/NetPlay/GameListDialog.h" +#include + #include #include #include #include "DolphinQt/GameList/GameListModel.h" #include "DolphinQt/Settings.h" +#include "UICommon/GameFile.h" GameListDialog::GameListDialog(QWidget* parent) : QDialog(parent) { @@ -35,12 +38,8 @@ void GameListDialog::CreateWidgets() void GameListDialog::ConnectWidgets() { - connect(m_game_list, &QListWidget::itemSelectionChanged, [this] { - int row = m_game_list->currentRow(); - - m_button_box->setEnabled(row != -1); - m_game_id = m_game_list->currentItem()->text(); - }); + connect(m_game_list, &QListWidget::itemSelectionChanged, + [this] { m_button_box->setEnabled(m_game_list->currentRow() != -1); }); connect(m_game_list, &QListWidget::itemDoubleClicked, this, &GameListDialog::accept); connect(m_button_box, &QDialogButtonBox::accepted, this, &GameListDialog::accept); @@ -54,16 +53,20 @@ void GameListDialog::PopulateGameList() for (int i = 0; i < game_list_model->rowCount(QModelIndex()); i++) { - auto* item = new QListWidgetItem(game_list_model->GetUniqueIdentifier(i)); + std::shared_ptr game = game_list_model->GetGameFile(i); + + auto* item = new QListWidgetItem(QString::fromStdString(game->GetNetPlayName())); + item->setData(Qt::UserRole, QVariant::fromValue(std::move(game))); m_game_list->addItem(item); } 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>(); } int GameListDialog::exec() diff --git a/Source/Core/DolphinQt/NetPlay/GameListDialog.h b/Source/Core/DolphinQt/NetPlay/GameListDialog.h index da04bd4996..78e5f90ff3 100644 --- a/Source/Core/DolphinQt/NetPlay/GameListDialog.h +++ b/Source/Core/DolphinQt/NetPlay/GameListDialog.h @@ -11,6 +11,11 @@ class QVBoxLayout; class QListWidget; class QDialogButtonBox; +namespace UICommon +{ +class GameFile; +} + class GameListDialog : public QDialog { Q_OBJECT @@ -18,7 +23,7 @@ public: explicit GameListDialog(QWidget* parent); int exec() override; - const QString& GetSelectedUniqueID() const; + const UICommon::GameFile& GetSelectedGame() const; private: void CreateWidgets(); @@ -28,5 +33,4 @@ private: QVBoxLayout* m_main_layout; QListWidget* m_game_list; QDialogButtonBox* m_button_box; - QString m_game_id; }; diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index 77ceb45523..82d2f0b141 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include "Common/CommonPaths.h" @@ -37,6 +38,7 @@ #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/NetPlayServer.h" +#include "Core/SyncIdentifier.h" #include "DolphinQt/GameList/GameListModel.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->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] { GameListDialog gld(this); if (gld.exec() != QDialog::Accepted) 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_record_input_action = m_other_menu->addAction(tr("Record Inputs")); @@ -321,9 +325,11 @@ void NetPlayDialog::ConnectWidgets() GameListDialog gld(this); if (gld.exec() == QDialog::Accepted) { - auto unique_id = gld.GetSelectedUniqueID(); - 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 = game.GetNetPlayName(); + Settings::Instance().GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(), netplay_name); + Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"), + QString::fromStdString(netplay_name)); } }); @@ -416,7 +422,7 @@ void NetPlayDialog::OnStart() return; } - const auto game = FindGameFile(m_current_game); + const auto game = FindGameFile(m_current_game_identifier); if (!game) { PanicAlertT("Selected game doesn't exist in game list!"); @@ -583,11 +589,12 @@ void NetPlayDialog::UpdateDiscordPresence() { #ifdef USE_DISCORD_PRESENCE // 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; 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()) @@ -602,7 +609,8 @@ void NetPlayDialog::UpdateDiscordPresence() return use_default(); 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 { @@ -612,7 +620,7 @@ void NetPlayDialog::UpdateDiscordPresence() Discord::UpdateDiscordPresence( 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 @@ -660,9 +668,10 @@ void NetPlayDialog::UpdateGUI() return '|' + str + '|'; }; - static const std::map player_status{ - {NetPlay::PlayerGameStatus::Ok, tr("OK")}, - {NetPlay::PlayerGameStatus::NotFound, tr("Not Found")}, + static const std::map player_status{ + {NetPlay::SyncIdentifierComparison::SameGame, tr("OK")}, + {NetPlay::SyncIdentifierComparison::DifferentVersion, tr("Wrong Version")}, + {NetPlay::SyncIdentifierComparison::DifferentGame, tr("Not Found")}, }; for (int i = 0; i < m_player_count; i++) @@ -805,15 +814,17 @@ void NetPlayDialog::AppendChat(const std::string& msg) 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); - QueueOnObject(this, [this, qtitle, title] { - m_game_button->setText(qtitle); - m_current_game = title; + QString qname = QString::fromStdString(netplay_name); + QueueOnObject(this, [this, qname, netplay_name, &sync_identifier] { + m_game_button->setText(qname); + m_current_game_identifier = sync_identifier; + m_current_game_name = netplay_name; UpdateDiscordPresence(); }); - DisplayMessage(tr("Game changed to \"%1\"").arg(qtitle), "magenta"); + DisplayMessage(tr("Game changed to \"%1\"").arg(qname), "magenta"); } void NetPlayDialog::GameStatusChanged(bool running) @@ -859,7 +870,12 @@ void NetPlayDialog::OnMsgStartGame() auto client = Settings::Instance().GetNetPlayClient(); 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(); }); } @@ -1017,29 +1033,24 @@ bool NetPlayDialog::IsRecording() return false; } -std::string NetPlayDialog::FindGame(const std::string& game) +std::shared_ptr +NetPlayDialog::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, + NetPlay::SyncIdentifierComparison* found) { - std::optional path = RunOnObject(this, [this, &game] { - for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) - { - if (m_game_list_model->GetUniqueIdentifier(i).toStdString() == game) - return m_game_list_model->GetPath(i).toStdString(); - } - return std::string(""); - }); - if (path) - return *path; - return std::string(""); -} + NetPlay::SyncIdentifierComparison temp; + if (!found) + found = &temp; + + *found = NetPlay::SyncIdentifierComparison::DifferentGame; -std::shared_ptr NetPlayDialog::FindGameFile(const std::string& game) -{ std::optional> game_file = - RunOnObject(this, [this, &game] { + RunOnObject(this, [this, &sync_identifier, found] { 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); + auto game_file = 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>(nullptr); }); @@ -1126,15 +1137,15 @@ void NetPlayDialog::SaveSettings() 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); if (m_md5_dialog->isVisible()) m_md5_dialog->close(); - m_md5_dialog->show(QString::fromStdString(file_identifier)); + m_md5_dialog->show(QString::fromStdString(title)); }); } diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h index dd0774174e..7459be4f16 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h @@ -45,7 +45,8 @@ public: void Update() 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 OnMsgStopGame() override; void OnMsgPowerButton() override; @@ -65,13 +66,14 @@ public: void OnIndexRefreshFailed(const std::string error) override; bool IsRecording() override; - std::string FindGame(const std::string& game) override; - std::shared_ptr FindGameFile(const std::string& game) override; + std::shared_ptr + FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, + NetPlay::SyncIdentifierComparison* found = nullptr) override; void LoadSettings(); 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 SetMD5Result(int pid, const std::string& result) override; void AbortMD5() override; @@ -145,7 +147,8 @@ private: MD5Dialog* m_md5_dialog; ChunkedProgressDialog* m_chunked_progress_dialog; PadMappingDialog* m_pad_mapping; - std::string m_current_game; + NetPlay::SyncIdentifier m_current_game_identifier; + std::string m_current_game_name; Common::Lazy m_external_ip_address; std::string m_nickname; GameListModel* m_game_list_model = nullptr; diff --git a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp index a9e31f5ed4..5f51f4a7cd 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp @@ -4,6 +4,8 @@ #include "DolphinQt/NetPlay/NetPlaySetupDialog.h" +#include + #include #include #include @@ -24,6 +26,7 @@ #include "DolphinQt/QtUtils/UTF8CodePointCountValidator.h" #include "DolphinQt/Settings.h" +#include "UICommon/GameFile.h" #include "UICommon/NetPlayIndex.h" NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent) @@ -347,7 +350,7 @@ void NetPlaySetupDialog::accept() return; } - emit Host(items[0]->text()); + emit Host(*items[0]->data(Qt::UserRole).value>()); } } @@ -358,11 +361,10 @@ void NetPlaySetupDialog::PopulateGameList() m_host_games->clear(); for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++) { - auto title = m_game_list_model->GetUniqueIdentifier(i); - auto path = m_game_list_model->GetPath(i); + std::shared_ptr game = m_game_list_model->GetGameFile(i); - auto* item = new QListWidgetItem(title); - item->setData(Qt::UserRole, path); + auto* item = new QListWidgetItem(QString::fromStdString(game->GetNetPlayName())); + item->setData(Qt::UserRole, QVariant::fromValue(std::move(game))); m_host_games->addItem(item); } diff --git a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h index ec0d4b3c5d..a2ff67264d 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h @@ -18,6 +18,11 @@ class QPushButton; class QSpinBox; class QTabWidget; +namespace UICommon +{ +class GameFile; +} + class NetPlaySetupDialog : public QDialog { Q_OBJECT @@ -29,7 +34,7 @@ public: signals: bool Join(); - bool Host(const QString& game_identifier); + bool Host(const UICommon::GameFile& game); private: void CreateMainLayout(); diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index b28d54e944..bd9f621f28 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -5,6 +5,7 @@ #include "UICommon/GameFile.h" #include +#include #include #include #include @@ -13,11 +14,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include "Common/ChunkFile.h" @@ -532,7 +535,7 @@ std::vector GameFile::GetLanguages() const return languages; } -std::string GameFile::GetUniqueIdentifier() const +std::string GameFile::GetNetPlayName() const { std::vector info; if (!GetGameID().empty()) @@ -566,6 +569,66 @@ std::string GameFile::GetUniqueIdentifier() const return name + " (" + ss.str() + ")"; } +std::array GameFile::GetSyncHash() const +{ + std::array hash{}; + + if (m_platform == DiscIO::Platform::ELFOrDOL) + { + std::string buffer; + if (File::ReadFileToString(m_file_path, buffer)) + mbedtls_sha1_ret(reinterpret_cast(buffer.data()), buffer.size(), hash.data()); + } + else + { + if (std::unique_ptr 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 { ASSERT(DiscIO::IsWii(m_platform)); diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h index dcf33ee677..68ad21dec6 100644 --- a/Source/Core/UICommon/GameFile.h +++ b/Source/Core/UICommon/GameFile.h @@ -4,11 +4,13 @@ #pragma once +#include #include #include #include #include "Common/CommonTypes.h" +#include "Core/SyncIdentifier.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" @@ -80,7 +82,16 @@ public: u16 GetRevision() const { return m_revision; } // 0 is the first disc, 1 is the second disc u8 GetDiscNumber() const { return m_disc_number; } - std::string GetUniqueIdentifier() const; + std::string GetNetPlayName() const; + + // This function is slow + std::array 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; DiscIO::Region GetRegion() const { return m_region; } DiscIO::Country GetCountry() const { return m_country; }