Achievements: Don't store hash as a string

This is needed to store achievement metadata in the game list.
pull/3360/head
Stenzek 1 month ago
parent 1bfc4b6e6c
commit 6a09d6ecda
No known key found for this signature in database

@ -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
// 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<typename... T>
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 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<HTTPDownloader> 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<GameHash> 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<std::recursive_mutex> Achievements::GetLock()
{
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);
}
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::vector<u8> 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::GameHash> Achievements::GetGameHash(const std::string_view executable_name,
std::span<const u8> executable_data,
u32* bytes_hashed /* = nullptr */)
{
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 {};
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<u32>(executable_data.size()));
const u32 hash_size = std::min(header->file_size + 2048, static_cast<u32>(executable_data.size()));
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)
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<GameHash> 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<void*>(static_cast<uintptr_t>(1)));
rc_client_begin_change_media_from_hash(s_state.client, GameHashToString(s_state.game_hash.value()),
ClientLoadGameCallback, reinterpret_cast<void*>(static_cast<uintptr_t>(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);
}

@ -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
#pragma once
@ -8,12 +8,11 @@
#include <functional>
#include <mutex>
#include <span>
#include <string>
#include <utility>
#include <vector>
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<u8, GAME_HASH_LENGTH>;
/// Acquires the achievements lock. Must be held when accessing any achievement state from another thread.
std::unique_lock<std::recursive_mutex> 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<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.
bool Initialize();

@ -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<u8>* 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<u8> 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;
}

@ -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);
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 GetGameHashFromBuffer(const std::string_view filename, const std::span<const u8> data);
DiscRegion GetRegionForSerial(const std::string_view serial);

Loading…
Cancel
Save