From f556eba0a32157aab284c1befc20da2ac780295f Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 7 Jul 2021 19:59:57 -0600 Subject: [PATCH 1/8] move async API code to cheevos_client.c --- Makefile.common | 4 + cheevos/cheevos.c | 565 +----------------------------------- cheevos/cheevos_client.c | 612 +++++++++++++++++++++++++++++++++++++++ cheevos/cheevos_client.h | 42 +++ 4 files changed, 662 insertions(+), 561 deletions(-) create mode 100644 cheevos/cheevos_client.c create mode 100644 cheevos/cheevos_client.h diff --git a/Makefile.common b/Makefile.common index da5bf14f9e..5f4654ba33 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2111,6 +2111,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 \ @@ -2130,6 +2131,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..11daa346d6 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,6 +50,7 @@ #endif #include "cheevos.h" +#include "cheevos_client.h" #include "cheevos_locals.h" #include "cheevos_parser.h" @@ -89,42 +86,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 */ @@ -167,10 +134,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 +147,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,207 +249,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 +410,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_start_session(locals->patchdata.game_id); /* validate the memrefs */ if (rcheevos_locals.memory.count != 0) @@ -858,112 +446,7 @@ static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id) return NULL; } -static void rcheevos_award_achievement(rcheevos_locals_t *locals, - rcheevos_racheevo_t* cheevo, bool widgets_ready) -{ - char buffer[256] = ""; - if (!cheevo) - return; - - CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n", - cheevo->id, cheevo->title, cheevo->description); - - /* Deactivates the cheevo. */ - rc_runtime_deactivate_achievement(&locals->runtime, cheevo->id); - - cheevo->active &= ~RCHEEVOS_ACTIVE_SOFTCORE; - if (locals->hardcore_active) - cheevo->active &= ~RCHEEVOS_ACTIVE_HARDCORE; - - cheevo->unlock_time = cpu_features_get_time_usec(); - - /* Show the OSD message. */ - { -#if defined(HAVE_GFX_WIDGETS) - 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); - } - } - - /* 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); - } - -#ifdef HAVE_SCREENSHOTS - { - 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) - return; - - snprintf(shotname, shotname_len, "%s/%s-cheevo-%u", - settings->paths.directory_screenshot, - path_basename(path_get(RARCH_PATH_BASENAME)), - cheevo->id); - shotname[shotname_len - 1] = '\0'; - - if (take_screenshot(settings->paths.directory_screenshot, - shotname, true, - video_driver_cached_frame_has_valid_framebuffer(), - false, true)) - 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", - 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) { @@ -979,46 +462,6 @@ static rcheevos_ralboard_t* rcheevos_find_lboard(unsigned id) return NULL; } -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); - snprintf(buffer, sizeof(buffer), "Submitted %s for %s", - 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) - 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); - } -} - static void rcheevos_lboard_canceled(rcheevos_ralboard_t * lboard, bool widgets_ready) { @@ -1506,7 +949,7 @@ static void rcheevos_runtime_event_handler(const rc_runtime_event_t* runtime_eve #endif case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: - rcheevos_award_achievement(&rcheevos_locals, rcheevos_find_cheevo(runtime_event->id), widgets_ready); + rcheevos_award_achievement(rcheevos_find_cheevo(runtime_event->id)); break; case RC_RUNTIME_EVENT_LBOARD_STARTED: @@ -1519,7 +962,7 @@ static void rcheevos_runtime_event_handler(const rc_runtime_event_t* runtime_eve break; case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: - rcheevos_lboard_submit(&rcheevos_locals, rcheevos_find_lboard(runtime_event->id), runtime_event->value, widgets_ready); + rcheevos_lboard_submit(rcheevos_find_lboard(runtime_event->id), runtime_event->value); break; case RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED: diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c new file mode 100644 index 0000000000..79bf2d3610 --- /dev/null +++ b/cheevos/cheevos_client.c @@ -0,0 +1,612 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2021-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 "../frontend/frontend_driver.h" +#include "../network/net_http_special.h" +#include "../tasks/tasks_internal.h" + +#ifdef HAVE_GFX_WIDGETS +#include "../gfx/gfx_widgets.h" +#endif + +#ifdef HAVE_DISCORD +#include "../network/discord.h" +#endif + +#include "../deps/rcheevos/include/rc_url.h" + + + +/* 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; + +/* forward declarations */ +static void rcheevos_async_schedule( + rcheevos_async_io_request* request, retro_time_t delay); +static void rcheevos_async_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 +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 + +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 +} + +/* start session */ + +void rcheevos_start_session(unsigned game_id) +{ + /* 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 = game_id; + request->type = CHEEVOS_ASYNC_RICHPRESENCE; + rcheevos_async_schedule(request, CHEEVOS_PING_FREQUENCY / 4); + } +} + +/* ping */ + +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; +} + +/* award achievement */ + +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 +} + +void rcheevos_award_achievement(rcheevos_racheevo_t* cheevo) +{ + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + + if (!cheevo) + return; + + CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n", + cheevo->id, cheevo->title, cheevo->description); + + /* Deactivates the achivement. */ + rc_runtime_deactivate_achievement(&rcheevos_locals->runtime, cheevo->id); + + cheevo->active &= ~RCHEEVOS_ACTIVE_SOFTCORE; + if (rcheevos_locals->hardcore_active) + cheevo->active &= ~RCHEEVOS_ACTIVE_HARDCORE; + + cheevo->unlock_time = cpu_features_get_time_usec(); + + /* Show the on screen message. */ +#if defined(HAVE_GFX_WIDGETS) + if (gfx_widgets_ready()) + { + gfx_widgets_push_achievement(cheevo->title, cheevo->badge); + } + else +#endif + { + 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 = rcheevos_locals->hardcore_active ? 1 : 0; + request->success_message = "Awarded achievement"; + request->failure_message = "Error awarding achievement"; + rcheevos_get_user_agent(rcheevos_locals, + request->user_agent, sizeof(request->user_agent)); + rcheevos_async_award_achievement(rcheevos_locals, request); + } + +#ifdef HAVE_SCREENSHOTS + { + 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) + return; + + snprintf(shotname, shotname_len, "%s/%s-cheevo-%u", + settings->paths.directory_screenshot, + path_basename(path_get(RARCH_PATH_BASENAME)), + cheevo->id); + shotname[shotname_len - 1] = '\0'; + + if (take_screenshot(settings->paths.directory_screenshot, + shotname, true, + video_driver_cached_frame_has_valid_framebuffer(), + false, true)) + 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", + cheevo->id); + free(shotname); + } + } +#endif +} + +/* submit leaderboard */ + +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); +} + +void rcheevos_lboard_submit(rcheevos_ralboard_t* lboard, int value) +{ + 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); + snprintf(buffer, sizeof(buffer), "Submitted %s for %s", + 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 (gfx_widgets_ready()) + gfx_widgets_set_leaderboard_display(lboard->id, NULL); +#endif + + /* Start the submit task. */ + { + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + 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(rcheevos_locals, + request->user_agent, sizeof(request->user_agent)); + rcheevos_async_submit_lboard(rcheevos_locals, request); + } +} + +/* dispatch */ + +static void rcheevos_async_task_handler(retro_task_t* task) +{ + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + 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); + } +} + + diff --git a/cheevos/cheevos_client.h b/cheevos/cheevos_client.h new file mode 100644 index 0000000000..535f112bb4 --- /dev/null +++ b/cheevos/cheevos_client.h @@ -0,0 +1,42 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2015-2018 - Andre Leiradella + * 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" + + +/* 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 + + +RETRO_BEGIN_DECLS + +void rcheevos_start_session(unsigned game_id); +void rcheevos_award_achievement(rcheevos_racheevo_t* cheevo); +void rcheevos_lboard_submit(rcheevos_ralboard_t* lboard, 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 */ From 7aca71e6253afb4466485a16a2662a9a9f6b0a49 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 7 Jul 2021 21:18:48 -0600 Subject: [PATCH 2/8] convert to rapi --- cheevos/cheevos.c | 117 +++++- cheevos/cheevos_client.c | 749 ++++++++++++++++++++------------------- cheevos/cheevos_client.h | 15 +- cheevos/cheevos_locals.h | 1 + 4 files changed, 506 insertions(+), 376 deletions(-) diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index 11daa346d6..eb4114f8d4 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -57,17 +57,13 @@ #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" @@ -105,6 +101,7 @@ static rcheevos_locals_t rcheevos_locals = {0}, /* token */ "N/A",/* hash */ "", /* user_agent_prefix */ + "", /* user_agent_core */ #ifdef HAVE_MENU NULL, /* menuitems */ 0, /* menuitem_capacity */ @@ -249,7 +246,6 @@ static unsigned rcheevos_peek(unsigned address, unsigned num_bytes, void* ud) return 0; } - static void rcheevos_activate_achievements(rcheevos_locals_t *locals, rcheevos_racheevo_t* cheevo, unsigned count, unsigned flags) { @@ -410,7 +406,7 @@ static int rcheevos_parse(rcheevos_locals_t *locals, const char* json) } } - rcheevos_start_session(locals->patchdata.game_id); + rcheevos_client_start_session(locals->patchdata.game_id); /* validate the memrefs */ if (rcheevos_locals.memory.count != 0) @@ -446,7 +442,85 @@ static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id) return NULL; } +void rcheevos_award_achievement(rcheevos_locals_t* locals, + rcheevos_racheevo_t* cheevo, bool widgets_ready) +{ + const settings_t *settings = config_get_ptr(); + if (!cheevo) + return; + + CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n", + cheevo->id, cheevo->title, cheevo->description); + + /* Deactivates the acheivement. */ + rc_runtime_deactivate_achievement(&locals->runtime, cheevo->id); + + cheevo->active &= ~RCHEEVOS_ACTIVE_SOFTCORE; + if (locals->hardcore_active) + cheevo->active &= ~RCHEEVOS_ACTIVE_HARDCORE; + + cheevo->unlock_time = cpu_features_get_time_usec(); + + /* Show the on screen message. */ +#if defined(HAVE_GFX_WIDGETS) + if (widgets_ready) + { + gfx_widgets_push_achievement(cheevo->title, cheevo->badge); + } + else +#endif + { + 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_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) + { + size_t shotname_len = sizeof(char) * 8192; + char *shotname = (char*)malloc(shotname_len); + + if (shotname) + { + snprintf(shotname, shotname_len, "%s/%s-cheevo-%u", + settings->paths.directory_screenshot, + path_basename(path_get(RARCH_PATH_BASENAME)), + cheevo->id); + shotname[shotname_len - 1] = '\0'; + + if (take_screenshot(settings->paths.directory_screenshot, + shotname, true, + video_driver_cached_frame_has_valid_framebuffer(), + false, true)) + 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", + cheevo->id); + + free(shotname); + } + } +#endif +} static rcheevos_ralboard_t* rcheevos_find_lboard(unsigned id) { @@ -462,6 +536,33 @@ static rcheevos_ralboard_t* rcheevos_find_lboard(unsigned id) return NULL; } +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]; + + 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); + 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 (gfx_widgets_ready()) + gfx_widgets_set_leaderboard_display(lboard->id, NULL); +#endif + + /* Start the submit task */ + rcheevos_client_submit_lboard_entry(lboard->id, value); +} + static void rcheevos_lboard_canceled(rcheevos_ralboard_t * lboard, bool widgets_ready) { @@ -949,7 +1050,7 @@ static void rcheevos_runtime_event_handler(const rc_runtime_event_t* runtime_eve #endif case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: - rcheevos_award_achievement(rcheevos_find_cheevo(runtime_event->id)); + rcheevos_award_achievement(&rcheevos_locals, rcheevos_find_cheevo(runtime_event->id), widgets_ready); break; case RC_RUNTIME_EVENT_LBOARD_STARTED: @@ -962,7 +1063,7 @@ static void rcheevos_runtime_event_handler(const rc_runtime_event_t* runtime_eve break; case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: - rcheevos_lboard_submit(rcheevos_find_lboard(runtime_event->id), runtime_event->value); + rcheevos_lboard_submit(&rcheevos_locals, rcheevos_find_lboard(runtime_event->id), runtime_event->value, widgets_ready); break; case RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED: diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 79bf2d3610..1927f834d5 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -21,27 +21,38 @@ #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_GFX_WIDGETS -#include "../gfx/gfx_widgets.h" -#endif - #ifdef HAVE_DISCORD #include "../network/discord.h" #endif -#include "../deps/rcheevos/include/rc_url.h" +#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, @@ -49,25 +60,35 @@ enum rcheevos_async_io_type 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; - int id; - int value; - int attempt_count; - char user_agent[256]; + const char* user_agent; char type; - char hardcore; } rcheevos_async_io_request; -/* forward declarations */ -static void rcheevos_async_schedule( - rcheevos_async_io_request* request, retro_time_t delay); -static void rcheevos_async_task_callback( + +/**************************** + * 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 */ + +/**************************** + * user agent construction * + ****************************/ static int append_no_spaces(char* buffer, char* stop, const char* text) { @@ -90,14 +111,14 @@ static int append_no_spaces(char* buffer, char* stop, const char* text) return (ptr - buffer); } -void rcheevos_get_user_agent( - rcheevos_locals_t *locals, +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 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(); @@ -149,6 +170,7 @@ void rcheevos_get_user_agent( } #ifdef CHEEVOS_LOG_URLS +#ifndef CHEEVOS_LOG_PASSWORD static void rcheevos_filter_url_param(char* url, char* param) { char *next; @@ -182,36 +204,34 @@ static void rcheevos_filter_url_param(char* url, char* param) } 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 "%s: %s\n", api, url); -#else + #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 "%s: %s\n", api, copy); -#endif + CHEEVOS_LOG(RCHEEVOS_TAG "GET %s\n", copy); + #endif #else (void)api; (void)url; #endif } -static void rcheevos_log_post_url( - const char* api, - const char* url, - const char* post) +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 "%s: %s&%s\n", api, url, post); + CHEEVOS_LOG(RCHEEVOS_TAG "POST %s %s\n", url, post); else - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url); + CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url); #else if (post && post[0]) { @@ -221,68 +241,286 @@ static void rcheevos_log_post_url( rcheevos_filter_url_param(post_copy, "t"); if (post_copy[0]) - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s&%s\n", api, url, post_copy); + CHEEVOS_LOG(RCHEEVOS_TAG "POST %s %s\n", url, post_copy); else - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url); + CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url); } else { - CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url); + CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url); } #endif #else - (void)api; (void)url; (void)post; #endif } -/* start session */ -void rcheevos_start_session(unsigned game_id) +/**************************** + * dispatch * + ****************************/ + +static void rcheevos_async_dispatch_scheduled_request(retro_task_t* task) { - /* schedule the first rich presence call in 30 seconds */ + rcheevos_async_io_request* request = (rcheevos_async_io_request*) + task->user_data; + + switch (request->type) { - rcheevos_async_io_request* request = (rcheevos_async_io_request*) - calloc(1, sizeof(rcheevos_async_io_request)); - request->id = game_id; - request->type = CHEEVOS_ASYNC_RICHPRESENCE; - rcheevos_async_schedule(request, CHEEVOS_PING_FREQUENCY / 4); + case CHEEVOS_ASYNC_RICHPRESENCE: + { + 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 for the next game */ + task_set_finished(task, 1); + free(request); + return; + } + + if (request->attempt_count > 0) + { + /* this is a retry attempt, discard the previous request and + * generate a new one. mark the retry task as completed - the + * periodic task still exists */ + rc_api_destroy_request(&request->request); + rcheevos_client_prepare_ping(request); + task_set_finished(task, 1); + } + else + { + /* update the request and set the task to fire again in + * two minutes */ + task->when = rcheevos_client_prepare_ping(request); + } + break; + } + + default: + /* this is a retry attempt, mark the task as completed */ + task_set_finished(task, 1); + break; } + + /* 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); } -/* ping */ - -static retro_time_t rcheevos_async_send_rich_presence( - rcheevos_locals_t *locals, - rcheevos_async_io_request* request) +static void rcheevos_async_schedule( + rcheevos_async_io_request* request, retro_time_t delay) { - 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; + retro_task_t* task = task_init(); + task->when = cpu_features_get_time_usec() + delay; + task->handler = rcheevos_async_dispatch_scheduled_request; + task->user_data = request; + task->progress = -1; + task_queue_push(task); +} - if (cheevos_richpresence_enable) - rcheevos_get_richpresence(buffer, sizeof(buffer)); +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]; - ret = rc_url_ping(url, sizeof(url), post_data, sizeof(post_data), - cheevos_username, locals->token, locals->patchdata.game_id, buffer); - - if (ret < 0) + /* if there was a communication error, automatically retry the request */ + if (error) { - CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); + /* 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); + 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 { - rcheevos_log_post_url("rc_url_ping", url, post_data); + buffer[0] = '\0'; /* indicate success unless handler provides error */ - 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); + /* Call appropriate handler to process the response */ + if (request->handler) + { + /* NOTE: data->data is not null-terminated. Assume 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->type = type; + request->handler = handler; + 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; +} + + +/**************************** + * 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)); + + /* 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 + { + request->id = game_id; + request->type = CHEEVOS_ASYNC_RICHPRESENCE; + request->user_agent = rcheevos_locals->user_agent_core; + request->failure_message = "Error sending ping"; + + rcheevos_async_schedule(request, CHEEVOS_PING_FREQUENCY / 4); + } + } +} + + +/**************************** + * 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(); + rc_api_ping_request_t api_params; + char buffer[256] = ""; + const bool cheevos_richpresence_enable = + settings->bools.cheevos_richpresence_enable; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = settings->arrays.cheevos_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); @@ -296,317 +534,116 @@ static retro_time_t rcheevos_async_send_rich_presence( return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY * 2; } -/* award achievement */ -static void rcheevos_async_award_achievement( - rcheevos_locals_t *locals, - rcheevos_async_io_request* request) +/**************************** + * award achievement * + ****************************/ + +static void rcheevos_async_award_achievement_callback(int id, + http_transfer_data_t *data, char buffer[], size_t buffer_size) { - 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); + rc_api_award_achievement_response_t api_response; - if (ret != 0) + int result = rc_api_process_award_achievement_response(&api_response, data->data); + if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size)) { - 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 -} - -void rcheevos_award_achievement(rcheevos_racheevo_t* cheevo) -{ - rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); - - if (!cheevo) - return; - - CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n", - cheevo->id, cheevo->title, cheevo->description); - - /* Deactivates the achivement. */ - rc_runtime_deactivate_achievement(&rcheevos_locals->runtime, cheevo->id); - - cheevo->active &= ~RCHEEVOS_ACTIVE_SOFTCORE; - if (rcheevos_locals->hardcore_active) - cheevo->active &= ~RCHEEVOS_ACTIVE_HARDCORE; - - cheevo->unlock_time = cpu_features_get_time_usec(); - - /* Show the on screen message. */ -#if defined(HAVE_GFX_WIDGETS) - if (gfx_widgets_ready()) - { - gfx_widgets_push_achievement(cheevo->title, cheevo->badge); - } - else -#endif - { - 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 = rcheevos_locals->hardcore_active ? 1 : 0; - request->success_message = "Awarded achievement"; - request->failure_message = "Error awarding achievement"; - rcheevos_get_user_agent(rcheevos_locals, - request->user_agent, sizeof(request->user_agent)); - rcheevos_async_award_achievement(rcheevos_locals, request); - } - -#ifdef HAVE_SCREENSHOTS - { - settings_t *settings = config_get_ptr(); - /* Take a screenshot of the achievement. */ - if (settings && settings->bools.cheevos_auto_screenshot) + if (api_response.awarded_achievement_id != id) { - 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)), - cheevo->id); - shotname[shotname_len - 1] = '\0'; - - if (take_screenshot(settings->paths.directory_screenshot, - shotname, true, - video_driver_cached_frame_has_valid_framebuffer(), - false, true)) - 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", - cheevo->id); - free(shotname); + 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); } } -#endif + + rc_api_destroy_award_achievement_response(&api_response); } -/* submit leaderboard */ - -static void rcheevos_async_submit_lboard(rcheevos_locals_t *locals, - rcheevos_async_io_request* request) +void rcheevos_client_award_achievement(unsigned achievement_id) { - 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); -} - -void rcheevos_lboard_submit(rcheevos_ralboard_t* lboard, int value) -{ - 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); - snprintf(buffer, sizeof(buffer), "Submitted %s for %s", - 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 (gfx_widgets_ready()) - gfx_widgets_set_leaderboard_display(lboard->id, NULL); -#endif - - /* Start the submit task. */ - { - rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); - rcheevos_async_io_request - *request = (rcheevos_async_io_request*) + 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(rcheevos_locals, - request->user_agent, sizeof(request->user_agent)); - rcheevos_async_submit_lboard(rcheevos_locals, request); - } -} - -/* dispatch */ - -static void rcheevos_async_task_handler(retro_task_t* task) -{ - rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); - rcheevos_async_io_request* request = (rcheevos_async_io_request*) - task->user_data; - - switch (request->type) + if (!request) { - 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; + CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate unlock request for achievement %u\n", + achievement_id); } -} - -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) + else { - 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)); - } + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + const settings_t *settings = config_get_ptr(); + rc_api_award_achievement_request_t api_params; - 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); + memset(&api_params, 0, sizeof(api_params)); + api_params.username = settings->arrays.cheevos_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; - switch (request->type) - { - case CHEEVOS_ASYNC_RICHPRESENCE: - /* Don't bother informing user when - * rich presence update fails */ - break; + rc_api_init_award_achievement_request(&request->request, &api_params); - 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); + 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(); + const settings_t *settings = config_get_ptr(); + rc_api_submit_lboard_entry_request_t api_params; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = settings->arrays.cheevos_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 index 535f112bb4..532847a06c 100644 --- a/cheevos/cheevos_client.h +++ b/cheevos/cheevos_client.h @@ -19,20 +19,11 @@ #include "cheevos_locals.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 - - RETRO_BEGIN_DECLS -void rcheevos_start_session(unsigned game_id); -void rcheevos_award_achievement(rcheevos_racheevo_t* cheevo); -void rcheevos_lboard_submit(rcheevos_ralboard_t* lboard, int value); +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); diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 5af6fbb464..2b14479ed3 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -136,6 +136,7 @@ typedef struct rcheevos_locals_t 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 */ From 46ffe5f4b23710ac51a7d9bd09d37daa34849314 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 8 Jul 2021 00:20:00 -0600 Subject: [PATCH 3/8] use case-corrected username for server calls --- cheevos/cheevos.c | 17 ++++++++++------- cheevos/cheevos_client.c | 28 +++++++++++++--------------- cheevos/cheevos_client.h | 1 - cheevos/cheevos_locals.h | 1 + cheevos/cheevos_parser.c | 7 +++++-- cheevos/cheevos_parser.h | 4 +++- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index eb4114f8d4..cd0b7f36c8 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -98,7 +98,8 @@ 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 */ @@ -1532,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) { @@ -1750,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) { @@ -1773,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)); @@ -1906,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) @@ -1941,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 index 1927f834d5..e8cce81678 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -1,5 +1,5 @@ /* RetroArch - A frontend for libretro. - * Copyright (C) 2021-2021 - Brian Weiss + * 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- @@ -371,9 +371,9 @@ static void rcheevos_async_http_task_callback( /* Call appropriate handler to process the response */ if (request->handler) { - /* NOTE: data->data is not null-terminated. Assume response is - * properly formatted or will encounter a parse failure before - * reading past the end of the data */ + /* 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)); } } @@ -422,12 +422,12 @@ 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->type = type; - request->handler = handler; - request->id = id; + request->handler = handler; + request->type = type; + request->id = id; request->success_message = success_message; request->failure_message = failure_message; - request->attempt_count = 0; + request->attempt_count = 0; if (!request->user_agent) request->user_agent = get_rcheevos_locals()->user_agent_core; @@ -501,13 +501,13 @@ static retro_time_t rcheevos_client_prepare_ping(rcheevos_async_io_request* requ { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); const settings_t *settings = config_get_ptr(); - rc_api_ping_request_t api_params; - char buffer[256] = ""; 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 = settings->arrays.cheevos_username; + api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.game_id = request->id; @@ -575,11 +575,10 @@ void rcheevos_client_award_achievement(unsigned achievement_id) else { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); - const settings_t *settings = config_get_ptr(); rc_api_award_achievement_request_t api_params; memset(&api_params, 0, sizeof(api_params)); - api_params.username = settings->arrays.cheevos_username; + 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; @@ -627,11 +626,10 @@ void rcheevos_client_submit_lboard_entry(unsigned leaderboard_id, int value) else { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); - const settings_t *settings = config_get_ptr(); rc_api_submit_lboard_entry_request_t api_params; memset(&api_params, 0, sizeof(api_params)); - api_params.username = settings->arrays.cheevos_username; + api_params.username = rcheevos_locals->username; api_params.api_token = rcheevos_locals->token; api_params.leaderboard_id = leaderboard_id; api_params.score = value; diff --git a/cheevos/cheevos_client.h b/cheevos/cheevos_client.h index 532847a06c..5d168a581c 100644 --- a/cheevos/cheevos_client.h +++ b/cheevos/cheevos_client.h @@ -1,5 +1,4 @@ /* RetroArch - A frontend for libretro. - * Copyright (C) 2015-2018 - Andre Leiradella * Copyright (C) 2019-2021 - Brian Weiss * * RetroArch is free software: you can redistribute it and/or modify it under the terms diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 2b14479ed3..a42e80ff3c 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -133,6 +133,7 @@ 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 */ 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); From bc4a9a3696d0cfb16b01047a2fc883cfacc144ac Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 8 Jul 2021 00:52:19 -0600 Subject: [PATCH 4/8] separate ping task handler from retry task handler --- cheevos/cheevos_client.c | 182 ++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 87 deletions(-) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index e8cce81678..3221681cd1 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -261,63 +261,46 @@ static void rcheevos_log_post_url(const char* url, const char* post) * dispatch * ****************************/ -static void rcheevos_async_dispatch_scheduled_request(retro_task_t* task) +static void rcheevos_async_retry_request(retro_task_t* task) { rcheevos_async_io_request* request = (rcheevos_async_io_request*) task->user_data; - switch (request->type) + if (request->type == CHEEVOS_ASYNC_RICHPRESENCE) { - case CHEEVOS_ASYNC_RICHPRESENCE: - { - 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 for the next game */ - task_set_finished(task, 1); - free(request); - return; - } - - if (request->attempt_count > 0) - { - /* this is a retry attempt, discard the previous request and - * generate a new one. mark the retry task as completed - the - * periodic task still exists */ - rc_api_destroy_request(&request->request); - rcheevos_client_prepare_ping(request); - task_set_finished(task, 1); - } - else - { - /* update the request and set the task to fire again in - * two minutes */ - task->when = rcheevos_client_prepare_ping(request); - } - break; - } - - default: - /* this is a retry attempt, mark the task as completed */ - task_set_finished(task, 1); - break; + /* this is a ping retry. discard the previous request and + * generate a new one. note the periodic task is still + * active for the requet */ + rc_api_destroy_request(&request->request); + rcheevos_client_prepare_ping(request); } - /* start the HTTP request */ + /* 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_schedule( - rcheevos_async_io_request* request, retro_time_t delay) +static void rcheevos_async_retry_request_after_delay(rcheevos_async_io_request* request) { retro_task_t* task = task_init(); - task->when = cpu_features_get_time_usec() + delay; - task->handler = rcheevos_async_dispatch_scheduled_request; + + /* 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); + + 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); } @@ -328,21 +311,13 @@ static void rcheevos_async_http_task_callback( http_transfer_data_t *data = (http_transfer_data_t*)task_data; char buffer[224]; - /* if there was a communication error, automatically retry the request */ if (error) { - /* 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); - + /* there was a communication error. automatically retry the request */ CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s\n", request->failure_message, request->id, error); + + rcheevos_async_retry_request_after_delay(request); return; } @@ -458,41 +433,6 @@ static bool rcheevos_async_succeeded(int result, } -/**************************** - * 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)); - - /* 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 - { - request->id = game_id; - request->type = CHEEVOS_ASYNC_RICHPRESENCE; - request->user_agent = rcheevos_locals->user_agent_core; - request->failure_message = "Error sending ping"; - - rcheevos_async_schedule(request, CHEEVOS_PING_FREQUENCY / 4); - } - } -} - - /**************************** * ping * ****************************/ @@ -534,6 +474,74 @@ static retro_time_t rcheevos_client_prepare_ping(rcheevos_async_io_request* requ 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); + 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)); + + /* 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 * From 41e25902a1eab9a43d3afeee567ec03584976700 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 8 Jul 2021 00:57:35 -0600 Subject: [PATCH 5/8] use non-HTTPS until everything is converted --- cheevos/cheevos_client.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 3221681cd1..2e4a493497 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -514,6 +514,9 @@ void rcheevos_client_start_session(unsigned game_id) 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*) From da8f257323b130343b4632a79d2fe3bcd8f10a38 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 18 Jul 2021 09:45:53 -0600 Subject: [PATCH 6/8] log retry delay when automatically retrying network failure --- cheevos/cheevos_client.c | 31 +++++++++++++++++-------------- griffin/griffin.c | 4 ++++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 2e4a493497..4881176b29 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -266,15 +266,6 @@ static void rcheevos_async_retry_request(retro_task_t* task) rcheevos_async_io_request* request = (rcheevos_async_io_request*) task->user_data; - if (request->type == CHEEVOS_ASYNC_RICHPRESENCE) - { - /* this is a ping retry. discard the previous request and - * generate a new one. note the periodic task is still - * active for the requet */ - rc_api_destroy_request(&request->request); - rcheevos_client_prepare_ping(request); - } - /* the timer task has done its job. let it dispose itself */ task_set_finished(task, 1); @@ -284,7 +275,7 @@ static void rcheevos_async_retry_request(retro_task_t* task) rcheevos_async_http_task_callback, request); } -static void rcheevos_async_retry_request_after_delay(rcheevos_async_io_request* request) +static void rcheevos_async_retry_request_after_delay(rcheevos_async_io_request* request, const char* error) { retro_task_t* task = task_init(); @@ -295,6 +286,9 @@ static void rcheevos_async_retry_request_after_delay(rcheevos_async_io_request* ? (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; @@ -313,11 +307,19 @@ static void rcheevos_async_http_task_callback( if (error) { - /* there was a communication error. automatically retry the request */ - CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s\n", request->failure_message, + /* 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); - - rcheevos_async_retry_request_after_delay(request); + } + else + { + /* automatically retry the request */ + rcheevos_async_retry_request_after_delay(request, error); + } return; } @@ -485,6 +487,7 @@ static void rcheevos_async_ping_handler(retro_task_t* task) /* 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; } diff --git a/griffin/griffin.c b/griffin/griffin.c index a04933717f..9b342331dd 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" From c9ca0489ed84fb03b9a38569e72941687d8fa381 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Fri, 23 Jul 2021 22:04:42 -0600 Subject: [PATCH 7/8] allow concurrent POST requests to same URL --- tasks/task_http.c | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tasks/task_http.c b/tasks/task_http.c index eac88020ed..164ec350d9 100644 --- a/tasks/task_http.c +++ b/tasks/task_http.c @@ -280,25 +280,34 @@ 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; - } - if (!conn) return NULL; + if (conn->methodcopy && (conn->methodcopy[0] == 'P' || conn->methodcopy[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) From 5ac432026ae20a4167851456baa28ab42216d40a Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 27 Jul 2021 21:02:14 -0600 Subject: [PATCH 8/8] add helper function for accessing connection method --- libretro-common/include/net/net_http.h | 2 ++ libretro-common/net/net_http.c | 5 +++++ tasks/task_http.c | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) 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 164ec350d9..f1040bf48f 100644 --- a/tasks/task_http.c +++ b/tasks/task_http.c @@ -282,11 +282,13 @@ static void* task_push_http_transfer_generic( { retro_task_t *t = NULL; http_handle_t *http = NULL; + const char *method = NULL; if (!conn) return NULL; - if (conn->methodcopy && (conn->methodcopy[0] == 'P' || conn->methodcopy[0] == 'p')) + 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