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);
|
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 |
|
||||||
\*****************************************************************************/
|
\*****************************************************************************/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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, ¶ms);
|
||||||
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([¶ms]() { 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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user