diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 2d538c88e..3297ca123 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 // TODO: Don't poll when booting the game, e.g. Crash Warped freaks out. @@ -129,6 +129,8 @@ struct AchievementProgressIndicator } // namespace +static TinyString GameHashToString(const GameHash& hash); + static void ReportError(std::string_view sv); template static void ReportFmtError(fmt::format_string fmt, T&&... args); @@ -136,7 +138,6 @@ template static void ReportRCError(int err, fmt::format_string fmt, T&&... args); static void ClearGameInfo(); static void ClearGameHash(); -static std::string GetGameHash(CDImage* image); static bool TryLoggingInWithToken(); static void SetHardcoreMode(bool enabled, bool force_display_message); static bool IsLoggedInOrLoggingIn(); @@ -229,10 +230,10 @@ struct State std::unique_ptr http_downloader; std::string game_path; - std::string game_hash; std::string game_title; std::string game_icon; std::string game_icon_url; + std::optional game_hash; rc_client_async_handle_t* login_request = nullptr; rc_client_async_handle_t* load_game_request = nullptr; @@ -253,6 +254,14 @@ ALIGN_TO_CACHE_LINE static State s_state; } // namespace Achievements +TinyString Achievements::GameHashToString(const GameHash& hash) +{ + return TinyString::from_format( + "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", hash[0], + hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], hash[8], hash[9], hash[10], hash[11], hash[12], + hash[13], hash[14], hash[15]); +} + std::unique_lock Achievements::GetLock() { return std::unique_lock(s_state.mutex); @@ -292,44 +301,46 @@ void Achievements::ReportRCError(int err, fmt::format_string fmt, T&&... a ReportError(str); } -std::string Achievements::GetGameHash(CDImage* image) +std::optional Achievements::GetGameHash(CDImage* image, u32* bytes_hashed) { + std::optional ret; + std::string executable_name; std::vector executable_data; if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data)) - return {}; + return ret; + + return GetGameHash(executable_name, executable_data, bytes_hashed); +} - BIOS::PSEXEHeader header = {}; - if (executable_data.size() >= sizeof(header)) - std::memcpy(&header, executable_data.data(), sizeof(header)); - if (!BIOS::IsValidPSExeHeader(header, executable_data.size())) +std::optional Achievements::GetGameHash(const std::string_view executable_name, + std::span executable_data, + u32* bytes_hashed /* = nullptr */) +{ + std::optional ret; + + // NOTE: Assumes executable_data is aligned to 4 bytes at least.. it should be. + const BIOS::PSEXEHeader* header = reinterpret_cast(executable_data.data()); + if (executable_data.size() < sizeof(BIOS::PSEXEHeader) || !BIOS::IsValidPSExeHeader(*header, executable_data.size())) { ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size()); - return {}; + return ret; } - // This is absolutely bonkers silly. Someone decided to hash the file size specified in the executable, plus 2048, - // instead of adding the size of the header. It _should_ be "header.file_size + sizeof(header)". But we have to hack - // around it because who knows how many games are affected by this. - // https://github.com/RetroAchievements/rcheevos/blob/b8dd5747a4ed38f556fd776e6f41b131ea16178f/src/rhash/hash.c#L2824 - const u32 hash_size = std::min(header.file_size + 2048, static_cast(executable_data.size())); + const u32 hash_size = std::min(header->file_size + 2048, static_cast(executable_data.size())); MD5Digest digest; - digest.Update(executable_name.c_str(), static_cast(executable_name.size())); + digest.Update(executable_name.data(), static_cast(executable_name.size())); if (hash_size > 0) digest.Update(executable_data.data(), hash_size); - u8 hash[16]; - digest.Final(hash); + ret = GameHash(); + digest.Final(ret.value()); - const std::string hash_str = - fmt::format("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", - hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], hash[8], hash[9], hash[10], - hash[11], hash[12], hash[13], hash[14], hash[15]); + if (bytes_hashed) + *bytes_hashed = hash_size; - INFO_LOG("Hash for '{}' ({} bytes, {} bytes hashed): {}", executable_name, executable_data.size(), hash_size, - hash_str); - return hash_str; + return ret; } std::string Achievements::GetLocalImagePath(const std::string_view image_name, int type) @@ -1029,9 +1040,14 @@ void Achievements::IdentifyGame(const std::string& path, CDImage* image) ERROR_LOG("Failed to open temporary CD image '{}'", path); } - std::string game_hash; + std::optional game_hash; if (image) - game_hash = GetGameHash(image); + { + u32 bytes_hashed; + game_hash = GetGameHash(image, &bytes_hashed); + if (game_hash.has_value()) + INFO_LOG("RA Hash: {} ({} bytes hashed)", GameHashToString(game_hash.value()), bytes_hashed); + } if (s_state.game_hash == game_hash) { @@ -1074,7 +1090,7 @@ void Achievements::BeginLoadGame() { ClearGameInfo(); - if (s_state.game_hash.empty()) + if (!s_state.game_hash.has_value()) { // when we're booting the bios, this will fail if (!s_state.game_path.empty()) @@ -1090,8 +1106,8 @@ void Achievements::BeginLoadGame() return; } - s_state.load_game_request = - rc_client_begin_load_game(s_state.client, s_state.game_hash.c_str(), ClientLoadGameCallback, nullptr); + s_state.load_game_request = rc_client_begin_load_game(s_state.client, GameHashToString(s_state.game_hash.value()), + ClientLoadGameCallback, nullptr); } void Achievements::BeginChangeDisc() @@ -1103,7 +1119,7 @@ void Achievements::BeginChangeDisc() s_state.load_game_request = nullptr; } - if (s_state.game_hash.empty()) + if (!s_state.game_hash.has_value()) { // when we're booting the bios, this will fail if (!s_state.game_path.empty()) @@ -1121,8 +1137,8 @@ void Achievements::BeginChangeDisc() } s_state.load_game_request = - rc_client_begin_change_media_from_hash(s_state.client, s_state.game_hash.c_str(), ClientLoadGameCallback, - reinterpret_cast(static_cast(1))); + rc_client_begin_change_media_from_hash(s_state.client, GameHashToString(s_state.game_hash.value()), + ClientLoadGameCallback, reinterpret_cast(static_cast(1))); } void Achievements::ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata) @@ -1134,7 +1150,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, if (result == RC_NO_GAME_LOADED) { // Unknown game. - INFO_LOG("Unknown game '{}', disabling achievements.", s_state.game_hash); + INFO_LOG("Unknown game '{}', disabling achievements.", GameHashToString(s_state.game_hash.value())); if (was_disc_change) { ClearGameInfo(); @@ -1259,7 +1275,7 @@ void Achievements::ClearGameInfo() void Achievements::ClearGameHash() { s_state.game_path = {}; - std::string().swap(s_state.game_hash); + s_state.game_hash.reset(); } void Achievements::DisplayAchievementSummary() @@ -3743,7 +3759,7 @@ void Achievements::RAIntegration::MainWindowChanged(void* new_handle) void Achievements::RAIntegration::GameChanged() { - s_state.game_id = s_state.game_hash.empty() ? 0 : RA_IdentifyHash(s_state.game_hash.c_str()); + s_state.game_id = s_state.game_hash.has_value() ? RA_IdentifyHash(GameHashToString(s_state.game_hash.value())) : 0; RA_ActivateGame(s_state.game_id); } diff --git a/src/core/achievements.h b/src/core/achievements.h index cbf93de2c..6dcb0b71b 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -8,12 +8,11 @@ #include #include +#include #include #include #include -struct rc_client_t; - class Error; class StateWrapper; class CDImage; @@ -28,11 +27,16 @@ enum class LoginRequestReason TokenInvalid, }; +static constexpr size_t GAME_HASH_LENGTH = 16; +using GameHash = std::array; + /// Acquires the achievements lock. Must be held when accessing any achievement state from another thread. std::unique_lock GetLock(); -/// Returns the rc_client instance. Should have the lock held. -rc_client_t* GetClient(); +/// Returns the achievements game hash for a given disc. +std::optional GetGameHash(CDImage* image, u32* bytes_hashed = nullptr); +std::optional GetGameHash(const std::string_view executable_name, std::span executable_data, + u32* bytes_hashed = nullptr); /// Initializes the RetroAchievments client. bool Initialize(); diff --git a/src/core/system.cpp b/src/core/system.cpp index 53cee6cd1..0721d823b 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -865,27 +865,23 @@ std::string System::GetGameHashId(GameHash hash) return fmt::format("HASH-{:X}", hash); } -bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash* out_hash) +bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash* out_hash, + std::string* out_executable_name, std::vector* out_executable_data) { IsoReader iso; - if (!iso.Open(cdi, 1)) - { - if (out_id) - out_id->clear(); - if (out_hash) - *out_hash = 0; - return false; - } - std::string id; std::string exe_name; std::vector exe_buffer; - if (!ReadExecutableFromImage(iso, &exe_name, &exe_buffer)) + if (!iso.Open(cdi, 1) || !ReadExecutableFromImage(iso, &exe_name, &exe_buffer)) { if (out_id) out_id->clear(); if (out_hash) *out_hash = 0; + if (out_executable_name) + out_executable_name->clear(); + if (out_executable_data) + out_executable_data->clear(); return false; } @@ -931,6 +927,11 @@ bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash if (out_hash) *out_hash = hash; + if (out_executable_name) + *out_executable_name = std::move(exe_name); + if (out_executable_data) + *out_executable_data = std::move(exe_buffer); + return true; } diff --git a/src/core/system.h b/src/core/system.h index 4207c8d57..35a2f4162 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -140,7 +140,9 @@ std::string GetExecutableNameForImage(CDImage* cdi, bool strip_subdirectories); bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector* out_executable_data); std::string GetGameHashId(GameHash hash); -bool GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash* out_hash); +bool GetGameDetailsFromImage(CDImage* cdi, std::string* out_id = nullptr, GameHash* out_hash = nullptr, + std::string* out_executable_name = nullptr, + std::vector* out_executable_data = nullptr); GameHash GetGameHashFromFile(const char* path); GameHash GetGameHashFromBuffer(const std::string_view filename, const std::span data); DiscRegion GetRegionForSerial(const std::string_view serial);