/* RetroArch - A frontend for libretro. * Copyright (C) 2019-2021 - Brian Weiss * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include "cheevos_client.h" #include "cheevos.h" #include "../configuration.h" #include "../file_path_special.h" #include "../paths.h" #include "../retroarch.h" #include "../version.h" #include #include #include #include #include "../frontend/frontend_driver.h" #include "../network/net_http_special.h" #include "../tasks/tasks_internal.h" #ifdef HAVE_PRESENCE #include "../network/presence.h" #endif #include "../deps/rcheevos/include/rc_api_runtime.h" #include "../deps/rcheevos/include/rc_api_user.h" /* Define this macro to log URLs. */ #undef CHEEVOS_LOG_URLS /* Define this macro to have the password and token logged. * THIS WILL DISCLOSE THE USER'S PASSWORD, TAKE CARE! */ #undef CHEEVOS_LOG_PASSWORD /* Define this macro to load a JSON file from disk instead of downloading * from retroachievements.org. */ #undef CHEEVOS_JSON_OVERRIDE /* Define this macro with a string to save the JSON file to disk with * that name. */ #undef CHEEVOS_SAVE_JSON /* Define this macro to log downloaded badge images. */ #undef CHEEVOS_LOG_BADGES /* Number of usecs to wait between posting rich presence to the site. */ /* Keep consistent with SERVER_PING_FREQUENCY from RAIntegration. */ #define CHEEVOS_PING_FREQUENCY 2 * 60 * 1000000 /**************************** * data types * ****************************/ enum rcheevos_async_io_type { CHEEVOS_ASYNC_RICHPRESENCE = 0, CHEEVOS_ASYNC_AWARD_ACHIEVEMENT, CHEEVOS_ASYNC_SUBMIT_LBOARD, CHEEVOS_ASYNC_LOGIN, CHEEVOS_ASYNC_RESOLVE_HASH, CHEEVOS_ASYNC_FETCH_GAME_DATA, CHEEVOS_ASYNC_FETCH_USER_UNLOCKS, CHEEVOS_ASYNC_FETCH_HARDCORE_USER_UNLOCKS, CHEEVOS_ASYNC_START_SESSION, CHEEVOS_ASYNC_FETCH_BADGE }; struct rcheevos_async_io_request; typedef void (*rcheevos_async_handler)(struct rcheevos_async_io_request *request, http_transfer_data_t *data, char buffer[], size_t buffer_size); typedef struct rcheevos_async_io_request { rc_api_request_t request; rcheevos_async_handler handler; int id; rcheevos_client_callback callback; void* callback_data; int attempt_count; const char* success_message; const char* failure_message; const char* user_agent; char type; } rcheevos_async_io_request; #ifdef HAVE_THREADS #define RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS 2 #else #define RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS 1 #endif typedef struct rcheevos_fetch_badge_state { unsigned badge_fetch_index; unsigned locked_badge_fetch_index; const char* badge_directory; rcheevos_client_callback callback; void* callback_data; char requested_badges[ RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS][32]; } rcheevos_fetch_badge_state; typedef struct rcheevos_fetch_badge_data { rcheevos_fetch_badge_state* state; int request_index; void* data; size_t data_len; rcheevos_client_callback callback; } rcheevos_fetch_badge_data; /**************************** * forward declarations * ****************************/ static retro_time_t rcheevos_client_prepare_ping(rcheevos_async_io_request* request); static void rcheevos_async_http_task_callback( retro_task_t* task, void* task_data, void* user_data, const char* error); static void rcheevos_async_end_request(rcheevos_async_io_request* request); static void rcheevos_async_fetch_badge_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size); /**************************** * user agent construction * ****************************/ static int append_no_spaces(char* buffer, char* stop, const char* text) { char* ptr = buffer; while (ptr < stop && *text) { if (*text == ' ') { *ptr++ = '_'; ++text; } else *ptr++ = *text++; } *ptr = '\0'; return (int)(ptr - buffer); } void rcheevos_get_user_agent(rcheevos_locals_t *locals, char *buffer, size_t len) { char* ptr; struct retro_system_info *sysinfo = &runloop_state_get_ptr()->system.info; /* if we haven't calculated the non-changing portion yet, do so now * [retroarch version + os version] */ if (!locals->user_agent_prefix[0]) { const frontend_ctx_driver_t *frontend = frontend_get_ptr(); if (frontend && frontend->get_os) { char tmp[64]; int major, minor; frontend->get_os(tmp, sizeof(tmp), &major, &minor); snprintf(locals->user_agent_prefix, sizeof(locals->user_agent_prefix), "RetroArch/%s (%s %d.%d)", PACKAGE_VERSION, tmp, major, minor); } else snprintf(locals->user_agent_prefix, sizeof(locals->user_agent_prefix), "RetroArch/%s", PACKAGE_VERSION); } /* append the non-changing portion */ ptr = buffer + strlcpy(buffer, locals->user_agent_prefix, len); /* if a core is loaded, append its information */ if (sysinfo && !string_is_empty(sysinfo->library_name)) { char* stop = buffer + len - 1; const char* path = path_get(RARCH_PATH_CORE); *ptr++ = ' '; if (!string_is_empty(path)) { append_no_spaces(ptr, stop, path_basename(path)); path_remove_extension(ptr); ptr += strlen(ptr); } else ptr += append_no_spaces(ptr, stop, sysinfo->library_name); if (sysinfo->library_version) { *ptr++ = '/'; ptr += append_no_spaces(ptr, stop, sysinfo->library_version); } } *ptr = '\0'; } #ifdef CHEEVOS_LOG_URLS #ifndef CHEEVOS_LOG_PASSWORD static void rcheevos_filter_url_param(char* url, char* param) { char *next; size_t param_len = strlen(param); char *start = strchr(url, '?'); if (!start) start = url; else ++start; do { next = strchr(start, '&'); if (start[param_len] == '=' && memcmp(start, param, param_len) == 0) { if (next) strcpy_literal(start, next + 1); else if (start > url) start[-1] = '\0'; else *start = '\0'; return; } if (!next) return; start = next + 1; } while (1); } #endif #endif void rcheevos_log_url(const char* api, const char* url) { #ifdef CHEEVOS_LOG_URLS #ifdef CHEEVOS_LOG_PASSWORD CHEEVOS_LOG(RCHEEVOS_TAG "GET %s\n", url); #else char copy[256]; strlcpy(copy, url, sizeof(copy)); rcheevos_filter_url_param(copy, "p"); rcheevos_filter_url_param(copy, "t"); CHEEVOS_LOG(RCHEEVOS_TAG "GET %s\n", copy); #endif #else (void)api; (void)url; #endif } static void rcheevos_log_post_url(const char* url, const char* post) { #ifdef CHEEVOS_LOG_URLS #ifdef CHEEVOS_LOG_PASSWORD if (post && post[0]) CHEEVOS_LOG(RCHEEVOS_TAG "POST %s %s\n", url, post); else CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url); #else if (post && post[0]) { char post_copy[2048]; strlcpy(post_copy, post, sizeof(post_copy)); rcheevos_filter_url_param(post_copy, "p"); rcheevos_filter_url_param(post_copy, "t"); if (post_copy[0]) CHEEVOS_LOG(RCHEEVOS_TAG "POST %s %s\n", url, post_copy); else CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url); } else { CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url); } #endif #else (void)url; (void)post; #endif } /**************************** * dispatch * ****************************/ static void rcheevos_async_begin_http_request(rcheevos_async_io_request* request) { if (request->request.post_data) task_push_http_post_transfer_with_user_agent(request->request.url, request->request.post_data, true, "POST", request->user_agent, rcheevos_async_http_task_callback, request); else task_push_http_transfer_with_user_agent(request->request.url, true, "GET", request->user_agent, rcheevos_async_http_task_callback, request); } static void rcheevos_async_retry_request(retro_task_t* task) { rcheevos_async_io_request* request = (rcheevos_async_io_request*) task->user_data; /* the timer task has done its job. let it dispose itself */ task_set_finished(task, 1); /* start a new task for the HTTP call */ rcheevos_async_begin_http_request(request); } static void rcheevos_async_retry_request_after_delay(rcheevos_async_io_request* request, const char* error) { retro_task_t* task = task_init(); /* Double the wait between each attempt until we hit * a maximum delay of two minutes. * 250ms -> 500ms -> 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s... */ retro_time_t retry_delay = (request->attempt_count > 8) ? (120 * 1000 * 1000) : ((250 * 1000) << request->attempt_count); CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s (automatic retry in %dms)\n", request->failure_message, request->id, error, (int)retry_delay / 1000); task->when = cpu_features_get_time_usec() + retry_delay; task->handler = rcheevos_async_retry_request; task->user_data = request; task->progress = -1; ++request->attempt_count; task_queue_push(task); } static bool rcheevos_async_request_failed(rcheevos_async_io_request* request, const char* error) { /* always retry any request once (attempt_count==0) in case of network hiccup */ if (request->attempt_count > 0) { /* retry failed, don't retry these requests */ switch (request->type) { /* timer will ping again */ case CHEEVOS_ASYNC_RICHPRESENCE: /* fallback to the placeholder image */ case CHEEVOS_ASYNC_FETCH_BADGE: return false; case CHEEVOS_ASYNC_RESOLVE_HASH: case CHEEVOS_ASYNC_LOGIN: /* make a maximum of four attempts (0ms -> 250ms -> 500ms -> 1s) */ if (request->attempt_count == 3) return false; break; default: break; } } /* automatically retry the request */ rcheevos_async_retry_request_after_delay(request, error); return true; } static void rcheevos_async_http_task_callback( retro_task_t* task, void* task_data, void* user_data, const char* error) { rcheevos_async_io_request *request = (rcheevos_async_io_request*)user_data; http_transfer_data_t *data = (http_transfer_data_t*)task_data; const bool aborted = rcheevos_load_aborted(); char buffer[224]; if (aborted) { /* load was aborted. don't process the response */ strlcpy(buffer, "Load aborted", sizeof(buffer)); } else if (error) { /* there was a communication error */ /* if automatically requeued, don't process any further */ if (rcheevos_async_request_failed(request, error)) return; strlcpy(buffer, error, sizeof(buffer)); } else if (!data) { /* Server did not return HTTP headers */ strlcpy(buffer, "Server communication error", sizeof(buffer)); } else if (!data->data || !data->len) { if (data->status <= 0) { /* something occurred which prevented the response from being processed. * assume the server request hasn't happened and try again. */ snprintf(buffer, sizeof(buffer), "task status code %d", data->status); rcheevos_async_request_failed(request, buffer); return; } if (data->status != 200) /* Server returned error via status code. */ { snprintf(buffer, sizeof(buffer), "HTTP error code %d", data->status); if (request->type == CHEEVOS_ASYNC_FETCH_BADGE) { /* This isn't a JSON request. An empty response with a status code is a valid response. */ if (request->handler) request->handler(request, data, buffer, sizeof(buffer)); } } else /* Server sent empty response without error status code */ strlcpy(buffer, "No response from server", sizeof(buffer)); } else { /* indicate success unless handler provides error */ buffer[0] = '\0'; /* Call appropriate handler to process the response */ /* NOTE: data->data is not null-terminated. Most handlers assume the * response is properly formatted or will encounter a parse failure * before reading past the end of the data */ if (request->handler) request->handler(request, data, buffer, sizeof(buffer)); } if (!buffer[0]) { /* success */ if (request->success_message) { if (request->id) CHEEVOS_LOG(RCHEEVOS_TAG "%s %u\n", request->success_message, request->id); else CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", request->success_message); } } else { /* encountered an error */ char errbuf[256]; if (request->id) snprintf(errbuf, sizeof(errbuf), "%s %u: %s", request->failure_message, request->id, buffer); else snprintf(errbuf, sizeof(errbuf), "%s: %s", request->failure_message, buffer); if (!aborted) { switch (request->type) { case CHEEVOS_ASYNC_RICHPRESENCE: case CHEEVOS_ASYNC_FETCH_BADGE: /* Don't bother informing user when these fail */ break; case CHEEVOS_ASYNC_LOGIN: case CHEEVOS_ASYNC_RESOLVE_HASH: if (error) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); size_t len = 0; char* ptr; if (rcheevos_locals->load_info.state == RCHEEVOS_LOAD_STATE_NETWORK_ERROR) break; rcheevos_locals->load_info.state = RCHEEVOS_LOAD_STATE_NETWORK_ERROR; while ( /* find the first single slash */ request->request.url[len] != '/' || request->request.url[len + 1] == '/' || request->request.url[len - 1] == '/') ++len; ptr = errbuf + snprintf(errbuf, sizeof(errbuf), "Could not communicate with "); memcpy(ptr, request->request.url, len); ptr[len] = '\0'; } /* fallthrough to default */ default: runloop_msg_queue_push(errbuf, 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); break; } } CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", errbuf); } rcheevos_async_end_request(request); } static void rcheevos_async_end_request(rcheevos_async_io_request* request) { rc_api_destroy_request(&request->request); if (request->callback) request->callback(request->callback_data); /* rich presence request will be reused on next ping - reset the attempt * counter. for all other request types, free the request object */ if (request->type == CHEEVOS_ASYNC_RICHPRESENCE) request->attempt_count = 0; else free(request); } static void rcheevos_async_begin_request( rcheevos_async_io_request* request, int init_result, rcheevos_async_handler handler, char type, int id, const char* success_message, const char* failure_message) { if (init_result != RC_OK) { char errbuf[256]; if (id) snprintf(errbuf, sizeof(errbuf), "%s %u: %s", failure_message, id, rc_error_str(init_result)); else snprintf(errbuf, sizeof(errbuf), "%s: %s", failure_message, rc_error_str(init_result)); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", errbuf); runloop_msg_queue_push(errbuf, 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); rcheevos_async_end_request(request); return; } request->handler = handler; request->type = type; request->id = id; request->success_message = success_message; request->failure_message = failure_message; request->attempt_count = 0; if (!request->user_agent) request->user_agent = get_rcheevos_locals()->user_agent_core; rcheevos_log_post_url(request->request.url, request->request.post_data); rcheevos_async_begin_http_request(request); } static bool rcheevos_async_succeeded(int result, const rc_api_response_t* response, char buffer[], size_t buffer_size) { if (result != RC_OK) { strlcpy(buffer, rc_error_str(result), buffer_size); return false; } if (!response->succeeded) { strlcpy(buffer, response->error_message, buffer_size); return false; } return true; } void rcheevos_client_initialize(void) { const settings_t* settings = config_get_ptr(); const char* host = settings->arrays.cheevos_custom_host; if (!host[0]) { #ifdef HAVE_SSL host = "https://retroachievements.org"; #else host = "http://retroachievements.org"; #endif } CHEEVOS_LOG(RCHEEVOS_TAG "Using host: %s\n", host); if (!string_is_equal(host, "https://retroachievements.org")) { rc_api_set_host(host); if (!string_is_equal(host, "http://retroachievements.org")) rc_api_set_image_host(host); } } /**************************** * login * ****************************/ static void rcheevos_async_login_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); rc_api_login_response_t api_response; int result = rc_api_process_login_response(&api_response, data->data); if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size)) { /* save the token to the config and clear the password on success */ settings_t* settings = config_get_ptr(); strlcpy(settings->arrays.cheevos_token, api_response.api_token, sizeof(settings->arrays.cheevos_token)); settings->arrays.cheevos_password[0] = '\0'; CHEEVOS_LOG(RCHEEVOS_TAG "%s logged in successfully\n", api_response.display_name); strlcpy(rcheevos_locals->displayname, api_response.display_name, sizeof(rcheevos_locals->displayname)); strlcpy(rcheevos_locals->username, api_response.username, sizeof(rcheevos_locals->username)); strlcpy(rcheevos_locals->token, api_response.api_token, sizeof(rcheevos_locals->token)); } else rcheevos_locals->token[0] = '\0'; rc_api_destroy_login_response(&api_response); } static void rcheevos_client_login(const char* username, const char* password, const char* token, rcheevos_client_callback callback, void* userdata) { rcheevos_async_io_request* request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate login request\n"); } else { rc_api_login_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.username = username; api_params.password = password; api_params.api_token = token; result = rc_api_init_login_request(&request->request, &api_params); request->callback = callback; request->callback_data = userdata; rcheevos_async_begin_request(request, result, rcheevos_async_login_callback, CHEEVOS_ASYNC_LOGIN, 0, NULL, "Error logging in"); } } void rcheevos_client_login_with_password(const char* username, const char* password, rcheevos_client_callback callback, void* userdata) { rcheevos_client_login(username, password, NULL, callback, userdata); } void rcheevos_client_login_with_token(const char* username, const char* token, rcheevos_client_callback callback, void* userdata) { rcheevos_client_login(username, NULL, token, callback, userdata); } /**************************** * identify game * ****************************/ static void rcheevos_async_resolve_hash_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rc_api_resolve_hash_response_t api_response; int result = rc_api_process_resolve_hash_response(&api_response, data->data); if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size)) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); rcheevos_locals->game.id = api_response.game_id; } rc_api_destroy_resolve_hash_response(&api_response); } void rcheevos_client_identify_game(const char* hash, rcheevos_client_callback callback, void* userdata) { rcheevos_async_io_request* request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate game identification request\n"); } else { rc_api_resolve_hash_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.game_hash = hash; result = rc_api_init_resolve_hash_request(&request->request, &api_params); request->callback = callback; request->callback_data = userdata; rcheevos_async_begin_request(request, result, rcheevos_async_resolve_hash_callback, CHEEVOS_ASYNC_RESOLVE_HASH, 0, NULL, "Error resolving hash"); } } /**************************** * initialize runtime * ****************************/ typedef struct rcheevos_async_initialize_runtime_data_t { rc_api_fetch_game_data_response_t game_data; rc_api_fetch_user_unlocks_response_t hardcore_unlocks; rc_api_fetch_user_unlocks_response_t non_hardcore_unlocks; rcheevos_client_callback callback; void* callback_data; } rcheevos_async_initialize_runtime_data_t; static void rcheevos_client_copy_achievements( rcheevos_async_initialize_runtime_data_t* runtime_data) { unsigned i, j; const rc_api_achievement_definition_t* definition; rcheevos_racheevo_t *achievement; rcheevos_locals_t *rcheevos_locals = get_rcheevos_locals(); const settings_t *settings = config_get_ptr(); rcheevos_locals->game.achievements = (rcheevos_racheevo_t*) calloc(runtime_data->game_data.num_achievements, sizeof(rcheevos_racheevo_t)); achievement = rcheevos_locals->game.achievements; if (!achievement) { CHEEVOS_ERR(RCHEEVOS_TAG "Could not allocate achievements\n"); return; } definition = runtime_data->game_data.achievements; for (i = 0; i < runtime_data->game_data.num_achievements; ++i, ++definition) { /* invalid definition, ignore */ if ( definition->category == 0 || !definition->definition || !definition->definition[0] || !definition->title || !definition->title[0] || !definition->description || !definition->description[0]) continue; if (definition->category != 3) { if (!settings->bools.cheevos_test_unofficial) continue; achievement->active = RCHEEVOS_ACTIVE_UNOFFICIAL | RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE; } else { achievement->active = RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE; for (j = 0; j < runtime_data->hardcore_unlocks.num_achievement_ids; ++j) { if (runtime_data->hardcore_unlocks.achievement_ids[j] == definition->id) { achievement->active &= ~(RCHEEVOS_ACTIVE_HARDCORE | RCHEEVOS_ACTIVE_SOFTCORE); break; } } if ((achievement->active & RCHEEVOS_ACTIVE_SOFTCORE) != 0) { for (j = 0; j < runtime_data->non_hardcore_unlocks.num_achievement_ids; ++j) { if (runtime_data->non_hardcore_unlocks.achievement_ids[j] == definition->id) { achievement->active &= ~RCHEEVOS_ACTIVE_SOFTCORE; break; } } } } achievement->id = definition->id; achievement->title = strdup(definition->title); achievement->description = strdup(definition->description); achievement->badge = strdup(definition->badge_name); achievement->points = definition->points; /* If an achievement has been fully unlocked, * we don't need to keep the definition around * as it won't be reactivated. Otherwise, * we do have to keep a copy of it. */ if ((achievement->active & ( RCHEEVOS_ACTIVE_HARDCORE | RCHEEVOS_ACTIVE_SOFTCORE)) != 0) achievement->memaddr = strdup(definition->definition); ++achievement; } rcheevos_locals->game.achievement_count = (unsigned)(achievement - rcheevos_locals->game.achievements); } static void rcheevos_client_copy_leaderboards( rcheevos_async_initialize_runtime_data_t* runtime_data) { unsigned i; rcheevos_ralboard_t *leaderboard; const rc_api_leaderboard_definition_t *definition; rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); rcheevos_locals->game.leaderboards = (rcheevos_ralboard_t*) calloc(runtime_data->game_data.num_leaderboards, sizeof(rcheevos_ralboard_t)); rcheevos_locals->game.leaderboard_count = runtime_data->game_data.num_leaderboards; leaderboard = rcheevos_locals->game.leaderboards; if (!leaderboard) { CHEEVOS_ERR(RCHEEVOS_TAG "Could not allocate leaderboards\n"); return; } definition = runtime_data->game_data.leaderboards; for (i = 0; i < runtime_data->game_data.num_leaderboards; ++i, ++definition, ++leaderboard) { leaderboard->id = definition->id; leaderboard->title = strdup(definition->title); leaderboard->description = strdup(definition->description); leaderboard->mem = strdup(definition->definition); leaderboard->format = definition->format; } } static void rcheevos_client_initialize_runtime_rich_presence( rcheevos_async_initialize_runtime_data_t* runtime_data) { if (runtime_data->game_data.rich_presence_script && *runtime_data->game_data.rich_presence_script) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); /* Just activate the rich presence script now. * It can't be toggled on or off, * so there's no reason to keep the unparsed version * around any longer than necessary, and we can avoid * making a copy in the process. */ int result = rc_runtime_activate_richpresence( &rcheevos_locals->runtime, runtime_data->game_data.rich_presence_script, NULL, 0); if (result != RC_OK) { const settings_t* settings = config_get_ptr(); char buffer[256]; snprintf(buffer, sizeof(buffer), "Could not activate rich presence: %s", rc_error_str(result)); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "%s\n", buffer); } } } static void rcheevos_client_initialize_runtime_callback(void* userdata) { rcheevos_async_initialize_runtime_data_t* runtime_data = (rcheevos_async_initialize_runtime_data_t*)userdata; if (rcheevos_end_load_state() > 0) return; if (!rcheevos_load_aborted()) { rcheevos_client_copy_achievements(runtime_data); rcheevos_client_copy_leaderboards(runtime_data); rcheevos_client_initialize_runtime_rich_presence(runtime_data); } rc_api_destroy_fetch_user_unlocks_response(&runtime_data->hardcore_unlocks); rc_api_destroy_fetch_user_unlocks_response(&runtime_data->non_hardcore_unlocks); rc_api_destroy_fetch_game_data_response(&runtime_data->game_data); if (runtime_data->callback) runtime_data->callback(runtime_data->callback_data); free(runtime_data); } static void rcheevos_client_fetch_game_badge_callback(void* userdata) { rcheevos_fetch_badge_data* data = (rcheevos_fetch_badge_data*)userdata; rcheevos_async_initialize_runtime_data_t* runtime_data = (rcheevos_async_initialize_runtime_data_t*)data->state->callback_data; free((void*)data->state->badge_directory); free(data->state); free(data); rcheevos_client_initialize_runtime_callback(runtime_data); } static void rcheevos_client_fetch_game_badge(const char* badge_name, rcheevos_async_initialize_runtime_data_t* runtime_data) { #if defined(HAVE_GFX_WIDGETS) /* don't need game badge unless widgets are enabled */ char badge_fullpath[PATH_MAX_LENGTH] = ""; char *badge_fullname = NULL; size_t badge_fullname_size = 0; /* make sure the directory exists */ fill_pathname_application_special(badge_fullpath, sizeof(badge_fullpath), APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES); if (!path_is_directory(badge_fullpath)) { CHEEVOS_LOG(RCHEEVOS_TAG "Creating %s\n", badge_fullpath); path_mkdir(badge_fullpath); } fill_pathname_slash(badge_fullpath, sizeof(badge_fullpath)); badge_fullname = badge_fullpath + strlen(badge_fullpath); badge_fullname_size = sizeof(badge_fullpath) - (badge_fullname - badge_fullpath); snprintf(badge_fullname, badge_fullname_size, "i%s" FILE_PATH_PNG_EXTENSION, badge_name); /* check if it's already available */ if (path_is_valid(badge_fullpath)) return; #ifdef CHEEVOS_LOG_BADGES CHEEVOS_LOG(RCHEEVOS_TAG "Downloading game badge %s\n", badge_name); #endif { rcheevos_async_io_request* request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); rcheevos_fetch_badge_data* data = (rcheevos_fetch_badge_data*) calloc(1, sizeof(rcheevos_fetch_badge_data)); rcheevos_fetch_badge_state* state = (rcheevos_fetch_badge_state*) calloc(1, sizeof(rcheevos_fetch_badge_state)); if (!request || !data || !state) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate fetch badge request\n"); } else { rc_api_fetch_image_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.image_name = badge_name; api_params.image_type = RC_IMAGE_TYPE_GAME; result = rc_api_init_fetch_image_request(&request->request, &api_params); strlcpy(state->requested_badges[0], badge_fullname, sizeof(state->requested_badges[0])); *badge_fullname = '\0'; state->badge_directory = strdup(badge_fullpath); state->callback_data = runtime_data; data->state = state; data->request_index = 0; data->callback = rcheevos_client_fetch_game_badge_callback; request->callback_data = data; rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_FETCHING_GAME_DATA); rcheevos_async_begin_request(request, result, rcheevos_async_fetch_badge_callback, CHEEVOS_ASYNC_FETCH_BADGE, atoi(badge_name), NULL, "Error fetching game badge"); } } #endif } static void rcheevos_async_fetch_user_unlocks_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rcheevos_async_initialize_runtime_data_t* runtime_data = (rcheevos_async_initialize_runtime_data_t*)request->callback_data; int result; if (request->type == CHEEVOS_ASYNC_FETCH_HARDCORE_USER_UNLOCKS) { result = rc_api_process_fetch_user_unlocks_response( &runtime_data->hardcore_unlocks, data->data); rcheevos_async_succeeded(result, &runtime_data->hardcore_unlocks.response, buffer, buffer_size); } else { result = rc_api_process_fetch_user_unlocks_response( &runtime_data->non_hardcore_unlocks, data->data); rcheevos_async_succeeded(result, &runtime_data->non_hardcore_unlocks.response, buffer, buffer_size); } } static void rcheevos_async_fetch_game_data_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rcheevos_async_initialize_runtime_data_t* runtime_data = (rcheevos_async_initialize_runtime_data_t*)request->callback_data; #ifdef CHEEVOS_SAVE_JSON filestream_write_file(CHEEVOS_SAVE_JSON, data->data, data->len); #endif int result = rc_api_process_fetch_game_data_response( &runtime_data->game_data, data->data); if (rcheevos_async_succeeded(result, &runtime_data->game_data.response, buffer, buffer_size)) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); rcheevos_locals->game.title = strdup( runtime_data->game_data.title); rcheevos_locals->game.console_id = runtime_data->game_data.console_id; snprintf(rcheevos_locals->game.badge_name, sizeof(rcheevos_locals->game.badge_name), "i%s", runtime_data->game_data.image_name); rcheevos_client_fetch_game_badge(runtime_data->game_data.image_name, runtime_data); } else { rcheevos_unload(); } } void rcheevos_client_initialize_runtime(unsigned game_id, rcheevos_client_callback callback, void* userdata) { rcheevos_async_io_request* request; const settings_t *settings = config_get_ptr(); const rcheevos_locals_t *rcheevos_locals = get_rcheevos_locals(); rcheevos_async_initialize_runtime_data_t *data = (rcheevos_async_initialize_runtime_data_t*) malloc(sizeof(rcheevos_async_initialize_runtime_data_t)); if (!data) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate runtime initalization data\n"); return; } data->callback = callback; data->callback_data = userdata; request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate game data fetch request\n"); } else { #ifdef CHEEVOS_JSON_OVERRIDE char buffer[128]; size_t size = 0; char* contents; http_transfer_data_t transfer_data; FILE* file = fopen(CHEEVOS_JSON_OVERRIDE, "rb"); fseek(file, 0, SEEK_END); size = ftell(file); fseek(file, 0, SEEK_SET); contents = (char*)malloc(size + 1); fread((void*)contents, 1, size, file); fclose(file); contents[size] = 0; transfer_data.data = contents; transfer_data.len = size; transfer_data.status = 200; request->callback_data = data; rcheevos_async_fetch_game_data_callback(request, &transfer_data, buffer, sizeof(buffer)); free(contents); #else rc_api_fetch_game_data_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.game_id = rcheevos_locals->game.id; result = rc_api_init_fetch_game_data_request(&request->request, &api_params); request->callback = rcheevos_client_initialize_runtime_callback; request->callback_data = data; rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_FETCHING_GAME_DATA); rcheevos_async_begin_request(request, result, rcheevos_async_fetch_game_data_callback, CHEEVOS_ASYNC_FETCH_GAME_DATA, rcheevos_locals->game.id, "Fetched game data", "Error fetching game data"); #endif } if (settings->bools.cheevos_start_active) { memset(&data->hardcore_unlocks, 0, sizeof(data->hardcore_unlocks)); memset(&data->non_hardcore_unlocks, 0, sizeof(data->non_hardcore_unlocks)); data->hardcore_unlocks.num_achievement_ids = 0; data->non_hardcore_unlocks.num_achievement_ids = 0; } else { int i; for (i = 0; i < 2; ++i) { request = (rcheevos_async_io_request*)calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate user unlock request\n"); } else { rc_api_fetch_user_unlocks_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.game_id = rcheevos_locals->game.id; api_params.hardcore = i; result = rc_api_init_fetch_user_unlocks_request(&request->request, &api_params); request->callback = rcheevos_client_initialize_runtime_callback; request->callback_data = data; rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_FETCHING_GAME_DATA); if (i == 0) { rcheevos_async_begin_request(request, result, rcheevos_async_fetch_user_unlocks_callback, CHEEVOS_ASYNC_FETCH_USER_UNLOCKS, rcheevos_locals->game.id, "Fetched user unlocks", "Error fetching user unlocks"); } else { rcheevos_async_begin_request(request, result, rcheevos_async_fetch_user_unlocks_callback, CHEEVOS_ASYNC_FETCH_HARDCORE_USER_UNLOCKS, rcheevos_locals->game.id, "Fetched hardcore user unlocks", "Error fetching hardcore user unlocks"); } } } } } /**************************** * ping * ****************************/ static retro_time_t rcheevos_client_prepare_ping( rcheevos_async_io_request* request) { rc_api_ping_request_t api_params; const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); const settings_t *settings = config_get_ptr(); const bool cheevos_richpresence_enable = rcheevos_hardcore_active() || settings->bools.cheevos_richpresence_enable; char buffer[256] = ""; memset(&api_params, 0, sizeof(api_params)); api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.game_id = request->id; if (cheevos_richpresence_enable) { rcheevos_get_richpresence(buffer, sizeof(buffer)); api_params.rich_presence = buffer; } rc_api_init_ping_request(&request->request, &api_params); rcheevos_log_post_url(request->request.url, request->request.post_data); #ifdef HAVE_PRESENCE presence_update(PRESENCE_RETROACHIEVEMENTS); #endif /* Update rich presence every two minutes */ if (cheevos_richpresence_enable) return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY; /* Send ping every four minutes */ return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY * 2; } static void rcheevos_async_ping_handler(retro_task_t* task) { rcheevos_async_io_request* request = (rcheevos_async_io_request*) task->user_data; const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); if (request->id != (int)rcheevos_locals->game.id) { CHEEVOS_LOG(RCHEEVOS_TAG "Stopping periodic rich presence update task for game %u\n", request->id); /* game changed; stop the recurring task - a new one will * be scheduled if a new game is loaded */ task_set_finished(task, 1); /* request->request was destroyed * in rcheevos_async_http_task_callback */ free(request); return; } /* update the request and set the task to fire again in * two minutes */ task->when = rcheevos_client_prepare_ping(request); /* start the HTTP request */ rcheevos_async_begin_http_request(request); } /**************************** * start session * ****************************/ static void rcheevos_async_start_session_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rc_api_start_session_response_t api_response; int result = rc_api_process_start_session_response( &api_response, data->data); rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size); rc_api_destroy_start_session_response(&api_response); } void rcheevos_client_start_session(unsigned game_id) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); /* the core won't change while a session is active, so only * calculate the user agent once */ rcheevos_get_user_agent(rcheevos_locals, rcheevos_locals->user_agent_core, sizeof(rcheevos_locals->user_agent_core)); /* schedule the first rich presence call in 30 seconds */ { rcheevos_async_io_request *request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate rich presence request\n"); } else { retro_task_t* task = task_init(); request->id = game_id; request->type = CHEEVOS_ASYNC_RICHPRESENCE; request->user_agent = rcheevos_locals->user_agent_core; request->failure_message = "Error sending ping"; task->handler = rcheevos_async_ping_handler; task->user_data = request; task->progress = -1; task->when = cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY / 4; CHEEVOS_LOG(RCHEEVOS_TAG "Starting periodic rich presence update task for game %u\n", game_id); task_queue_push(task); } } /* send the new session request */ { rcheevos_async_io_request* request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate new session request\n"); } else { rc_api_start_session_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.game_id = game_id; result = rc_api_init_start_session_request( &request->request, &api_params); rcheevos_async_begin_request(request, result, rcheevos_async_start_session_callback, CHEEVOS_ASYNC_START_SESSION, game_id, "Started session for game", "Error starting session for game"); } } } /**************************** * fetch badge * ****************************/ static bool rcheevos_fetch_next_badge(rcheevos_fetch_badge_state* state); static void rcheevos_end_fetch_badges(rcheevos_fetch_badge_state* state) { if (state->callback) state->callback(state->callback_data); free((void*)state->badge_directory); free(state); } static void rcheevos_async_download_next_badge(void* userdata) { rcheevos_fetch_badge_data* badge_data = (rcheevos_fetch_badge_data*)userdata; rcheevos_fetch_next_badge(badge_data->state); if (rcheevos_end_load_state() == 0) rcheevos_end_fetch_badges(badge_data->state); free(badge_data); } static void rcheevos_async_fetch_badge_complete(rcheevos_fetch_badge_data* badge_data) { #ifdef HAVE_THREADS const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); slock_lock(rcheevos_locals->load_info.request_lock); #endif badge_data->state->requested_badges[badge_data->request_index][0] = '\0'; #ifdef HAVE_THREADS slock_unlock(rcheevos_locals->load_info.request_lock); #endif if (badge_data->callback) badge_data->callback(badge_data); } static void rcheevos_async_write_badge(retro_task_t* task) { char badge_fullpath[PATH_MAX_LENGTH]; rcheevos_fetch_badge_data* badge_data = (rcheevos_fetch_badge_data*)task->user_data; fill_pathname_join_special(badge_fullpath, badge_data->state->badge_directory, badge_data->state->requested_badges[badge_data->request_index], sizeof(badge_fullpath)); if (!filestream_write_file(badge_fullpath, badge_data->data, badge_data->data_len)) { CHEEVOS_ERR(RCHEEVOS_TAG "Error writing badge %s\n", badge_fullpath); } free(badge_data->data); badge_data->data = NULL; badge_data->data_len = 0; task_set_finished(task, true); rcheevos_async_fetch_badge_complete(badge_data); } static void rcheevos_async_fetch_badge_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rcheevos_fetch_badge_data* badge_data = (rcheevos_fetch_badge_data*)request->callback_data; retro_task_t* task; if (!data->data) { /* error retrieving badge, nothing to write. just call the callback */ rcheevos_async_fetch_badge_complete(badge_data); return; } /* take ownership of the file data */ badge_data->data = data->data; badge_data->data_len = data->len; data->data = NULL; data->len = 0; /* this is called on the primary thread. use a task to write the file from a background thread */ task = task_init(); task->handler = rcheevos_async_write_badge; task->user_data = badge_data; task_queue_push(task); } static bool rcheevos_client_fetch_badge( const char* badge_name, int locked, rcheevos_fetch_badge_state* state) { char badge_fullpath[PATH_MAX_LENGTH]; char* badge_fullname = NULL; size_t badge_fullname_size = 0; int request_index = -1; if (!badge_name || !badge_name[0]) return false; strlcpy(badge_fullpath, state->badge_directory, sizeof(badge_fullpath)); fill_pathname_slash(badge_fullpath, sizeof(badge_fullpath)); badge_fullname = badge_fullpath + strlen(badge_fullpath); badge_fullname_size = sizeof(badge_fullpath) - (badge_fullname - badge_fullpath); snprintf(badge_fullname, badge_fullname_size, "%s%s" FILE_PATH_PNG_EXTENSION, badge_name, locked ? "_lock" : ""); /* check if it's already available */ if (path_is_valid(badge_fullpath)) return false; /* check if it's already requested */ { int i; int found_index = -1; #ifdef HAVE_THREADS const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); slock_lock(rcheevos_locals->load_info.request_lock); #endif for (i = RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS - 1; i >= 0; --i) { if (!state->requested_badges[i][0]) request_index = i; else if (string_is_equal(badge_fullname, state->requested_badges[i])) { found_index = i; break; } } if (found_index == -1) { /* unexpected - but if it happens, * the queue is full. Pretend we found * a match to prevent an exception */ if (request_index == -1) found_index = 0; else strlcpy(state->requested_badges[request_index], badge_fullname, sizeof(state->requested_badges[request_index])); } #ifdef HAVE_THREADS slock_unlock(rcheevos_locals->load_info.request_lock); #endif if (found_index != -1) return false; } /* request the new badge */ #ifdef CHEEVOS_LOG_BADGES CHEEVOS_LOG(RCHEEVOS_TAG "Downloading badge %s\n", badge_name); #endif { rcheevos_async_io_request* request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); rcheevos_fetch_badge_data* data = (rcheevos_fetch_badge_data*) calloc(1, sizeof(rcheevos_fetch_badge_data)); if (!request || !data) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate fetch badge request\n"); } else { rc_api_fetch_image_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.image_name = badge_name; api_params.image_type = locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; result = rc_api_init_fetch_image_request(&request->request, &api_params); data->state = state; data->request_index = request_index; data->callback = rcheevos_async_download_next_badge; request->callback_data = data; rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_FETCHING_BADGES); rcheevos_async_begin_request(request, result, rcheevos_async_fetch_badge_callback, CHEEVOS_ASYNC_FETCH_BADGE, atoi(badge_name), NULL, "Error fetching badge"); } } return true; } static bool rcheevos_fetch_next_badge(rcheevos_fetch_badge_state* state) { if (rcheevos_load_aborted()) { CHEEVOS_LOG(RCHEEVOS_TAG "Load aborted while fetching badges\n"); } else { int active = 0; const rcheevos_racheevo_t *cheevo = NULL; const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); /* fetch badges for current state of achievements first */ do { #ifdef HAVE_THREADS slock_lock(rcheevos_locals->load_info.request_lock); #endif if ( state->locked_badge_fetch_index < rcheevos_locals->game.achievement_count) cheevo = &rcheevos_locals->game.achievements[state->locked_badge_fetch_index++]; else cheevo = NULL; #ifdef HAVE_THREADS slock_unlock(rcheevos_locals->load_info.request_lock); #endif if (!cheevo) break; active = (cheevo->active & (RCHEEVOS_ACTIVE_HARDCORE | RCHEEVOS_ACTIVE_SOFTCORE)); if (rcheevos_client_fetch_badge(cheevo->badge, active, state)) return true; } while (true); /* then fetch badges for unlocked state so they're ready when the user unlocks something */ do { #ifdef HAVE_THREADS slock_lock(rcheevos_locals->load_info.request_lock); #endif if (state->badge_fetch_index < rcheevos_locals->game.achievement_count) cheevo = &rcheevos_locals->game.achievements[state->badge_fetch_index++]; else cheevo = NULL; #ifdef HAVE_THREADS slock_unlock(rcheevos_locals->load_info.request_lock); #endif if (!cheevo) break; if (rcheevos_client_fetch_badge(cheevo->badge, 0, state)) return true; } while (true); } return false; } void rcheevos_client_fetch_badges(rcheevos_client_callback callback, void* userdata) { #if defined(HAVE_MENU) || defined(HAVE_GFX_WIDGETS) /* don't need badges unless menu or widgets are enabled */ rcheevos_fetch_badge_state* state = NULL; char badge_fullpath[PATH_MAX_LENGTH] = ""; #if !defined(HAVE_GFX_WIDGETS) /* we always want badges if widgets are enabled */ settings_t* settings = config_get_ptr(); /* User has explicitly disabled badges */ if (!settings->bools.cheevos_badges_enable) return; /* badges are only needed for xmb and ozone menus */ if (!string_is_equal(settings->arrays.menu_driver, "xmb") && !string_is_equal(settings->arrays.menu_driver, "ozone")) return; #endif /* !defined(HAVE_GFX_WIDGETS) */ /* make sure the directory exists */ fill_pathname_application_special(badge_fullpath, sizeof(badge_fullpath), APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES); if (!path_is_directory(badge_fullpath)) { CHEEVOS_LOG(RCHEEVOS_TAG "Creating %s\n", badge_fullpath); path_mkdir(badge_fullpath); } /* start the download task */ state = (rcheevos_fetch_badge_state*) calloc(1, sizeof(rcheevos_fetch_badge_state)); if (!state) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate fetch badge state\n"); } else { int num_concurrent = RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS; state->badge_directory = strdup(badge_fullpath); state->locked_badge_fetch_index = 0; state->badge_fetch_index = 0; state->callback = callback; state->callback_data = userdata; rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_FETCHING_BADGES); /* fetch the placeholder image */ if (rcheevos_client_fetch_badge("00000", 0, state)) num_concurrent--; /* queue up additional requests so up to {num_concurrent} downloads are queued */ while (num_concurrent--) { if (!rcheevos_fetch_next_badge(state)) break; } if (rcheevos_end_load_state() == 0) rcheevos_end_fetch_badges(state); } #endif /* defined(HAVE_MENU) || defined(HAVE_GFX_WIDGETS) */ } #undef RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS /**************************** * award achievement * ****************************/ static void rcheevos_async_award_achievement_callback( struct rcheevos_async_io_request* request, http_transfer_data_t *data, char buffer[], size_t buffer_size) { rc_api_award_achievement_response_t api_response; int result = rc_api_process_award_achievement_response( &api_response, data->data); if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size)) { if ((int)api_response.awarded_achievement_id != request->id) snprintf(buffer, buffer_size, "Achievement %u awarded instead", api_response.awarded_achievement_id); else if (api_response.response.error_message) { /* previously unlocked achievements are returned as a "successful" error */ CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u: %s\n", request->id, api_response.response.error_message); } if (api_response.achievements_remaining == 0) rcheevos_show_mastery_placard(); } rc_api_destroy_award_achievement_response(&api_response); } void rcheevos_client_award_achievement(unsigned achievement_id) { rcheevos_async_io_request *request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate unlock request for achievement %u\n", achievement_id); } else { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); rc_api_award_achievement_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.achievement_id = achievement_id; api_params.hardcore = rcheevos_locals->hardcore_active ? 1 : 0; api_params.game_hash = rcheevos_locals->game.hash; result = rc_api_init_award_achievement_request(&request->request, &api_params); rcheevos_async_begin_request(request, result, rcheevos_async_award_achievement_callback, CHEEVOS_ASYNC_AWARD_ACHIEVEMENT, achievement_id, "Awarded achievement", "Error awarding achievement"); } } /**************************** * submit leaderboard * ****************************/ static void rcheevos_async_submit_lboard_entry_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size) { rc_api_submit_lboard_entry_response_t api_response; int result = rc_api_process_submit_lboard_entry_response( &api_response, data->data); /* not currently doing anything with the response */ if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size)) { } rc_api_destroy_submit_lboard_entry_response(&api_response); } void rcheevos_client_submit_lboard_entry(unsigned leaderboard_id, int value) { rcheevos_async_io_request *request = (rcheevos_async_io_request*) calloc(1, sizeof(rcheevos_async_io_request)); if (!request) { CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate request for lboard %u submit\n", leaderboard_id); } else { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); rc_api_submit_lboard_entry_request_t api_params; int result; memset(&api_params, 0, sizeof(api_params)); api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.leaderboard_id = leaderboard_id; api_params.score = value; api_params.game_hash = rcheevos_locals->game.hash; result = rc_api_init_submit_lboard_entry_request(&request->request, &api_params); rcheevos_async_begin_request(request, result, rcheevos_async_submit_lboard_entry_callback, CHEEVOS_ASYNC_SUBMIT_LBOARD, leaderboard_id, "Submitted leaderboard", "Error submitting leaderboard"); } }