Achievements: Use rc_client API for fetching game titles (#3658)

* dep/rcheevos: Bump to 7fb4300

* Achievements: Use rc_client API for fetching game titles
This commit is contained in:
mariobob
2025-12-14 11:12:24 +01:00
committed by GitHub
parent 2498e66a6e
commit 1ee0f73df8
3 changed files with 223 additions and 38 deletions

View File

@@ -403,6 +403,41 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_hash_library(
*/ */
RC_EXPORT void RC_CCONV rc_client_destroy_hash_library(rc_client_hash_library_t* list); RC_EXPORT void RC_CCONV rc_client_destroy_hash_library(rc_client_hash_library_t* list);
/*****************************************************************************\
| Fetch Game Titles |
\*****************************************************************************/
typedef struct rc_client_game_title_entry_t {
uint32_t game_id;
const char* title;
char badge_name[16];
} rc_client_game_title_entry_t;
typedef struct rc_client_game_title_list_t {
rc_client_game_title_entry_t* entries;
uint32_t num_entries;
} rc_client_game_title_list_t;
/**
* Callback that is fired when a game titles request completes. list may be null if the query failed.
*/
typedef void(RC_CCONV* rc_client_fetch_game_titles_callback_t)(int result, const char* error_message,
rc_client_game_title_list_t* list, rc_client_t* client,
void* callback_userdata);
/**
* Starts an asynchronous request for titles and badge names for the specified games.
* The caller must provide an array of game IDs and the number of IDs in the array.
*/
RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_game_titles(
rc_client_t* client, const uint32_t* game_ids, uint32_t num_game_ids,
rc_client_fetch_game_titles_callback_t callback, void* callback_userdata);
/**
* Destroys a previously-allocated result from the rc_client_begin_fetch_game_titles() callback.
*/
RC_EXPORT void RC_CCONV rc_client_destroy_game_title_list(rc_client_game_title_list_t* list);
/*****************************************************************************\ /*****************************************************************************\
| Achievements | | Achievements |
\*****************************************************************************/ \*****************************************************************************/

View File

@@ -3605,6 +3605,146 @@ void rc_client_destroy_hash_library(rc_client_hash_library_t* list)
free(list); free(list);
} }
/* ===== Fetch Game Titles ===== */
typedef struct rc_client_fetch_game_titles_callback_data_t {
rc_client_t* client;
rc_client_fetch_game_titles_callback_t callback;
void* callback_userdata;
rc_client_async_handle_t async_handle;
} rc_client_fetch_game_titles_callback_data_t;
static void rc_client_fetch_game_titles_callback(const rc_api_server_response_t* server_response, void* callback_data)
{
rc_client_fetch_game_titles_callback_data_t* titles_callback_data =
(rc_client_fetch_game_titles_callback_data_t*)callback_data;
rc_client_t* client = titles_callback_data->client;
rc_api_fetch_game_titles_response_t titles_response;
const char* error_message;
int result;
result = rc_client_end_async(client, &titles_callback_data->async_handle);
if (result) {
if (result != RC_CLIENT_ASYNC_DESTROYED)
RC_CLIENT_LOG_VERBOSE(client, "Fetch game titles aborted");
free(titles_callback_data);
return;
}
result = rc_api_process_fetch_game_titles_server_response(&titles_response, server_response);
error_message =
rc_client_server_error_message(&result, server_response->http_status_code, &titles_response.response);
if (error_message) {
RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch game titles failed: %s", error_message);
titles_callback_data->callback(result, error_message, NULL, client, titles_callback_data->callback_userdata);
} else {
rc_client_game_title_list_t* list;
size_t strings_size = 0;
const rc_api_game_title_entry_t* src;
const rc_api_game_title_entry_t* stop;
size_t list_size;
/* calculate string buffer size */
for (src = titles_response.entries, stop = src + titles_response.num_entries; src < stop; ++src) {
if (src->title)
strings_size += strlen(src->title) + 1;
}
list_size = sizeof(*list) + sizeof(rc_client_game_title_entry_t) * titles_response.num_entries + strings_size;
list = (rc_client_game_title_list_t*)malloc(list_size);
if (!list) {
titles_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client,
titles_callback_data->callback_userdata);
} else {
rc_client_game_title_entry_t* entry = list->entries =
(rc_client_game_title_entry_t*)((uint8_t*)list + sizeof(*list));
char* strings = (char*)((uint8_t*)list + sizeof(*list) +
sizeof(rc_client_game_title_entry_t) * titles_response.num_entries);
for (src = titles_response.entries, stop = src + titles_response.num_entries; src < stop; ++src, ++entry) {
entry->game_id = src->id;
if (src->title) {
const size_t len = strlen(src->title) + 1;
entry->title = strings;
memcpy(strings, src->title, len);
strings += len;
} else {
entry->title = NULL;
}
if (src->image_name)
snprintf(entry->badge_name, sizeof(entry->badge_name), "%s", src->image_name);
else
entry->badge_name[0] = '\0';
}
list->num_entries = titles_response.num_entries;
titles_callback_data->callback(RC_OK, NULL, list, client, titles_callback_data->callback_userdata);
}
}
rc_api_destroy_fetch_game_titles_response(&titles_response);
free(titles_callback_data);
}
rc_client_async_handle_t* rc_client_begin_fetch_game_titles(rc_client_t* client, const uint32_t* game_ids,
uint32_t num_game_ids,
rc_client_fetch_game_titles_callback_t callback,
void* callback_userdata)
{
rc_api_fetch_game_titles_request_t api_params;
rc_client_fetch_game_titles_callback_data_t* callback_data;
rc_client_async_handle_t* async_handle;
rc_api_request_t request;
int result;
const char* error_message;
if (!client) {
callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata);
return NULL;
}
if (!game_ids || num_game_ids == 0) {
callback(RC_INVALID_STATE, "game_ids is required", NULL, client, callback_userdata);
return NULL;
}
api_params.game_ids = game_ids;
api_params.num_game_ids = num_game_ids;
result = rc_api_init_fetch_game_titles_request_hosted(&request, &api_params, &client->state.host);
if (result != RC_OK) {
error_message = rc_error_str(result);
callback(result, error_message, NULL, client, callback_userdata);
return NULL;
}
callback_data = (rc_client_fetch_game_titles_callback_data_t*)calloc(1, sizeof(*callback_data));
if (!callback_data) {
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata);
return NULL;
}
callback_data->client = client;
callback_data->callback = callback;
callback_data->callback_userdata = callback_userdata;
async_handle = &callback_data->async_handle;
rc_client_begin_async(client, async_handle);
client->callbacks.server_call(&request, rc_client_fetch_game_titles_callback, callback_data, client);
rc_api_destroy_request(&request);
return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL;
}
void rc_client_destroy_game_title_list(rc_client_game_title_list_t* list)
{
free(list);
}
/* ===== Achievements ===== */ /* ===== Achievements ===== */
static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time)

View File

@@ -101,6 +101,14 @@ struct LoginWithPasswordParameters
bool result; bool result;
}; };
struct FetchGameTitlesParameters
{
Error* error;
rc_client_async_handle_t* request;
rc_client_game_title_list_t* list;
bool success;
};
struct LeaderboardTrackerIndicator struct LeaderboardTrackerIndicator
{ {
u32 tracker_id; u32 tracker_id;
@@ -179,6 +187,8 @@ static void HandleServerReconnectedEvent(const rc_client_event_t* event);
static void ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, void* userdata); static void ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, void* userdata);
static void ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, void* userdata); static void ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, void* userdata);
static void FetchGameTitlesCallback(int result, const char* error_message, rc_client_game_title_list_t* list,
rc_client_t* client, void* userdata);
static void ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata); static void ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata);
static void DisplayHardcoreDeferredMessage(); static void DisplayHardcoreDeferredMessage();
@@ -1981,6 +1991,26 @@ void Achievements::ClientLoginWithPasswordCallback(int result, const char* error
FinishLogin(); FinishLogin();
} }
void Achievements::FetchGameTitlesCallback(int result, const char* error_message, rc_client_game_title_list_t* list,
rc_client_t* client, void* userdata)
{
FetchGameTitlesParameters* params = static_cast<FetchGameTitlesParameters*>(userdata);
params->request = nullptr;
if (result != RC_OK || !list)
{
if (error_message)
Error::SetString(params->error, error_message);
else
Error::SetStringFmt(params->error, TRANSLATE_FS("Achievements", "Failed to fetch game titles (code {})."), result);
params->success = false;
return;
}
params->list = list;
params->success = true;
}
void Achievements::ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, void Achievements::ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client,
void* userdata) void* userdata)
{ {
@@ -2110,19 +2140,6 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
progress->FormatStatusText(TRANSLATE_FS("Achievements", "Fetching icon info for {} games..."), game_ids.size()); progress->FormatStatusText(TRANSLATE_FS("Achievements", "Fetching icon info for {} games..."), game_ids.size());
// Fetch game titles (includes badge names) from RetroAchievements
const rc_api_fetch_game_titles_request_t titles_request = {
.game_ids = game_ids.data(),
.num_game_ids = static_cast<u32>(game_ids.size()),
};
rc_api_request_t request;
if (rc_api_init_fetch_game_titles_request(&request, &titles_request) != RC_OK)
{
Error::SetStringView(error, "Failed to create API request.");
return false;
}
auto lock = GetLock(); auto lock = GetLock();
if (!IsActive()) if (!IsActive())
{ {
@@ -2130,30 +2147,23 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
return false; return false;
} }
std::optional<rc_api_fetch_game_titles_response_t> titles_response; // Fetch game titles (includes badge names) from RetroAchievements
s_state.http_downloader->CreatePostRequest( FetchGameTitlesParameters params = {error, nullptr, nullptr, false};
request.url, request.post_data, params.request = rc_client_begin_fetch_game_titles(s_state.client, game_ids.data(),
[&titles_response, error](s32 status_code, const Error&, const std::string&, HTTPDownloader::Request::Data data) { static_cast<u32>(game_ids.size()), FetchGameTitlesCallback, &params);
const rc_api_server_response_t rr = MakeRCAPIServerResponse(status_code, data); if (!params.request)
const int parse_result = rc_api_process_fetch_game_titles_server_response(&titles_response.emplace(), &rr); {
if (parse_result != RC_OK) Error::SetStringView(error, TRANSLATE_SV("Achievements", "Failed to create game titles request."));
{ return false;
Error::SetStringFmt(error, "rc_api_process_fetch_game_titles_server_response() failed: {}", }
rc_error_str(parse_result));
titles_response.reset();
}
},
progress);
rc_api_destroy_request(&request);
WaitForHTTPRequestsWithYield(lock); WaitForHTTPRequestsWithYield(lock);
if (!titles_response.has_value()) if (!params.success || !params.list)
return false; return false;
const ScopedGuard response_guard( const ScopedGuard list_guard([&params]() { rc_client_destroy_game_title_list(params.list); });
[&titles_response]() { rc_api_destroy_fetch_game_titles_response(&titles_response.value()); }); if (params.list->num_entries == 0)
if (titles_response->num_entries == 0)
{ {
Error::SetStringView(error, TRANSLATE_SV("Achievements", "No image names returned.")); Error::SetStringView(error, TRANSLATE_SV("Achievements", "No image names returned."));
return false; return false;
@@ -2161,22 +2171,22 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
// Create all download requests in parallel // Create all download requests in parallel
u32 badges_to_download = 0; u32 badges_to_download = 0;
for (u32 i = 0; i < titles_response->num_entries; i++) for (u32 i = 0; i < params.list->num_entries; i++)
{ {
const rc_api_game_title_entry_t& entry = titles_response->entries[i]; const rc_client_game_title_entry_t& entry = params.list->entries[i];
if (!entry.image_name || entry.image_name[0] == '\0') if (entry.badge_name[0] == '\0')
continue; continue;
std::string path = GetLocalImagePath(entry.image_name, RC_IMAGE_TYPE_GAME); std::string path = GetLocalImagePath(entry.badge_name, RC_IMAGE_TYPE_GAME);
if (FileSystem::FileExists(path.c_str())) if (FileSystem::FileExists(path.c_str()))
{ {
// Already have this icon, just update the cache // Already have this icon, just update the cache
GameList::UpdateAchievementBadgeName(entry.id, entry.image_name); GameList::UpdateAchievementBadgeName(entry.game_id, entry.badge_name);
continue; continue;
} }
std::string url = GetImageURL(entry.image_name, RC_IMAGE_TYPE_GAME); std::string url = GetImageURL(entry.badge_name, RC_IMAGE_TYPE_GAME);
if (url.empty()) if (url.empty())
continue; continue;