mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-04 05:04:33 +00:00
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:
@@ -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);
|
||||
|
||||
/*****************************************************************************\
|
||||
| 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 |
|
||||
\*****************************************************************************/
|
||||
|
||||
@@ -3605,6 +3605,146 @@ void rc_client_destroy_hash_library(rc_client_hash_library_t* 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 ===== */
|
||||
|
||||
static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time)
|
||||
|
||||
@@ -101,6 +101,14 @@ struct LoginWithPasswordParameters
|
||||
bool result;
|
||||
};
|
||||
|
||||
struct FetchGameTitlesParameters
|
||||
{
|
||||
Error* error;
|
||||
rc_client_async_handle_t* request;
|
||||
rc_client_game_title_list_t* list;
|
||||
bool success;
|
||||
};
|
||||
|
||||
struct LeaderboardTrackerIndicator
|
||||
{
|
||||
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 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 DisplayHardcoreDeferredMessage();
|
||||
@@ -1981,6 +1991,26 @@ void Achievements::ClientLoginWithPasswordCallback(int result, const char* error
|
||||
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* 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());
|
||||
|
||||
// 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();
|
||||
if (!IsActive())
|
||||
{
|
||||
@@ -2130,30 +2147,23 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<rc_api_fetch_game_titles_response_t> titles_response;
|
||||
s_state.http_downloader->CreatePostRequest(
|
||||
request.url, request.post_data,
|
||||
[&titles_response, error](s32 status_code, const Error&, const std::string&, HTTPDownloader::Request::Data data) {
|
||||
const rc_api_server_response_t rr = MakeRCAPIServerResponse(status_code, data);
|
||||
const int parse_result = rc_api_process_fetch_game_titles_server_response(&titles_response.emplace(), &rr);
|
||||
if (parse_result != RC_OK)
|
||||
{
|
||||
Error::SetStringFmt(error, "rc_api_process_fetch_game_titles_server_response() failed: {}",
|
||||
rc_error_str(parse_result));
|
||||
titles_response.reset();
|
||||
}
|
||||
},
|
||||
progress);
|
||||
// Fetch game titles (includes badge names) from RetroAchievements
|
||||
FetchGameTitlesParameters params = {error, nullptr, nullptr, false};
|
||||
params.request = rc_client_begin_fetch_game_titles(s_state.client, game_ids.data(),
|
||||
static_cast<u32>(game_ids.size()), FetchGameTitlesCallback, ¶ms);
|
||||
if (!params.request)
|
||||
{
|
||||
Error::SetStringView(error, TRANSLATE_SV("Achievements", "Failed to create game titles request."));
|
||||
return false;
|
||||
}
|
||||
|
||||
rc_api_destroy_request(&request);
|
||||
WaitForHTTPRequestsWithYield(lock);
|
||||
|
||||
if (!titles_response.has_value())
|
||||
if (!params.success || !params.list)
|
||||
return false;
|
||||
|
||||
const ScopedGuard response_guard(
|
||||
[&titles_response]() { rc_api_destroy_fetch_game_titles_response(&titles_response.value()); });
|
||||
if (titles_response->num_entries == 0)
|
||||
const ScopedGuard list_guard([¶ms]() { rc_client_destroy_game_title_list(params.list); });
|
||||
if (params.list->num_entries == 0)
|
||||
{
|
||||
Error::SetStringView(error, TRANSLATE_SV("Achievements", "No image names returned."));
|
||||
return false;
|
||||
@@ -2161,22 +2171,22 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
|
||||
|
||||
// Create all download requests in parallel
|
||||
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;
|
||||
|
||||
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()))
|
||||
{
|
||||
// Already have this icon, just update the cache
|
||||
GameList::UpdateAchievementBadgeName(entry.id, entry.image_name);
|
||||
GameList::UpdateAchievementBadgeName(entry.game_id, entry.badge_name);
|
||||
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())
|
||||
continue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user