Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions dep/rcheevos/include/rc_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
\*****************************************************************************/
Expand Down
140 changes: 140 additions & 0 deletions dep/rcheevos/src/rc_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
86 changes: 48 additions & 38 deletions src/core/achievements.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -2110,73 +2140,53 @@ 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())
{
Error::SetStringView(error, TRANSLATE_SV("Achievements", "Achievements are not enabled."));
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, &params);
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([&params]() { 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;
}

// 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;

Expand Down