Achievements: Don't store hash as a string

This is needed to store achievement metadata in the game list.
This commit is contained in:
Stenzek 2025-01-25 15:35:58 +10:00
parent 1bfc4b6e6c
commit 6a09d6ecda
No known key found for this signature in database
4 changed files with 76 additions and 53 deletions

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> // SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0 // SPDX-License-Identifier: CC-BY-NC-ND-4.0
// TODO: Don't poll when booting the game, e.g. Crash Warped freaks out. // TODO: Don't poll when booting the game, e.g. Crash Warped freaks out.
@ -129,6 +129,8 @@ struct AchievementProgressIndicator
} // namespace } // namespace
static TinyString GameHashToString(const GameHash& hash);
static void ReportError(std::string_view sv); static void ReportError(std::string_view sv);
template<typename... T> template<typename... T>
static void ReportFmtError(fmt::format_string<T...> fmt, T&&... args); static void ReportFmtError(fmt::format_string<T...> fmt, T&&... args);
@ -136,7 +138,6 @@ template<typename... T>
static void ReportRCError(int err, fmt::format_string<T...> fmt, T&&... args); static void ReportRCError(int err, fmt::format_string<T...> fmt, T&&... args);
static void ClearGameInfo(); static void ClearGameInfo();
static void ClearGameHash(); static void ClearGameHash();
static std::string GetGameHash(CDImage* image);
static bool TryLoggingInWithToken(); static bool TryLoggingInWithToken();
static void SetHardcoreMode(bool enabled, bool force_display_message); static void SetHardcoreMode(bool enabled, bool force_display_message);
static bool IsLoggedInOrLoggingIn(); static bool IsLoggedInOrLoggingIn();
@ -229,10 +230,10 @@ struct State
std::unique_ptr<HTTPDownloader> http_downloader; std::unique_ptr<HTTPDownloader> http_downloader;
std::string game_path; std::string game_path;
std::string game_hash;
std::string game_title; std::string game_title;
std::string game_icon; std::string game_icon;
std::string game_icon_url; std::string game_icon_url;
std::optional<GameHash> game_hash;
rc_client_async_handle_t* login_request = nullptr; rc_client_async_handle_t* login_request = nullptr;
rc_client_async_handle_t* load_game_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 } // 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<std::recursive_mutex> Achievements::GetLock() std::unique_lock<std::recursive_mutex> Achievements::GetLock()
{ {
return std::unique_lock(s_state.mutex); return std::unique_lock(s_state.mutex);
@ -292,44 +301,46 @@ void Achievements::ReportRCError(int err, fmt::format_string<T...> fmt, T&&... a
ReportError(str); ReportError(str);
} }
std::string Achievements::GetGameHash(CDImage* image) std::optional<Achievements::GameHash> Achievements::GetGameHash(CDImage* image, u32* bytes_hashed)
{ {
std::optional<GameHash> ret;
std::string executable_name; std::string executable_name;
std::vector<u8> executable_data; std::vector<u8> executable_data;
if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data)) if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data))
return {}; return ret;
BIOS::PSEXEHeader header = {}; return GetGameHash(executable_name, executable_data, bytes_hashed);
if (executable_data.size() >= sizeof(header))
std::memcpy(&header, executable_data.data(), sizeof(header));
if (!BIOS::IsValidPSExeHeader(header, executable_data.size()))
{
ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size());
return {};
} }
// This is absolutely bonkers silly. Someone decided to hash the file size specified in the executable, plus 2048, std::optional<Achievements::GameHash> Achievements::GetGameHash(const std::string_view executable_name,
// instead of adding the size of the header. It _should_ be "header.file_size + sizeof(header)". But we have to hack std::span<const u8> executable_data,
// around it because who knows how many games are affected by this. u32* bytes_hashed /* = nullptr */)
// https://github.com/RetroAchievements/rcheevos/blob/b8dd5747a4ed38f556fd776e6f41b131ea16178f/src/rhash/hash.c#L2824 {
const u32 hash_size = std::min(header.file_size + 2048, static_cast<u32>(executable_data.size())); std::optional<GameHash> ret;
// NOTE: Assumes executable_data is aligned to 4 bytes at least.. it should be.
const BIOS::PSEXEHeader* header = reinterpret_cast<const BIOS::PSEXEHeader*>(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 ret;
}
const u32 hash_size = std::min(header->file_size + 2048, static_cast<u32>(executable_data.size()));
MD5Digest digest; MD5Digest digest;
digest.Update(executable_name.c_str(), static_cast<u32>(executable_name.size())); digest.Update(executable_name.data(), static_cast<u32>(executable_name.size()));
if (hash_size > 0) if (hash_size > 0)
digest.Update(executable_data.data(), hash_size); digest.Update(executable_data.data(), hash_size);
u8 hash[16]; ret = GameHash();
digest.Final(hash); digest.Final(ret.value());
const std::string hash_str = if (bytes_hashed)
fmt::format("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", *bytes_hashed = hash_size;
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]);
INFO_LOG("Hash for '{}' ({} bytes, {} bytes hashed): {}", executable_name, executable_data.size(), hash_size, return ret;
hash_str);
return hash_str;
} }
std::string Achievements::GetLocalImagePath(const std::string_view image_name, int type) 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); ERROR_LOG("Failed to open temporary CD image '{}'", path);
} }
std::string game_hash; std::optional<GameHash> game_hash;
if (image) 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) if (s_state.game_hash == game_hash)
{ {
@ -1074,7 +1090,7 @@ void Achievements::BeginLoadGame()
{ {
ClearGameInfo(); ClearGameInfo();
if (s_state.game_hash.empty()) if (!s_state.game_hash.has_value())
{ {
// when we're booting the bios, this will fail // when we're booting the bios, this will fail
if (!s_state.game_path.empty()) if (!s_state.game_path.empty())
@ -1090,8 +1106,8 @@ void Achievements::BeginLoadGame()
return; return;
} }
s_state.load_game_request = s_state.load_game_request = rc_client_begin_load_game(s_state.client, GameHashToString(s_state.game_hash.value()),
rc_client_begin_load_game(s_state.client, s_state.game_hash.c_str(), ClientLoadGameCallback, nullptr); ClientLoadGameCallback, nullptr);
} }
void Achievements::BeginChangeDisc() void Achievements::BeginChangeDisc()
@ -1103,7 +1119,7 @@ void Achievements::BeginChangeDisc()
s_state.load_game_request = nullptr; 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 // when we're booting the bios, this will fail
if (!s_state.game_path.empty()) if (!s_state.game_path.empty())
@ -1121,8 +1137,8 @@ void Achievements::BeginChangeDisc()
} }
s_state.load_game_request = s_state.load_game_request =
rc_client_begin_change_media_from_hash(s_state.client, s_state.game_hash.c_str(), ClientLoadGameCallback, rc_client_begin_change_media_from_hash(s_state.client, GameHashToString(s_state.game_hash.value()),
reinterpret_cast<void*>(static_cast<uintptr_t>(1))); ClientLoadGameCallback, reinterpret_cast<void*>(static_cast<uintptr_t>(1)));
} }
void Achievements::ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata) 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) if (result == RC_NO_GAME_LOADED)
{ {
// Unknown game. // 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) if (was_disc_change)
{ {
ClearGameInfo(); ClearGameInfo();
@ -1259,7 +1275,7 @@ void Achievements::ClearGameInfo()
void Achievements::ClearGameHash() void Achievements::ClearGameHash()
{ {
s_state.game_path = {}; s_state.game_path = {};
std::string().swap(s_state.game_hash); s_state.game_hash.reset();
} }
void Achievements::DisplayAchievementSummary() void Achievements::DisplayAchievementSummary()
@ -3743,7 +3759,7 @@ void Achievements::RAIntegration::MainWindowChanged(void* new_handle)
void Achievements::RAIntegration::GameChanged() 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); RA_ActivateGame(s_state.game_id);
} }

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> // SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0 // SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once #pragma once
@ -8,12 +8,11 @@
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <span>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
struct rc_client_t;
class Error; class Error;
class StateWrapper; class StateWrapper;
class CDImage; class CDImage;
@ -28,11 +27,16 @@ enum class LoginRequestReason
TokenInvalid, TokenInvalid,
}; };
static constexpr size_t GAME_HASH_LENGTH = 16;
using GameHash = std::array<u8, GAME_HASH_LENGTH>;
/// Acquires the achievements lock. Must be held when accessing any achievement state from another thread. /// Acquires the achievements lock. Must be held when accessing any achievement state from another thread.
std::unique_lock<std::recursive_mutex> GetLock(); std::unique_lock<std::recursive_mutex> GetLock();
/// Returns the rc_client instance. Should have the lock held. /// Returns the achievements game hash for a given disc.
rc_client_t* GetClient(); std::optional<GameHash> GetGameHash(CDImage* image, u32* bytes_hashed = nullptr);
std::optional<GameHash> GetGameHash(const std::string_view executable_name, std::span<const u8> executable_data,
u32* bytes_hashed = nullptr);
/// Initializes the RetroAchievments client. /// Initializes the RetroAchievments client.
bool Initialize(); bool Initialize();

View File

@ -865,27 +865,23 @@ std::string System::GetGameHashId(GameHash hash)
return fmt::format("HASH-{:X}", 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<u8>* out_executable_data)
{ {
IsoReader iso; 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 id;
std::string exe_name; std::string exe_name;
std::vector<u8> exe_buffer; std::vector<u8> exe_buffer;
if (!ReadExecutableFromImage(iso, &exe_name, &exe_buffer)) if (!iso.Open(cdi, 1) || !ReadExecutableFromImage(iso, &exe_name, &exe_buffer))
{ {
if (out_id) if (out_id)
out_id->clear(); out_id->clear();
if (out_hash) if (out_hash)
*out_hash = 0; *out_hash = 0;
if (out_executable_name)
out_executable_name->clear();
if (out_executable_data)
out_executable_data->clear();
return false; return false;
} }
@ -931,6 +927,11 @@ bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash
if (out_hash) if (out_hash)
*out_hash = 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; return true;
} }

View File

@ -140,7 +140,9 @@ std::string GetExecutableNameForImage(CDImage* cdi, bool strip_subdirectories);
bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector<u8>* out_executable_data); bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector<u8>* out_executable_data);
std::string GetGameHashId(GameHash hash); 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<u8>* out_executable_data = nullptr);
GameHash GetGameHashFromFile(const char* path); GameHash GetGameHashFromFile(const char* path);
GameHash GetGameHashFromBuffer(const std::string_view filename, const std::span<const u8> data); GameHash GetGameHashFromBuffer(const std::string_view filename, const std::span<const u8> data);
DiscRegion GetRegionForSerial(const std::string_view serial); DiscRegion GetRegionForSerial(const std::string_view serial);