diff --git a/Makefile.common b/Makefile.common index bd1188034b..5b9947dd28 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2119,6 +2119,7 @@ ifeq ($(HAVE_NETWORKING), 1) INCLUDE_DIRS += -Ideps/rcheevos/include OBJ += cheevos/cheevos.o \ + cheevos/cheevos_client.o \ cheevos/cheevos_menu.o \ cheevos/cheevos_parser.o \ $(LIBRETRO_COMM_DIR)/formats/cdfs/cdfs.o \ @@ -2138,6 +2139,9 @@ ifeq ($(HAVE_NETWORKING), 1) deps/rcheevos/src/rcheevos/trigger.o \ deps/rcheevos/src/rcheevos/value.o \ deps/rcheevos/src/rhash/hash.o \ + deps/rcheevos/src/rapi/rc_api_common.o \ + deps/rcheevos/src/rapi/rc_api_runtime.o \ + deps/rcheevos/src/rapi/rc_api_user.o \ deps/rcheevos/src/rurl/url.o ifeq ($(HAVE_LUA), 1) diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index 902b187070..cd0b7f36c8 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -41,10 +41,6 @@ #include #endif -#ifdef HAVE_DISCORD -#include "../network/discord.h" -#endif - #ifdef HAVE_CHEATS #include "../cheat_manager.h" #endif @@ -54,23 +50,20 @@ #endif #include "cheevos.h" +#include "cheevos_client.h" #include "cheevos_locals.h" #include "cheevos_parser.h" #include "../file_path_special.h" #include "../paths.h" #include "../command.h" -#include "../dynamic.h" #include "../configuration.h" #include "../performance_counters.h" #include "../msg_hash.h" #include "../retroarch.h" #include "../core.h" #include "../core_option_manager.h" -#include "../version.h" -#include "../frontend/frontend_driver.h" -#include "../network/net_http_special.h" #include "../tasks/tasks_internal.h" #include "../deps/rcheevos/include/rc_runtime.h" @@ -89,42 +82,12 @@ * that name. */ #undef CHEEVOS_SAVE_JSON -/* 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 log downloaded badge images. */ #undef CHEEVOS_LOG_BADGES /* Define this macro to capture how long it takes to generate a hash */ #undef CHEEVOS_TIME_HASH -/* 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 - -enum rcheevos_async_io_type -{ - CHEEVOS_ASYNC_RICHPRESENCE = 0, - CHEEVOS_ASYNC_AWARD_ACHIEVEMENT, - CHEEVOS_ASYNC_SUBMIT_LBOARD -}; - -typedef struct rcheevos_async_io_request -{ - const char* success_message; - const char* failure_message; - int id; - int value; - int attempt_count; - char user_agent[256]; - char type; - char hardcore; -} rcheevos_async_io_request; - static rcheevos_locals_t rcheevos_locals = { {0}, /* runtime */ @@ -135,9 +98,11 @@ static rcheevos_locals_t rcheevos_locals = NULL, /* task_lock */ CMD_EVENT_NONE, /* queued_command */ #endif - {0}, /* token */ + "", /* username */ + "", /* token */ "N/A",/* hash */ "", /* user_agent_prefix */ + "", /* user_agent_core */ #ifdef HAVE_MENU NULL, /* menuitems */ 0, /* menuitem_capacity */ @@ -167,10 +132,6 @@ rcheevos_locals_t* get_rcheevos_locals(void) #define CHEEVOS_MB(x) ((x) * 1024 * 1024) /* Forward declaration */ -static void rcheevos_async_task_callback( - retro_task_t* task, void* task_data, void* user_data, const char* error); -static void rcheevos_async_submit_lboard(rcheevos_locals_t *locals, - rcheevos_async_io_request* request); static void rcheevos_validate_memrefs(rcheevos_locals_t* locals); /***************************************************************************** @@ -184,173 +145,6 @@ void rcheevos_log(const char *fmt, ...) } #endif -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 (ptr - buffer); -} - -static void rcheevos_get_user_agent( - rcheevos_locals_t *locals, - char *buffer, size_t len) -{ - struct retro_system_info *system = runloop_get_libretro_system_info(); - char* ptr; - - /* 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(); - int major, minor; - char tmp[64]; - - if (frontend && frontend->get_os) - { - 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 (system && !string_is_empty(system->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, system->library_name); - } - - if (system->library_version) - { - *ptr++ = '/'; - ptr += append_no_spaces(ptr, stop, system->library_version); - } - } - - *ptr = '\0'; -} - -#ifdef CHEEVOS_LOG_URLS -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 - -static void rcheevos_log_url(const char* api, const char* url) -{ -#ifdef CHEEVOS_LOG_URLS -#ifdef CHEEVOS_LOG_PASSWORD - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, 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 "%s: %s\n", api, copy); -#endif -#else - (void)api; - (void)url; -#endif -} - -static void rcheevos_log_post_url( - const char* api, - const char* url, - const char* post) -{ -#ifdef CHEEVOS_LOG_URLS - #ifdef CHEEVOS_LOG_PASSWORD - if (post && post[0]) - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s&%s\n", api, url, post); - else - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, 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 "%s: %s&%s\n", api, url, post_copy); - else - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url); - } - else - { - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url); - } - #endif -#else - (void)api; - (void)url; - (void)post; -#endif -} static void rcheevos_achievement_disabled(rcheevos_racheevo_t* cheevo, unsigned address) { @@ -453,208 +247,6 @@ static unsigned rcheevos_peek(unsigned address, unsigned num_bytes, void* ud) return 0; } -static void rcheevos_async_award_achievement( - rcheevos_locals_t *locals, - rcheevos_async_io_request* request) -{ - char buffer[256]; - settings_t *settings = config_get_ptr(); - int ret = rc_url_award_cheevo(buffer, sizeof(buffer), - settings->arrays.cheevos_username, - locals->token, - request->id, - request->hardcore, - locals->hash); - - if (ret != 0) - { - CHEEVOS_ERR(RCHEEVOS_TAG "Buffer too small to create URL\n"); - free(request); - return; - } - - rcheevos_log_url("rc_url_award_cheevo", buffer); - task_push_http_transfer_with_user_agent(buffer, true, NULL, - request->user_agent, rcheevos_async_task_callback, request); - -#ifdef HAVE_AUDIOMIXER - if (settings->bools.cheevos_unlock_sound_enable) - audio_driver_mixer_play_menu_sound( - AUDIO_MIXER_SYSTEM_SLOT_ACHIEVEMENT_UNLOCK); -#endif -} - -static retro_time_t rcheevos_async_send_rich_presence( - rcheevos_locals_t *locals, - rcheevos_async_io_request* request) -{ - char url[256], post_data[1024]; - char buffer[256] = ""; - const settings_t *settings = config_get_ptr(); - const char *cheevos_username = settings->arrays.cheevos_username; - const bool cheevos_richpresence_enable = settings->bools.cheevos_richpresence_enable; - int ret; - - if (cheevos_richpresence_enable) - rcheevos_get_richpresence(buffer, sizeof(buffer)); - - ret = rc_url_ping(url, sizeof(url), post_data, sizeof(post_data), - cheevos_username, locals->token, locals->patchdata.game_id, buffer); - - if (ret < 0) - { - CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); - } - else - { - rcheevos_log_post_url("rc_url_ping", url, post_data); - - rcheevos_get_user_agent(locals, - request->user_agent, sizeof(request->user_agent)); - task_push_http_post_transfer_with_user_agent(url, post_data, true, "POST", request->user_agent, NULL, NULL); - } - -#ifdef HAVE_DISCORD - if (settings->bools.discord_enable && discord_is_ready()) - discord_update(DISCORD_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_task_handler(retro_task_t* task) -{ - rcheevos_async_io_request* request = (rcheevos_async_io_request*) - task->user_data; - - switch (request->type) - { - case CHEEVOS_ASYNC_RICHPRESENCE: - /* update the task to fire again in two minutes */ - if (request->id == (int)rcheevos_locals.patchdata.game_id) - task->when = rcheevos_async_send_rich_presence(&rcheevos_locals, - request); - else - { - /* game changed; stop the recurring task - a new one will - * be scheduled for the next game */ - task_set_finished(task, 1); - free(request); - } - break; - - case CHEEVOS_ASYNC_AWARD_ACHIEVEMENT: - rcheevos_async_award_achievement(&rcheevos_locals, request); - task_set_finished(task, 1); - break; - - case CHEEVOS_ASYNC_SUBMIT_LBOARD: - rcheevos_async_submit_lboard(&rcheevos_locals, request); - task_set_finished(task, 1); - break; - } -} - -static void rcheevos_async_schedule( - rcheevos_async_io_request* request, retro_time_t delay) -{ - retro_task_t* task = task_init(); - task->when = cpu_features_get_time_usec() + delay; - task->handler = rcheevos_async_task_handler; - task->user_data = request; - task->progress = -1; - task_queue_push(task); -} - -static void rcheevos_async_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; - - if (!error) - { - char buffer[224] = ""; - /* Server did not return HTTP headers */ - if (!data) - snprintf(buffer, sizeof(buffer), "Server communication error"); - else if (data->status != 200) - { - /* Server returned an error via status code. - * Check to see if it also returned a JSON error */ - if (!data->data || rcheevos_get_json_error(data->data, buffer, sizeof(buffer)) != RC_OK) - snprintf(buffer, sizeof(buffer), "HTTP error code: %d", - data->status); - } - else if (!data->data || !data->len) - { - /* Server sent an empty response without an error status code */ - snprintf(buffer, sizeof(buffer), "No response from server"); - } - else - { - /* Server sent a message - assume it's JSON - * and check for a JSON error */ - rcheevos_get_json_error(data->data, buffer, sizeof(buffer)); - } - - if (buffer[0]) - { - char errbuf[256]; - snprintf(errbuf, sizeof(errbuf), "%s %u: %s", - request->failure_message, request->id, buffer); - CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", errbuf); - - switch (request->type) - { - case CHEEVOS_ASYNC_RICHPRESENCE: - /* Don't bother informing user when - * rich presence update fails */ - break; - - case CHEEVOS_ASYNC_AWARD_ACHIEVEMENT: - /* ignore already unlocked */ - if (string_starts_with_size(buffer, "User already has ", - STRLEN_CONST("User already has "))) - break; - /* fallthrough to default */ - - default: - runloop_msg_queue_push(errbuf, 0, 5 * 60, false, NULL, - MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); - break; - } - } - else - { - CHEEVOS_LOG(RCHEEVOS_TAG "%s %u\n", request->success_message, request->id); - } - - free(request); - } - else - { - /* 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); - - request->attempt_count++; - rcheevos_async_schedule(request, retry_delay); - - CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s\n", request->failure_message, - request->id, error); - } -} - static void rcheevos_activate_achievements(rcheevos_locals_t *locals, rcheevos_racheevo_t* cheevo, unsigned count, unsigned flags) { @@ -815,14 +407,7 @@ static int rcheevos_parse(rcheevos_locals_t *locals, const char* json) } } - /* 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)); - request->id = locals->patchdata.game_id; - request->type = CHEEVOS_ASYNC_RICHPRESENCE; - rcheevos_async_schedule(request, CHEEVOS_PING_FREQUENCY / 4); - } + rcheevos_client_start_session(locals->patchdata.game_id); /* validate the memrefs */ if (rcheevos_locals.memory.count != 0) @@ -858,10 +443,10 @@ static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id) return NULL; } -static void rcheevos_award_achievement(rcheevos_locals_t *locals, +void rcheevos_award_achievement(rcheevos_locals_t* locals, rcheevos_racheevo_t* cheevo, bool widgets_ready) { - char buffer[256] = ""; + const settings_t *settings = config_get_ptr(); if (!cheevo) return; @@ -869,7 +454,7 @@ static void rcheevos_award_achievement(rcheevos_locals_t *locals, CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n", cheevo->id, cheevo->title, cheevo->description); - /* Deactivates the cheevo. */ + /* Deactivates the acheivement. */ rc_runtime_deactivate_achievement(&locals->runtime, cheevo->id); cheevo->active &= ~RCHEEVOS_ACTIVE_SOFTCORE; @@ -878,49 +463,44 @@ static void rcheevos_award_achievement(rcheevos_locals_t *locals, cheevo->unlock_time = cpu_features_get_time_usec(); - /* Show the OSD message. */ - { + /* Show the on screen message. */ #if defined(HAVE_GFX_WIDGETS) - if (widgets_ready) - gfx_widgets_push_achievement(cheevo->title, cheevo->badge); - else + if (widgets_ready) + { + gfx_widgets_push_achievement(cheevo->title, cheevo->badge); + } + else #endif - { - snprintf(buffer, sizeof(buffer), - "Achievement Unlocked: %s", cheevo->title); - runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, - MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); - runloop_msg_queue_push(cheevo->description, 0, 3 * 60, false, NULL, - MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); - } + { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%s: %s", + msg_hash_to_str(MSG_ACHIEVEMENT_UNLOCKED), cheevo->title); + runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); + runloop_msg_queue_push(cheevo->description, 0, 3 * 60, false, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } /* Start the award task (unofficial achievement unlocks are not submitted). */ if (!(cheevo->active & RCHEEVOS_ACTIVE_UNOFFICIAL)) - { - rcheevos_async_io_request *request = (rcheevos_async_io_request*)calloc(1, sizeof(rcheevos_async_io_request)); - request->type = CHEEVOS_ASYNC_AWARD_ACHIEVEMENT; - request->id = cheevo->id; - request->hardcore = locals->hardcore_active ? 1 : 0; - request->success_message = "Awarded achievement"; - request->failure_message = "Error awarding achievement"; - rcheevos_get_user_agent(locals, - request->user_agent, sizeof(request->user_agent)); - rcheevos_async_award_achievement(locals, request); - } + rcheevos_client_award_achievement(cheevo->id); + /* play the unlock sound */ +#ifdef HAVE_AUDIOMIXER + if (settings->bools.cheevos_unlock_sound_enable) + audio_driver_mixer_play_menu_sound( + AUDIO_MIXER_SYSTEM_SLOT_ACHIEVEMENT_UNLOCK); +#endif + + /* Take a screenshot of the achievement. */ #ifdef HAVE_SCREENSHOTS + if (settings->bools.cheevos_auto_screenshot) { - settings_t *settings = config_get_ptr(); - /* Take a screenshot of the achievement. */ - if (settings && settings->bools.cheevos_auto_screenshot) + size_t shotname_len = sizeof(char) * 8192; + char *shotname = (char*)malloc(shotname_len); + + if (shotname) { - size_t shotname_len = sizeof(char) * 8192; - char *shotname = (char*)malloc(shotname_len); - - if (!shotname) - return; - snprintf(shotname, shotname_len, "%s/%s-cheevo-%u", settings->paths.directory_screenshot, path_basename(path_get(RARCH_PATH_BASENAME)), @@ -931,40 +511,18 @@ static void rcheevos_award_achievement(rcheevos_locals_t *locals, shotname, true, video_driver_cached_frame_has_valid_framebuffer(), false, true)) - CHEEVOS_LOG( - RCHEEVOS_TAG "Captured screenshot for achievement %u\n", + CHEEVOS_LOG(RCHEEVOS_TAG "Captured screenshot for achievement %u\n", cheevo->id); else - CHEEVOS_LOG( - RCHEEVOS_TAG "Failed to capture screenshot for achievement %u\n", + CHEEVOS_LOG(RCHEEVOS_TAG "Failed to capture screenshot for achievement %u\n", cheevo->id); + free(shotname); } } #endif } -static void rcheevos_async_submit_lboard(rcheevos_locals_t *locals, - rcheevos_async_io_request* request) -{ - char buffer[256]; - settings_t *settings = config_get_ptr(); - int ret = rc_url_submit_lboard(buffer, sizeof(buffer), - settings->arrays.cheevos_username, - locals->token, request->id, request->value); - - if (ret != 0) - { - CHEEVOS_ERR(RCHEEVOS_TAG "Buffer too small to create URL\n"); - free(request); - return; - } - - rcheevos_log_url("rc_url_submit_lboard", buffer); - task_push_http_transfer_with_user_agent(buffer, true, NULL, - request->user_agent, rcheevos_async_task_callback, request); -} - static rcheevos_ralboard_t* rcheevos_find_lboard(unsigned id) { rcheevos_ralboard_t* lboard = rcheevos_locals.patchdata.lboards; @@ -979,44 +537,31 @@ static rcheevos_ralboard_t* rcheevos_find_lboard(unsigned id) return NULL; } -static void rcheevos_lboard_submit(rcheevos_locals_t *locals, +static void rcheevos_lboard_submit(rcheevos_locals_t* locals, rcheevos_ralboard_t* lboard, int value, bool widgets_ready) { char buffer[256]; char formatted_value[16]; - /* Show the OSD message (regardless of notifications setting). */ rc_runtime_format_lboard_value(formatted_value, sizeof(formatted_value), value, lboard->format); - CHEEVOS_LOG(RCHEEVOS_TAG "Submitting %s for leaderboard %u\n", formatted_value, lboard->id); + + /* Show the on-screen message (regardless of notifications setting). */ snprintf(buffer, sizeof(buffer), "Submitted %s for %s", - formatted_value, lboard->title); + formatted_value, lboard->title); runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); #if defined(HAVE_GFX_WIDGETS) /* Hide the tracker */ - if (widgets_ready) + if (gfx_widgets_ready()) gfx_widgets_set_leaderboard_display(lboard->id, NULL); #endif - /* Start the submit task. */ - { - rcheevos_async_io_request - *request = (rcheevos_async_io_request*) - calloc(1, sizeof(rcheevos_async_io_request)); - - request->type = CHEEVOS_ASYNC_SUBMIT_LBOARD; - request->id = lboard->id; - request->value = value; - request->success_message = "Submitted leaderboard"; - request->failure_message = "Error submitting leaderboard"; - rcheevos_get_user_agent(locals, - request->user_agent, sizeof(request->user_agent)); - rcheevos_async_submit_lboard(locals, request); - } + /* Start the submit task */ + rcheevos_client_submit_lboard_entry(lboard->id, value); } static void rcheevos_lboard_canceled(rcheevos_ralboard_t * lboard, @@ -1988,7 +1533,7 @@ static int rcheevos_iterate(rcheevos_coro_t* coro) CORO_GOSUB(RCHEEVOS_LOGIN); - ret = rc_url_get_patch(coro->url, sizeof(coro->url), coro->settings->arrays.cheevos_username, rcheevos_locals.token, coro->gameid); + ret = rc_url_get_patch(coro->url, sizeof(coro->url), rcheevos_locals.username, rcheevos_locals.token, coro->gameid); if (ret < 0) { @@ -2206,7 +1751,9 @@ static int rcheevos_iterate(rcheevos_coro_t* coro) CORO_STOP(); } - ret = rcheevos_get_token(coro->json, tok, sizeof(tok)); + ret = rcheevos_get_token(coro->json, + rcheevos_locals.username, sizeof(rcheevos_locals.username), + tok, sizeof(tok)); if (ret != 0) { @@ -2229,12 +1776,12 @@ static int rcheevos_iterate(rcheevos_coro_t* coro) char msg[256]; snprintf(msg, sizeof(msg), "RetroAchievements: Logged in as \"%s\".", - coro->settings->arrays.cheevos_username); + rcheevos_locals.username); msg[sizeof(msg) - 1] = 0; runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } - CHEEVOS_LOG(RCHEEVOS_TAG "logged in successfully\n"); + CHEEVOS_LOG(RCHEEVOS_TAG "%s logged in successfully\n", rcheevos_locals.username); strlcpy(rcheevos_locals.token, tok, sizeof(rcheevos_locals.token)); @@ -2362,7 +1909,7 @@ static int rcheevos_iterate(rcheevos_coro_t* coro) for (coro->i = 0; coro->i < 2; coro->i++) { ret = rc_url_get_unlock_list(coro->url, sizeof(coro->url), - coro->settings->arrays.cheevos_username, + rcheevos_locals.username, rcheevos_locals.token, coro->gameid, coro->i); if (ret < 0) @@ -2397,7 +1944,7 @@ static int rcheevos_iterate(rcheevos_coro_t* coro) { int ret = rc_url_post_playing(coro->url, sizeof(coro->url), - coro->settings->arrays.cheevos_username, + rcheevos_locals.username, rcheevos_locals.token, coro->gameid); if (ret < 0) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c new file mode 100644 index 0000000000..4881176b29 --- /dev/null +++ b/cheevos/cheevos_client.c @@ -0,0 +1,661 @@ +/* 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 "../paths.h" +#include "../version.h" + +#include +#include + +#include "../frontend/frontend_driver.h" +#include "../network/net_http_special.h" +#include "../tasks/tasks_internal.h" + +#ifdef HAVE_DISCORD +#include "../network/discord.h" +#endif + +#include "../deps/rcheevos/include/rc_api_runtime.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 + + +/* 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 +}; + +typedef void (*rcheevos_async_handler)(int id, + 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; + int attempt_count; + const char* success_message; + const char* failure_message; + const char* user_agent; + char type; +} rcheevos_async_io_request; + + +/**************************** + * 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); + + +/**************************** + * 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 (ptr - buffer); +} + +void rcheevos_get_user_agent(rcheevos_locals_t *locals, + char *buffer, size_t len) +{ + struct retro_system_info *system = runloop_get_libretro_system_info(); + char* ptr; + + /* 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(); + int major, minor; + char tmp[64]; + + if (frontend && frontend->get_os) + { + 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 (system && !string_is_empty(system->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, system->library_name); + } + + if (system->library_version) + { + *ptr++ = '/'; + ptr += append_no_spaces(ptr, stop, system->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_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 */ + 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); +} + +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 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; + char buffer[224]; + + if (error) + { + /* there was a communication error */ + if (request->type == CHEEVOS_ASYNC_RICHPRESENCE && request->attempt_count > 0) + { + /* only retry the ping once (in case of network hiccup), otherwise let + * the timer handle it after the normal ping period has elapsed */ + CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s\n", request->failure_message, + request->id, error); + } + else + { + /* automatically retry the request */ + rcheevos_async_retry_request_after_delay(request, error); + } + return; + } + + if (!data) + { + /* Server did not return HTTP headers */ + strlcpy(buffer, "Server communication error", sizeof(buffer)); + } + else if (!data->data || !data->len) + { + if (data->status != 200) + { + /* Server returned an error via status code. */ + snprintf(buffer, sizeof(buffer), "HTTP error code %d", data->status); + } + else + { + /* Server sent an empty response without an error status code */ + strlcpy(buffer, "No response from server", sizeof(buffer)); + } + } + else + { + buffer[0] = '\0'; /* indicate success unless handler provides error */ + + /* Call appropriate handler to process the response */ + if (request->handler) + { + /* 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 */ + request->handler(request->id, data, buffer, sizeof(buffer)); + } + } + + if (!buffer[0]) + { + /* success */ + if (request->success_message) + { + CHEEVOS_LOG(RCHEEVOS_TAG "%s %u\n", request->success_message, request->id); + } + } + else + { + /* encountered an error */ + char errbuf[256]; + snprintf(errbuf, sizeof(errbuf), "%s %u: %s", + request->failure_message, request->id, buffer); + CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", errbuf); + + switch (request->type) + { + case CHEEVOS_ASYNC_RICHPRESENCE: + /* Don't bother informing user when + * rich presence update fails */ + break; + + default: + runloop_msg_queue_push(errbuf, 0, 5 * 60, false, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); + break; + } + } + + rc_api_destroy_request(&request->request); + + /* 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, + rcheevos_async_handler handler, char type, int id, + const char* success_message, const char* failure_message) +{ + 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); + + 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); +} + +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; +} + + +/**************************** + * ping * + ****************************/ + +static retro_time_t rcheevos_client_prepare_ping(rcheevos_async_io_request* request) +{ + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + const settings_t *settings = config_get_ptr(); + const bool cheevos_richpresence_enable = + settings->bools.cheevos_richpresence_enable; + rc_api_ping_request_t api_params; + 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_DISCORD + if (settings->bools.discord_enable && discord_is_ready()) + discord_update(DISCORD_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->patchdata.game_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 */ + 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); +} + + +/**************************** + * start session * + ****************************/ + +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)); + + /* force non-HTTPS until everything uses RAPI */ + rc_api_set_host("http://retroachievements.org"); + + /* 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; + + task_queue_push(task); + } + } +} + + +/**************************** + * award achievement * + ****************************/ + +static void rcheevos_async_award_achievement_callback(int id, + 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 (api_response.awarded_achievement_id != 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", + id, api_response.response.error_message); + } + } + + 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; + + 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->hash; + + rc_api_init_award_achievement_request(&request->request, &api_params); + + rcheevos_async_begin_request(request, + 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(int id, + 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); + + if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size)) + { + /* not currently doing anything with the response */ + } + + 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; + + 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->hash; + + rc_api_init_submit_lboard_entry_request(&request->request, &api_params); + + rcheevos_async_begin_request(request, + rcheevos_async_submit_lboard_entry_callback, + CHEEVOS_ASYNC_SUBMIT_LBOARD, leaderboard_id, + "Submitted leaderboard", + "Error submitting leaderboard"); + } +} + diff --git a/cheevos/cheevos_client.h b/cheevos/cheevos_client.h new file mode 100644 index 0000000000..5d168a581c --- /dev/null +++ b/cheevos/cheevos_client.h @@ -0,0 +1,32 @@ +/* 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 . + */ + +#ifndef __RARCH_CHEEVOS_CLIENT_H +#define __RARCH_CHEEVOS_CLIENT_H + +#include "cheevos_locals.h" + +RETRO_BEGIN_DECLS + +void rcheevos_client_start_session(unsigned game_id); +void rcheevos_client_award_achievement(unsigned achievement_id); +void rcheevos_client_submit_lboard_entry(unsigned leaderboard_id, int value); + +void rcheevos_log_url(const char* api, const char* url); +void rcheevos_get_user_agent(rcheevos_locals_t *locals, char *buffer, size_t len); + +RETRO_END_DECLS + +#endif /* __RARCH_CHEEVOS_MENU_H */ diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 5af6fbb464..a42e80ff3c 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -133,9 +133,11 @@ typedef struct rcheevos_locals_t enum event_command queued_command; /* action queued by background thread to be run on main thread */ #endif + char username[32]; /* case-corrected username */ char token[32]; /* user's session token */ char hash[33]; /* retroachievements hash for current content */ char user_agent_prefix[128]; /* RetroArch/OS version information */ + char user_agent_core[256]; /* RetroArch/OS/Core version information */ #ifdef HAVE_MENU rcheevos_menuitem_t* menuitems; /* array of items for the achievements quick menu */ diff --git a/cheevos/cheevos_parser.c b/cheevos/cheevos_parser.c index cc8374485c..4928a2f7fd 100644 --- a/cheevos/cheevos_parser.c +++ b/cheevos/cheevos_parser.c @@ -23,6 +23,7 @@ #define CHEEVOS_JSON_KEY_BADGENAME 0x887685d9U #define CHEEVOS_JSON_KEY_CONSOLE_ID 0x071656e5U #define CHEEVOS_JSON_KEY_TOKEN 0x0e2dbd26U +#define CHEEVOS_JSON_KEY_USERNAME 0x7c8da264U #define CHEEVOS_JSON_KEY_FLAGS 0x0d2e96b2U #define CHEEVOS_JSON_KEY_LEADERBOARDS 0xf1247d2dU #define CHEEVOS_JSON_KEY_RICHPRESENCE 0xf18dd230U @@ -149,14 +150,16 @@ static int rcheevos_get_value(const char* json, unsigned key_hash, Returns the token or the error message *****************************************************************************/ -int rcheevos_get_token(const char* json, char* token, size_t length) +int rcheevos_get_token(const char* json, char* username, size_t username_length, + char* token, size_t length) { rcheevos_get_value(json, CHEEVOS_JSON_KEY_ERROR, token, length); if (!string_is_empty(token)) return -1; - return rcheevos_get_value(json, CHEEVOS_JSON_KEY_TOKEN, token, length); + return rcheevos_get_value(json, CHEEVOS_JSON_KEY_TOKEN, token, length) + + rcheevos_get_value(json, CHEEVOS_JSON_KEY_USERNAME, username, username_length); } int rcheevos_get_json_error(const char* json, char* token, size_t length) diff --git a/cheevos/cheevos_parser.h b/cheevos/cheevos_parser.h index 6bec0063f2..aa35c6077a 100644 --- a/cheevos/cheevos_parser.h +++ b/cheevos/cheevos_parser.h @@ -23,7 +23,9 @@ RETRO_BEGIN_DECLS typedef void (*rcheevos_unlock_cb_t)(unsigned id, void* userdata); int rcheevos_get_json_error(const char* json, char* token, size_t length); -int rcheevos_get_token(const char* json, char* token, size_t length); + +int rcheevos_get_token(const char* json, char* username, size_t username_length, + char* token, size_t length); int rcheevos_get_patchdata(const char* json, rcheevos_rapatchdata_t* patchdata); void rcheevos_free_patchdata(rcheevos_rapatchdata_t* patchdata); diff --git a/griffin/griffin.c b/griffin/griffin.c index 0b880cba5c..5d295cf2e3 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -191,9 +191,13 @@ ACHIEVEMENTS #include "../network/net_http_special.c" #include "../cheevos/cheevos.c" +#include "../cheevos/cheevos_client.c" #include "../cheevos/cheevos_menu.c" #include "../cheevos/cheevos_parser.c" +#include "../deps/rcheevos/src/rapi/rc_api_common.c" +#include "../deps/rcheevos/src/rapi/rc_api_runtime.c" +#include "../deps/rcheevos/src/rapi/rc_api_user.c" #include "../deps/rcheevos/src/rcheevos/alloc.c" #include "../deps/rcheevos/src/rcheevos/compat.c" #include "../deps/rcheevos/src/rcheevos/condition.c" diff --git a/libretro-common/include/net/net_http.h b/libretro-common/include/net/net_http.h index ded3b091a0..4b376653c3 100644 --- a/libretro-common/include/net/net_http.h +++ b/libretro-common/include/net/net_http.h @@ -46,6 +46,8 @@ void net_http_connection_set_user_agent(struct http_connection_t* conn, const ch const char *net_http_connection_url(struct http_connection_t *conn); +const char* net_http_connection_method(struct http_connection_t* conn); + struct http_t *net_http_new(struct http_connection_t *conn); /* You can use this to call net_http_update diff --git a/libretro-common/net/net_http.c b/libretro-common/net/net_http.c index 33ac59dc2d..7cf8e527f3 100644 --- a/libretro-common/net/net_http.c +++ b/libretro-common/net/net_http.c @@ -682,6 +682,11 @@ const char *net_http_connection_url(struct http_connection_t *conn) return conn->urlcopy; } +const char* net_http_connection_method(struct http_connection_t* conn) +{ + return conn->methodcopy; +} + struct http_t *net_http_new(struct http_connection_t *conn) { bool error = false; diff --git a/tasks/task_http.c b/tasks/task_http.c index eac88020ed..f1040bf48f 100644 --- a/tasks/task_http.c +++ b/tasks/task_http.c @@ -280,25 +280,36 @@ static void* task_push_http_transfer_generic( const char *url, bool mute, const char *type, retro_task_callback_t cb, void *user_data) { - task_finder_data_t find_data; retro_task_t *t = NULL; http_handle_t *http = NULL; - - find_data.func = task_http_finder; - find_data.userdata = (void*)url; - - /* Concurrent download of the same file is not allowed */ - if (task_queue_find(&find_data)) - { - if (conn) - net_http_connection_free(conn); - - return NULL; - } + const char *method = NULL; if (!conn) return NULL; + method = net_http_connection_method(conn); + if (method && (method[0] == 'P' || method[0] == 'p')) + { + /* POST requests usually mutate the server, so assume multiple calls are + * intended, even if they're duplicated. Additionally, they may differ + * only by the POST data, and task_http_finder doesn't look at that, so + * unique requests could be misclassified as duplicates. + */ + } + else + { + task_finder_data_t find_data; + find_data.func = task_http_finder; + find_data.userdata = (void*)url; + + /* Concurrent download of the same file is not allowed */ + if (task_queue_find(&find_data)) + { + net_http_connection_free(conn); + return NULL; + } + } + http = (http_handle_t*)malloc(sizeof(*http)); if (!http)