/* RetroArch - A frontend for libretro. * Copyright (C) 2015-2016 - Andre Leiradella * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_CONFIG_H #include "../config.h" #endif #ifdef HAVE_GFX_WIDGETS #include "../gfx/gfx_widgets.h" #endif #ifdef HAVE_THREADS #include #endif #ifdef HAVE_CHEATS #include "../cheat_manager.h" #endif #ifdef HAVE_CHD #include "streams/chd_stream.h" #endif #include "cheevos.h" #include "cheevos_client.h" #include "cheevos_locals.h" #include "cheevos_parser.h" #include "../file_path_special.h" #include "../paths.h" #include "../command.h" #include "../configuration.h" #include "../performance_counters.h" #include "../msg_hash.h" #include "../retroarch.h" #include "../core.h" #include "../core_option_manager.h" #include "../tasks/tasks_internal.h" #include "../deps/rcheevos/include/rc_runtime.h" #include "../deps/rcheevos/include/rc_url.h" #include "../deps/rcheevos/include/rc_hash.h" #include "../deps/rcheevos/src/rcheevos/rc_libretro.h" /* Define this macro to prevent cheevos from being deactivated. */ #undef CHEEVOS_DONT_DEACTIVATE /* Define this macro to load a JSON file from disk instead of downloading * from retroachievements.org. */ #undef CHEEVOS_JSON_OVERRIDE /* Define this macro with a string to save the JSON file to disk with * that name. */ #undef CHEEVOS_SAVE_JSON /* Define this macro to log downloaded badge images. */ #undef CHEEVOS_LOG_BADGES /* Define this macro to capture how long it takes to generate a hash */ #undef CHEEVOS_TIME_HASH static rcheevos_locals_t rcheevos_locals = { {0}, /* runtime */ {0}, /* patchdata */ {{0}},/* memory */ NULL, /* task */ #ifdef HAVE_THREADS NULL, /* task_lock */ CMD_EVENT_NONE, /* queued_command */ #endif "", /* username */ "", /* token */ "N/A",/* hash */ "", /* user_agent_prefix */ "", /* user_agent_core */ #ifdef HAVE_MENU NULL, /* menuitems */ 0, /* menuitem_capacity */ 0, /* menuitem_count */ #endif false,/* hardcore_active */ false,/* loaded */ true, /* core_supports */ false,/* network_error */ false,/* leaderboards_enabled */ false,/* leaderboard_notifications */ false /* leaderboard_trackers */ }; rcheevos_locals_t* get_rcheevos_locals(void) { return &rcheevos_locals; } #ifdef HAVE_THREADS #define CHEEVOS_LOCK(l) do { slock_lock(l); } while (0) #define CHEEVOS_UNLOCK(l) do { slock_unlock(l); } while (0) #else #define CHEEVOS_LOCK(l) #define CHEEVOS_UNLOCK(l) #endif #define CHEEVOS_MB(x) ((x) * 1024 * 1024) /* Forward declaration */ static void rcheevos_validate_memrefs(rcheevos_locals_t* locals); /***************************************************************************** Supporting functions. *****************************************************************************/ #ifndef CHEEVOS_VERBOSE void rcheevos_log(const char *fmt, ...) { (void)fmt; } #endif static void rcheevos_achievement_disabled(rcheevos_racheevo_t* cheevo, unsigned address) { if (!cheevo) return; CHEEVOS_ERR(RCHEEVOS_TAG "Achievement %u disabled (invalid address %06X): %s\n", cheevo->id, address, cheevo->title); CHEEVOS_FREE(cheevo->memaddr); cheevo->memaddr = NULL; } static void rcheevos_lboard_disabled(rcheevos_ralboard_t* lboard, unsigned address) { if (!lboard) return; CHEEVOS_ERR(RCHEEVOS_TAG "Leaderboard %u disabled (invalid address %06X): %s\n", lboard->id, address, lboard->title); CHEEVOS_FREE(lboard->mem); lboard->mem = NULL; } static void rcheevos_handle_log_message(const char* message) { CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message); } static void rcheevos_get_core_memory_info(unsigned id, rc_libretro_core_memory_info_t* info) { retro_ctx_memory_info_t ctx_info; if (!info) return; ctx_info.id = id; if (core_get_memory(&ctx_info)) { info->data = (unsigned char*)ctx_info.data; info->size = ctx_info.size; } else { info->data = NULL; info->size = 0; } } static int rcheevos_init_memory(rcheevos_locals_t* locals) { rarch_system_info_t* system = runloop_get_system_info(); rarch_memory_map_t* mmaps = &system->mmaps; struct retro_memory_descriptor* descriptors; struct retro_memory_map mmap; unsigned i; int result; descriptors = (struct retro_memory_descriptor*)malloc(mmaps->num_descriptors * sizeof(*descriptors)); if (!descriptors) return 0; mmap.descriptors = &descriptors[0]; mmap.num_descriptors = mmaps->num_descriptors; /* RetroArch wraps the retro_memory_descriptor's in rarch_memory_descriptor_t's, pull them back out */ for (i = 0; i < mmap.num_descriptors; ++i) memcpy(&descriptors[i], &mmaps->descriptors[i].core, sizeof(descriptors[0])); rc_libretro_init_verbose_message_callback(rcheevos_handle_log_message); result = rc_libretro_memory_init(&locals->memory, &mmap, rcheevos_get_core_memory_info, locals->patchdata.console_id); free(descriptors); return result; } uint8_t* rcheevos_patch_address(unsigned address) { if (rcheevos_locals.memory.count == 0) { /* memory map was not previously initialized (no achievements for this game?) try now */ rcheevos_init_memory(&rcheevos_locals); } return rc_libretro_memory_find(&rcheevos_locals.memory, address); } static unsigned rcheevos_peek(unsigned address, unsigned num_bytes, void* ud) { uint8_t* data = rc_libretro_memory_find(&rcheevos_locals.memory, address); if (data) { switch (num_bytes) { case 4: return (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | (data[0]); case 3: return (data[2] << 16) | (data[1] << 8) | (data[0]); case 2: return (data[1] << 8) | (data[0]); case 1: return data[0]; } } return 0; } static void rcheevos_activate_achievements(rcheevos_locals_t *locals, rcheevos_racheevo_t* cheevo, unsigned count, unsigned flags) { int res; unsigned i; char buffer[256]; settings_t *settings = config_get_ptr(); for (i = 0; i < count; i++, cheevo++) { res = rc_runtime_activate_achievement(&locals->runtime, cheevo->id, cheevo->memaddr, NULL, 0); if (res < 0) { snprintf(buffer, sizeof(buffer), "Could not activate achievement %d \"%s\": %s", cheevo->id, cheevo->title, rc_error_str(res)); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "%s: mem %s\n", buffer, cheevo->memaddr); CHEEVOS_FREE(cheevo->memaddr); cheevo->memaddr = NULL; continue; } cheevo->active = RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE | flags; } } static int rcheevos_parse(rcheevos_locals_t *locals, const char* json) { char buffer[256]; unsigned j = 0; unsigned count = 0; settings_t *settings = NULL; rcheevos_ralboard_t* lboard = NULL; int res = rcheevos_get_patchdata( json, &locals->patchdata); if (res != 0) { char *ptr = NULL; strcpy_literal(buffer, "Error retrieving achievement data: "); ptr = buffer + strlen(buffer); /* Extract the Error field from the JSON. * If not found, remove the colon from the message. */ if (rcheevos_get_json_error(json, ptr, sizeof(buffer) - (ptr - buffer)) == -1) ptr[-2] = '\0'; runloop_msg_queue_push(buffer, 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); RARCH_ERR(RCHEEVOS_TAG "%s", buffer); return -1; } if ( locals->patchdata.core_count == 0 && locals->patchdata.unofficial_count == 0 && locals->patchdata.lboard_count == 0 && (!locals->patchdata.richpresence_script || !*locals->patchdata.richpresence_script)) { rcheevos_free_patchdata(&locals->patchdata); return 0; } settings = config_get_ptr(); if (!rcheevos_init_memory(locals)) { /* some cores (like Mupen64-Plus) don't expose the * memory until the first call to retro_run. * in that case, there will be a total_size of * memory reported by the core, but init will return * false, as all of the pointers were null. */ /* reset the memory count and we'll re-evaluate in rcheevos_test() */ if (locals->memory.total_size != 0) locals->memory.count = 0; else { CHEEVOS_ERR(RCHEEVOS_TAG "No memory exposed by core.\n"); rcheevos_locals.core_supports = false; if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE), 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); goto error; } } /* Initialize. */ rcheevos_activate_achievements(locals, locals->patchdata.core, locals->patchdata.core_count, 0); if (settings->bools.cheevos_test_unofficial) rcheevos_activate_achievements(locals, locals->patchdata.unofficial, locals->patchdata.unofficial_count, RCHEEVOS_ACTIVE_UNOFFICIAL); if (locals->hardcore_active && locals->leaderboards_enabled) { lboard = locals->patchdata.lboards; count = locals->patchdata.lboard_count; for (j = 0; j < count; j++, lboard++) { res = rc_runtime_activate_lboard(&locals->runtime, lboard->id, lboard->mem, NULL, 0); if (res < 0) { snprintf(buffer, sizeof(buffer), "Could not activate leaderboard %d \"%s\": %s", lboard->id, lboard->title, rc_error_str(res)); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "%s mem: %s\n", buffer, lboard->mem); CHEEVOS_FREE(lboard->mem); lboard->mem = NULL; continue; } } } res = RC_MISSING_DISPLAY_STRING; if ( locals->patchdata.richpresence_script && *locals->patchdata.richpresence_script) { res = rc_runtime_activate_richpresence(&locals->runtime, locals->patchdata.richpresence_script, NULL, 0); if (res < 0) { snprintf(buffer, sizeof(buffer), "Could not activate rich presence: %s", rc_error_str(res)); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "%s\n", buffer); } } rcheevos_client_start_session(locals->patchdata.game_id); /* validate the memrefs */ if (rcheevos_locals.memory.count != 0) rcheevos_validate_memrefs(&rcheevos_locals); return 0; error: rcheevos_free_patchdata(&locals->patchdata); rc_libretro_memory_destroy(&locals->memory); return -1; } static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id) { unsigned i; rcheevos_racheevo_t* cheevo; cheevo = rcheevos_locals.patchdata.core; for (i = 0; i < rcheevos_locals.patchdata.core_count; i++, cheevo++) { if (cheevo->id == id) return cheevo; } cheevo = rcheevos_locals.patchdata.unofficial; for (i = 0; i < rcheevos_locals.patchdata.unofficial_count; i++, cheevo++) { if (cheevo->id == id) return cheevo; } 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) { rcheevos_ralboard_t* lboard = rcheevos_locals.patchdata.lboards; unsigned i; for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard) { if (lboard->id == id) return lboard; } 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) { char buffer[256]; if (!lboard) return; CHEEVOS_LOG(RCHEEVOS_TAG "Leaderboard %u canceled: %s\n", lboard->id, lboard->title); #if defined(HAVE_GFX_WIDGETS) if (widgets_ready) gfx_widgets_set_leaderboard_display(lboard->id, NULL); #endif if (rcheevos_locals.leaderboard_notifications) { snprintf(buffer, sizeof(buffer), "Leaderboard attempt failed: %s", lboard->title); runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } static void rcheevos_lboard_started(rcheevos_ralboard_t * lboard, int value, bool widgets_ready) { char buffer[256]; if (!lboard) return; CHEEVOS_LOG(RCHEEVOS_TAG "Leaderboard %u started: %s\n", lboard->id, lboard->title); #if defined(HAVE_GFX_WIDGETS) if (widgets_ready && rcheevos_locals.leaderboard_trackers) { rc_runtime_format_lboard_value(buffer, sizeof(buffer), value, lboard->format); gfx_widgets_set_leaderboard_display(lboard->id, buffer); } #endif if (rcheevos_locals.leaderboard_notifications) { if (lboard->description && *lboard->description) snprintf(buffer, sizeof(buffer), "Leaderboard attempt started: %s - %s", lboard->title, lboard->description); else snprintf(buffer, sizeof(buffer), "Leaderboard attempt started: %s", 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) static void rcheevos_lboard_updated(rcheevos_ralboard_t* lboard, int value, bool widgets_ready) { if (!lboard) return; if (widgets_ready && rcheevos_locals.leaderboard_trackers) { char buffer[32]; rc_runtime_format_lboard_value(buffer, sizeof(buffer), value, lboard->format); gfx_widgets_set_leaderboard_display(lboard->id, buffer); } } static void rcheevos_challenge_started(rcheevos_racheevo_t* cheevo, int value, bool widgets_ready) { settings_t* settings = config_get_ptr(); if (cheevo && widgets_ready && settings->bools.cheevos_challenge_indicators) gfx_widgets_set_challenge_display(cheevo->id, cheevo->badge); } static void rcheevos_challenge_ended(rcheevos_racheevo_t* cheevo, int value, bool widgets_ready) { if (cheevo && widgets_ready) gfx_widgets_set_challenge_display(cheevo->id, NULL); } #endif int rcheevos_get_richpresence(char buffer[], int buffer_size) { int ret = rc_runtime_get_richpresence(&rcheevos_locals.runtime, buffer, buffer_size, &rcheevos_peek, NULL, NULL); if (ret <= 0 && rcheevos_locals.patchdata.title) ret = snprintf(buffer, buffer_size, "Playing %s", rcheevos_locals.patchdata.title); return ret; } void rcheevos_reset_game(bool widgets_ready) { #if defined(HAVE_GFX_WIDGETS) /* Hide any visible trackers */ if (widgets_ready) { rcheevos_ralboard_t* lboard; rcheevos_racheevo_t* cheevo; unsigned i; lboard = rcheevos_locals.patchdata.lboards; for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard) gfx_widgets_set_leaderboard_display(lboard->id, NULL); cheevo = rcheevos_locals.patchdata.core; for (i = 0; i < rcheevos_locals.patchdata.core_count; ++i, ++cheevo) gfx_widgets_set_challenge_display(cheevo->id, NULL); cheevo = rcheevos_locals.patchdata.unofficial; for (i = 0; i < rcheevos_locals.patchdata.unofficial_count; ++i, ++cheevo) gfx_widgets_set_challenge_display(cheevo->id, NULL); } #endif rc_runtime_reset(&rcheevos_locals.runtime); /* Some cores reallocate memory on reset, * make sure we update our pointers */ if (rcheevos_locals.memory.total_size > 0) rcheevos_init_memory(&rcheevos_locals); } bool rcheevos_hardcore_active(void) { return rcheevos_locals.hardcore_active; } void rcheevos_pause_hardcore(void) { if (rcheevos_locals.hardcore_active) rcheevos_toggle_hardcore_paused(); } bool rcheevos_unload(void) { bool running = false; settings_t* settings = config_get_ptr(); CHEEVOS_LOCK(rcheevos_locals.task_lock); running = rcheevos_locals.task != NULL; CHEEVOS_UNLOCK(rcheevos_locals.task_lock); if (running) { CHEEVOS_LOG(RCHEEVOS_TAG "Asked the load thread to terminate\n"); task_queue_cancel_task(rcheevos_locals.task); #ifdef HAVE_THREADS do { CHEEVOS_LOCK(rcheevos_locals.task_lock); running = rcheevos_locals.task != NULL; CHEEVOS_UNLOCK(rcheevos_locals.task_lock); } while(running); #endif } if (rcheevos_locals.memory.count > 0) rc_libretro_memory_destroy(&rcheevos_locals.memory); if (rcheevos_locals.loaded) { #ifdef HAVE_MENU rcheevos_menu_reset_badges(); if (rcheevos_locals.menuitems) { CHEEVOS_FREE(rcheevos_locals.menuitems); rcheevos_locals.menuitems = NULL; rcheevos_locals.menuitem_capacity = rcheevos_locals.menuitem_count = 0; } #endif rcheevos_free_patchdata(&rcheevos_locals.patchdata); rcheevos_locals.loaded = false; rcheevos_locals.hardcore_active = false; } #ifdef HAVE_THREADS rcheevos_locals.queued_command = CMD_EVENT_NONE; #endif rc_runtime_destroy(&rcheevos_locals.runtime); /* If the config-level token has been cleared, * we need to re-login on loading the next game */ if (!settings->arrays.cheevos_token[0]) rcheevos_locals.token[0] = '\0'; return true; } static void rcheevos_toggle_hardcore_achievements(rcheevos_locals_t *locals, rcheevos_racheevo_t* cheevo, unsigned count) { const unsigned active_mask = RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE; while (count--) { if (cheevo->memaddr && (cheevo->active & active_mask) == RCHEEVOS_ACTIVE_HARDCORE) { /* player has unlocked achievement in non-hardcore, * but has not unlocked in hardcore. Toggle state */ if (locals->hardcore_active) { rc_runtime_activate_achievement(&locals->runtime, cheevo->id, cheevo->memaddr, NULL, 0); CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u activated: %s\n", cheevo->id, cheevo->title); } else { rc_runtime_deactivate_achievement(&locals->runtime, cheevo->id); CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u deactivated: %s\n", cheevo->id, cheevo->title); } } ++cheevo; } } static void rcheevos_activate_leaderboards(rcheevos_locals_t* locals) { rcheevos_ralboard_t* lboard = locals->patchdata.lboards; unsigned i; for (i = 0; i < locals->patchdata.lboard_count; ++i, ++lboard) { if (lboard->mem) rc_runtime_activate_lboard(&locals->runtime, lboard->id, lboard->mem, NULL, 0); } } static void rcheevos_deactivate_leaderboards(rcheevos_locals_t* locals) { rcheevos_ralboard_t* lboard = locals->patchdata.lboards; unsigned i; for (i = 0; i < locals->patchdata.lboard_count; ++i, ++lboard) { if (lboard->mem) { rc_runtime_deactivate_lboard(&locals->runtime, lboard->id); #if defined(HAVE_GFX_WIDGETS) /* Hide any visible trackers */ gfx_widgets_set_leaderboard_display(lboard->id, NULL); #endif } } } void rcheevos_leaderboards_enabled_changed(void) { const settings_t* settings = config_get_ptr(); const bool leaderboards_enabled = rcheevos_locals.leaderboards_enabled; const bool leaderboard_trackers = rcheevos_locals.leaderboard_trackers; rcheevos_locals.leaderboards_enabled = rcheevos_locals.hardcore_active; if (string_is_equal(settings->arrays.cheevos_leaderboards_enable, "true")) { rcheevos_locals.leaderboard_notifications = true; rcheevos_locals.leaderboard_trackers = true; } #if defined(HAVE_GFX_WIDGETS) else if (string_is_equal( settings->arrays.cheevos_leaderboards_enable, "trackers")) { rcheevos_locals.leaderboard_notifications = false; rcheevos_locals.leaderboard_trackers = true; } else if (string_is_equal( settings->arrays.cheevos_leaderboards_enable, "notifications")) { rcheevos_locals.leaderboard_notifications = true; rcheevos_locals.leaderboard_trackers = false; } #endif else { rcheevos_locals.leaderboards_enabled = false; rcheevos_locals.leaderboard_notifications = false; rcheevos_locals.leaderboard_trackers = false; } if (rcheevos_locals.loaded) { if (leaderboards_enabled != rcheevos_locals.leaderboards_enabled) { if (rcheevos_locals.leaderboards_enabled) rcheevos_activate_leaderboards(&rcheevos_locals); else rcheevos_deactivate_leaderboards(&rcheevos_locals); } #if defined(HAVE_GFX_WIDGETS) if (!rcheevos_locals.leaderboard_trackers && leaderboard_trackers) { /* Hide any visible trackers */ unsigned i; rcheevos_ralboard_t* lboard = rcheevos_locals.patchdata.lboards; for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard) { if (lboard->mem) gfx_widgets_set_leaderboard_display(lboard->id, NULL); } } #endif } } static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals) { settings_t* settings = config_get_ptr(); bool rewind_enable = settings->bools.rewind_enable; if (!locals->hardcore_active) { /* Activate hardcore */ locals->hardcore_active = true; /* If one or more invalid settings is enabled, abort*/ rcheevos_validate_config_settings(); if (!locals->hardcore_active) return; #ifdef HAVE_CHEATS /* If one or more emulator managed cheats is active, abort */ cheat_manager_apply_cheats(); if (!locals->hardcore_active) return; #endif if (locals->loaded) { const char* msg = msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_ENABLE); CHEEVOS_LOG("%s\n", msg); runloop_msg_queue_push(msg, 0, 3 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); /* Reactivate leaderboards */ if (locals->leaderboards_enabled) rcheevos_activate_leaderboards(locals); /* reset the game */ command_event(CMD_EVENT_RESET, NULL); } /* deinit rewind */ if (rewind_enable) { #ifdef HAVE_THREADS /* have to "schedule" this. * CMD_EVENT_REWIND_DEINIT should * only be called on the main thread */ rcheevos_locals.queued_command = CMD_EVENT_REWIND_DEINIT; #else command_event(CMD_EVENT_REWIND_DEINIT, NULL); #endif } } else { /* pause hardcore */ locals->hardcore_active = false; if (locals->loaded) { CHEEVOS_LOG(RCHEEVOS_TAG "Hardcore paused\n"); /* deactivate leaderboards */ rcheevos_deactivate_leaderboards(locals); } /* re-init rewind */ if (rewind_enable) { #ifdef HAVE_THREADS /* have to "schedule" this. * CMD_EVENT_REWIND_INIT should * only be called on the main thread */ rcheevos_locals.queued_command = CMD_EVENT_REWIND_INIT; #else command_event(CMD_EVENT_REWIND_INIT, NULL); #endif } } if (locals->loaded) { rcheevos_toggle_hardcore_achievements(locals, locals->patchdata.core, locals->patchdata.core_count); if (settings->bools.cheevos_test_unofficial) rcheevos_toggle_hardcore_achievements(locals, locals->patchdata.unofficial, locals->patchdata.unofficial_count); } } void rcheevos_toggle_hardcore_paused(void) { settings_t* settings = config_get_ptr(); /* if hardcore mode is not enabled, we can't toggle it */ if (settings->bools.cheevos_hardcore_mode_enable) rcheevos_toggle_hardcore_active(&rcheevos_locals); } void rcheevos_hardcore_enabled_changed(void) { const settings_t* settings = config_get_ptr(); const bool enabled = settings && settings->bools.cheevos_enable && settings->bools.cheevos_hardcore_mode_enable; if (enabled != rcheevos_locals.hardcore_active) { rcheevos_toggle_hardcore_active(&rcheevos_locals); /* update leaderboard state flags */ rcheevos_leaderboards_enabled_changed(); } } void rcheevos_validate_config_settings(void) { const rc_disallowed_setting_t* disallowed_settings; core_option_manager_t* coreopts = NULL; struct retro_system_info* system = runloop_get_libretro_system_info(); int i; if (!system->library_name || !rcheevos_locals.hardcore_active) return; if (!(disallowed_settings = rc_libretro_get_disallowed_settings(system->library_name))) return; if (!rarch_ctl(RARCH_CTL_CORE_OPTIONS_LIST_GET, &coreopts)) return; for (i = 0; i < (int)coreopts->size; i++) { const char* key = coreopts->opts[i].key; const char* val = core_option_manager_get_val(coreopts, i); if (!rc_libretro_is_setting_allowed(disallowed_settings, key, val)) { char buffer[256]; snprintf(buffer, sizeof(buffer), "Hardcore paused. Setting not allowed: %s=%s", key, val); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", buffer); rcheevos_pause_hardcore(); runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); break; } } } static void rcheevos_runtime_event_handler(const rc_runtime_event_t* runtime_event) { #if defined(HAVE_GFX_WIDGETS) bool widgets_ready = gfx_widgets_ready(); #else bool widgets_ready = false; #endif switch (runtime_event->type) { #if defined(HAVE_GFX_WIDGETS) case RC_RUNTIME_EVENT_LBOARD_UPDATED: rcheevos_lboard_updated(rcheevos_find_lboard(runtime_event->id), runtime_event->value, widgets_ready); break; case RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED: rcheevos_challenge_started(rcheevos_find_cheevo(runtime_event->id), runtime_event->value, widgets_ready); break; case RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED: rcheevos_challenge_ended(rcheevos_find_cheevo(runtime_event->id), runtime_event->value, widgets_ready); break; #endif case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: rcheevos_award_achievement(&rcheevos_locals, rcheevos_find_cheevo(runtime_event->id), widgets_ready); break; case RC_RUNTIME_EVENT_LBOARD_STARTED: rcheevos_lboard_started(rcheevos_find_lboard(runtime_event->id), runtime_event->value, widgets_ready); break; case RC_RUNTIME_EVENT_LBOARD_CANCELED: rcheevos_lboard_canceled(rcheevos_find_lboard(runtime_event->id), widgets_ready); break; case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: rcheevos_lboard_submit(&rcheevos_locals, rcheevos_find_lboard(runtime_event->id), runtime_event->value, widgets_ready); break; case RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED: rcheevos_achievement_disabled(rcheevos_find_cheevo(runtime_event->id), runtime_event->value); break; case RC_RUNTIME_EVENT_LBOARD_DISABLED: rcheevos_lboard_disabled(rcheevos_find_lboard(runtime_event->id), runtime_event->value); break; default: break; } } static int rcheevos_runtime_address_validator(unsigned address) { return (rc_libretro_memory_find(&rcheevos_locals.memory, address) != NULL); } static void rcheevos_validate_memrefs(rcheevos_locals_t* locals) { rc_runtime_validate_addresses(&locals->runtime, rcheevos_runtime_event_handler, rcheevos_runtime_address_validator); } /***************************************************************************** Test all the achievements (call once per frame). *****************************************************************************/ void rcheevos_test(void) { #ifdef HAVE_THREADS if (rcheevos_locals.queued_command != CMD_EVENT_NONE) { command_event(rcheevos_locals.queued_command, NULL); rcheevos_locals.queued_command = CMD_EVENT_NONE; } #endif if (!rcheevos_locals.loaded) return; if (rcheevos_locals.memory.count == 0) { /* we were unable to initialize memory earlier, try now */ if (!rcheevos_init_memory(&rcheevos_locals)) { const settings_t* settings = config_get_ptr(); rcheevos_locals.core_supports = false; CHEEVOS_ERR(RCHEEVOS_TAG "No memory exposed by core\n"); if (settings && settings->bools.cheevos_verbose_enable) { runloop_msg_queue_push(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE), 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); } rcheevos_unload(); rcheevos_pause_hardcore(); return; } rcheevos_validate_memrefs(&rcheevos_locals); } rc_runtime_do_frame(&rcheevos_locals.runtime, &rcheevos_runtime_event_handler, rcheevos_peek, NULL, 0); } size_t rcheevos_get_serialize_size(void) { if (!rcheevos_locals.loaded) return 0; return rc_runtime_progress_size(&rcheevos_locals.runtime, NULL); } bool rcheevos_get_serialized_data(void* buffer) { if (!rcheevos_locals.loaded) return false; return (rc_runtime_serialize_progress(buffer, &rcheevos_locals.runtime, NULL) == RC_OK); } bool rcheevos_set_serialized_data(void* buffer) { if (rcheevos_locals.loaded) { if (buffer && rc_runtime_deserialize_progress(&rcheevos_locals.runtime, (const unsigned char*)buffer, NULL) == RC_OK) return true; rc_runtime_reset(&rcheevos_locals.runtime); } return false; } void rcheevos_set_support_cheevos(bool state) { rcheevos_locals.core_supports = state; } bool rcheevos_get_support_cheevos(void) { return rcheevos_locals.core_supports; } const char* rcheevos_get_hash(void) { return rcheevos_locals.hash; } static void rcheevos_unlock_cb(unsigned id, void* userdata) { rcheevos_racheevo_t* cheevo = rcheevos_find_cheevo(id); if (cheevo) { unsigned mode = *(unsigned*)userdata; #ifndef CHEEVOS_DONT_DEACTIVATE cheevo->active &= ~mode; #endif if ((rcheevos_locals.hardcore_active && mode == RCHEEVOS_ACTIVE_HARDCORE) || (!rcheevos_locals.hardcore_active && mode == RCHEEVOS_ACTIVE_SOFTCORE)) { rc_runtime_deactivate_achievement(&rcheevos_locals.runtime, cheevo->id); CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u deactivated: %s\n", id, cheevo->title); } } } #include "coro.h" /* Uncomment the following two lines to debug rcheevos_iterate, this will * disable the coroutine yielding. * * The code is very easy to understand. It's meant to be like BASIC: * CORO_GOTO will jump execution to another label, CORO_GOSUB will * call another label, and CORO_RET will return from a CORO_GOSUB. * * This coroutine code is inspired in a very old pure C implementation * that runs everywhere: * * https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html */ /*#undef CORO_YIELD #define CORO_YIELD()*/ typedef struct { /* variables used in the co-routine */ char badge_name[16]; char url[256]; char badge_basepath[PATH_MAX_LENGTH]; char badge_fullpath[PATH_MAX_LENGTH]; char hash[33]; unsigned gameid; unsigned i; unsigned j; unsigned k; size_t len; retro_time_t t0; void *data; char *json; const char *path; rcheevos_racheevo_t *cheevo; const rcheevos_racheevo_t *cheevo_end; settings_t *settings; struct http_connection_t *conn; struct http_t *http; struct rc_hash_iterator iterator; /* co-routine required fields */ CORO_FIELDS } rcheevos_coro_t; enum { /* Negative values because CORO_SUB generates positive values */ RCHEEVOS_GET_GAMEID = -1, RCHEEVOS_GET_CHEEVOS = -2, RCHEEVOS_GET_BADGES = -3, RCHEEVOS_LOGIN = -4, RCHEEVOS_HTTP_GET = -5, RCHEEVOS_DEACTIVATE = -6, RCHEEVOS_PLAYING = -7, RCHEEVOS_DELAY = -8 }; static int rcheevos_iterate(rcheevos_coro_t* coro) { char buffer[2048]; bool ret; #ifdef CHEEVOS_TIME_HASH retro_time_t start; #endif CORO_ENTER(); coro->settings = config_get_ptr(); /* Bail out if cheevos are disabled. * But set the above anyways, * command_read_ram needs it. */ if (!coro->settings->bools.cheevos_enable) CORO_STOP(); /* reset the network error flag */ rcheevos_locals.network_error = false; /* reset the identified game id */ rcheevos_locals.patchdata.game_id = 0; /* iterate over the possible hashes for the file being loaded */ rc_hash_initialize_iterator(&coro->iterator, coro->path, (uint8_t*)coro->data, coro->len); #ifdef CHEEVOS_TIME_HASH start = cpu_features_get_time_usec(); #endif while (rc_hash_iterate(coro->hash, &coro->iterator)) { #ifdef CHEEVOS_TIME_HASH CHEEVOS_LOG(RCHEEVOS_TAG "hash generated in %ums\n", (cpu_features_get_time_usec() - start) / 1000); #endif CORO_GOSUB(RCHEEVOS_GET_GAMEID); if (coro->gameid != 0) break; #ifdef CHEEVOS_TIME_HASH start = cpu_features_get_time_usec(); #endif } rc_hash_destroy_iterator(&coro->iterator); /* if no match was found, bail */ if (coro->gameid == 0) { CHEEVOS_LOG(RCHEEVOS_TAG "this game doesn't feature achievements\n"); strlcpy(rcheevos_locals.hash, "N/A", sizeof(rcheevos_locals.hash)); rcheevos_pause_hardcore(); CORO_STOP(); } /* capture the identified game id in case we bail before fetching the patch data (not logged in) */ rcheevos_locals.patchdata.game_id = coro->gameid; #ifdef CHEEVOS_JSON_OVERRIDE { size_t size = 0; FILE *file = fopen(CHEEVOS_JSON_OVERRIDE, "rb"); fseek(file, 0, SEEK_END); size = ftell(file); fseek(file, 0, SEEK_SET); coro->json = (char*)malloc(size + 1); fread((void*)coro->json, 1, size, file); fclose(file); coro->json[size] = 0; } #else CORO_GOSUB(RCHEEVOS_GET_CHEEVOS); if (!coro->json) { runloop_msg_queue_push("Error loading achievements.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "error loading achievements\n"); CORO_STOP(); } #endif #ifdef CHEEVOS_SAVE_JSON { FILE *file = fopen(CHEEVOS_SAVE_JSON, "w"); fwrite((void*)coro->json, 1, strlen(coro->json), file); fclose(file); } #endif #if HAVE_REWIND if (!rcheevos_locals.hardcore_active) { /* deactivate rewind while we activate the achievements */ if (coro->settings->bools.rewind_enable) { #ifdef HAVE_THREADS /* have to "schedule" this. CMD_EVENT_REWIND_DEINIT should only be called on the main thread */ rcheevos_locals.queued_command = CMD_EVENT_REWIND_DEINIT; /* wait for rewind to be disabled */ while (rcheevos_locals.queued_command != CMD_EVENT_NONE) { CORO_YIELD(); } #else command_event(CMD_EVENT_REWIND_DEINIT, NULL); #endif } } #endif ret = rcheevos_parse(&rcheevos_locals, coro->json); CHEEVOS_FREE(coro->json); if (ret == 0) { if ( rcheevos_locals.patchdata.core_count == 0 && rcheevos_locals.patchdata.unofficial_count == 0 && rcheevos_locals.patchdata.lboard_count == 0 ) { runloop_msg_queue_push( "This game has no achievements.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); if (rcheevos_locals.patchdata.richpresence_script && *rcheevos_locals.patchdata.richpresence_script) { rcheevos_locals.loaded = true; } else { rcheevos_pause_hardcore(); } } else { rcheevos_locals.loaded = true; } } #if HAVE_REWIND if (!rcheevos_locals.hardcore_active) { /* re-enable rewind. if rcheevos_locals.loaded is true, additional space will be allocated * for the achievement state data */ if (coro->settings->bools.rewind_enable) { #ifdef HAVE_THREADS /* have to "schedule" this. CMD_EVENT_REWIND_INIT should only be called on the main thread */ rcheevos_locals.queued_command = CMD_EVENT_REWIND_INIT; #else command_event(CMD_EVENT_REWIND_INIT, NULL); #endif } } #endif if (!rcheevos_locals.loaded) { /* parse failure or no achievements - nothing more to do */ CORO_STOP(); } /* * Inputs: CHEEVOS_VAR_GAMEID * Outputs: */ if (!coro->settings->bools.cheevos_start_active) CORO_GOSUB(RCHEEVOS_DEACTIVATE); /* * Inputs: CHEEVOS_VAR_GAMEID * Outputs: */ CORO_GOSUB(RCHEEVOS_PLAYING); if (coro->settings->bools.cheevos_verbose_enable && rcheevos_locals.patchdata.core_count > 0) { char msg[256]; int mode = RCHEEVOS_ACTIVE_SOFTCORE; const rcheevos_racheevo_t* cheevo = rcheevos_locals.patchdata.core; const rcheevos_racheevo_t* end = cheevo + rcheevos_locals.patchdata.core_count; int number_of_unlocked = rcheevos_locals.patchdata.core_count; int number_of_unsupported = 0; if (rcheevos_locals.hardcore_active) mode = RCHEEVOS_ACTIVE_HARDCORE; for (; cheevo < end; cheevo++) { if (!cheevo->memaddr) number_of_unsupported++; else if (cheevo->active & mode) number_of_unlocked--; } if (!number_of_unsupported) { if (coro->settings->bools.cheevos_start_active) { snprintf(msg, sizeof(msg), "All %d achievements activated for this session.", rcheevos_locals.patchdata.core_count); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); } else { snprintf(msg, sizeof(msg), "You have %d of %d achievements unlocked.", number_of_unlocked, rcheevos_locals.patchdata.core_count); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", &msg[9]); } } else { if (coro->settings->bools.cheevos_start_active) { snprintf(msg, sizeof(msg), "All %d achievements activated for this session (%d unsupported).", rcheevos_locals.patchdata.core_count, number_of_unsupported); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); } else { snprintf(msg, sizeof(msg), "You have %d of %d achievements unlocked (%d unsupported).", number_of_unlocked - number_of_unsupported, rcheevos_locals.patchdata.core_count, number_of_unsupported); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", &msg[9]); } } msg[sizeof(msg) - 1] = 0; runloop_msg_queue_push(msg, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } CORO_GOSUB(RCHEEVOS_GET_BADGES); CORO_STOP(); /************************************************************************** * Info Gets the achievements from Retro Achievements * Inputs coro->hash * Outputs coro->gameid *************************************************************************/ CORO_SUB(RCHEEVOS_GET_GAMEID) { int size; CHEEVOS_LOG(RCHEEVOS_TAG "checking %s\n", coro->hash); memcpy(rcheevos_locals.hash, coro->hash, sizeof(coro->hash)); size = rc_url_get_gameid(coro->url, sizeof(coro->url), rcheevos_locals.hash); if (size < 0) { CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); CORO_RET(); } rcheevos_log_url("rc_url_get_gameid", coro->url); CORO_GOSUB(RCHEEVOS_HTTP_GET); if (!coro->json) CORO_RET(); coro->gameid = chevos_get_gameid(coro->json); CHEEVOS_FREE(coro->json); CHEEVOS_LOG(RCHEEVOS_TAG "got game id %u\n", coro->gameid); CORO_RET(); } /************************************************************************** * Info Gets the achievements from Retro Achievements * Inputs CHEEVOS_VAR_GAMEID * Outputs CHEEVOS_VAR_JSON *************************************************************************/ CORO_SUB(RCHEEVOS_GET_CHEEVOS) { int ret; CORO_GOSUB(RCHEEVOS_LOGIN); ret = rc_url_get_patch(coro->url, sizeof(coro->url), rcheevos_locals.username, rcheevos_locals.token, coro->gameid); if (ret < 0) { CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); CORO_STOP(); } rcheevos_log_url("rc_url_get_patch", coro->url); CORO_GOSUB(RCHEEVOS_HTTP_GET); if (!coro->json) { CHEEVOS_ERR(RCHEEVOS_TAG "error getting achievements for game id %u\n", coro->gameid); CORO_STOP(); } CHEEVOS_LOG(RCHEEVOS_TAG "got achievements for game id %u\n", coro->gameid); CORO_RET(); } /************************************************************************** * Info Gets the achievements from Retro Achievements * Inputs CHEEVOS_VAR_GAMEID * Outputs CHEEVOS_VAR_JSON *************************************************************************/ CORO_SUB(RCHEEVOS_GET_BADGES) /* we always want badges if display widgets are enabled */ #if !defined(HAVE_GFX_WIDGETS) { settings_t *settings = config_get_ptr(); if (!( string_is_equal(settings->arrays.menu_driver, "xmb") || string_is_equal(settings->arrays.menu_driver, "ozone") ) || !settings->bools.cheevos_badges_enable) CORO_RET(); } #endif /* make sure the directory exists */ coro->badge_fullpath[0] = '\0'; fill_pathname_application_special(coro->badge_fullpath, sizeof(coro->badge_fullpath), APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES); if (!path_is_directory(coro->badge_fullpath)) path_mkdir(coro->badge_fullpath); /* fetch the placeholder image */ strlcpy(coro->badge_name, "00000" FILE_PATH_PNG_EXTENSION, sizeof(coro->badge_name)); fill_pathname_join(coro->badge_fullpath, coro->badge_fullpath, coro->badge_name, sizeof(coro->badge_fullpath)); if (!path_is_valid(coro->badge_fullpath)) { #ifdef CHEEVOS_LOG_BADGES CHEEVOS_LOG(RCHEEVOS_TAG "downloading badge %s\n", coro->badge_fullpath); #endif snprintf(coro->url, sizeof(coro->url), FILE_PATH_RETROACHIEVEMENTS_URL "/Badge/%s", coro->badge_name); CORO_GOSUB(RCHEEVOS_HTTP_GET); if (coro->json) { if (!filestream_write_file(coro->badge_fullpath, coro->json, coro->k)) CHEEVOS_ERR(RCHEEVOS_TAG "Error writing badge %s\n", coro->badge_fullpath); CHEEVOS_FREE(coro->json); coro->json = NULL; } } /* fetch the game images */ for (coro->i = 0; coro->i < 2; coro->i++) { if (coro->i == 0) { coro->cheevo = rcheevos_locals.patchdata.core; coro->cheevo_end = coro->cheevo + rcheevos_locals.patchdata.core_count; } else { coro->cheevo = rcheevos_locals.patchdata.unofficial; coro->cheevo_end = coro->cheevo + rcheevos_locals.patchdata.unofficial_count; } for (; coro->cheevo < coro->cheevo_end; coro->cheevo++) { if (!coro->cheevo->badge || !coro->cheevo->badge[0]) continue; for (coro->j = 0 ; coro->j < 2; coro->j++) { CORO_YIELD(); if (coro->j == 0) snprintf(coro->badge_name, sizeof(coro->badge_name), "%s" FILE_PATH_PNG_EXTENSION, coro->cheevo->badge); else snprintf(coro->badge_name, sizeof(coro->badge_name), "%s_lock" FILE_PATH_PNG_EXTENSION, coro->cheevo->badge); coro->badge_fullpath[0] = '\0'; fill_pathname_application_special(coro->badge_fullpath, sizeof(coro->badge_fullpath), APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES); fill_pathname_join( coro->badge_fullpath, coro->badge_fullpath, coro->badge_name, sizeof(coro->badge_fullpath)); if (!path_is_valid(coro->badge_fullpath)) { #ifdef CHEEVOS_LOG_BADGES CHEEVOS_LOG( RCHEEVOS_TAG "downloading badge %s\n", coro->badge_fullpath); #endif snprintf(coro->url, sizeof(coro->url), FILE_PATH_RETROACHIEVEMENTS_URL "/Badge/%s", coro->badge_name); CORO_GOSUB(RCHEEVOS_HTTP_GET); if (coro->json) { if (!filestream_write_file(coro->badge_fullpath, coro->json, coro->k)) CHEEVOS_ERR(RCHEEVOS_TAG "Error writing badge %s\n", coro->badge_fullpath); CHEEVOS_FREE(coro->json); coro->json = NULL; } } } } } CORO_RET(); /************************************************************************** * Info Logs in the user at Retro Achievements *************************************************************************/ CORO_SUB(RCHEEVOS_LOGIN) { int ret; char tok[256]; if (rcheevos_locals.token[0]) CORO_RET(); if (string_is_empty(coro->settings->arrays.cheevos_username)) { runloop_msg_queue_push( "Missing RetroAchievements account information.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); runloop_msg_queue_push( "Please fill in your account information in Settings.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "login info not informed\n"); CORO_STOP(); } if (string_is_empty(coro->settings->arrays.cheevos_token)) { ret = rc_url_login_with_password(coro->url, sizeof(coro->url), coro->settings->arrays.cheevos_username, coro->settings->arrays.cheevos_password); if (ret == RC_OK) { CHEEVOS_LOG(RCHEEVOS_TAG "attempting to login %s (with password)\n", coro->settings->arrays.cheevos_username); rcheevos_log_url("rc_url_login_with_password", coro->url); } } else { ret = rc_url_login_with_token(coro->url, sizeof(coro->url), coro->settings->arrays.cheevos_username, coro->settings->arrays.cheevos_token); if (ret == RC_OK) { CHEEVOS_LOG(RCHEEVOS_TAG "attempting to login %s (with token)\n", coro->settings->arrays.cheevos_username); rcheevos_log_url("rc_url_login_with_token", coro->url); } } if (ret < 0) { CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); CORO_STOP(); } CORO_GOSUB(RCHEEVOS_HTTP_GET); if (!coro->json) { runloop_msg_queue_push("RetroAchievements: Error contacting server.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "error getting user token\n"); CORO_STOP(); } ret = rcheevos_get_token(coro->json, rcheevos_locals.username, sizeof(rcheevos_locals.username), tok, sizeof(tok)); if (ret != 0) { char msg[512]; snprintf(msg, sizeof(msg), "RetroAchievements: %s", tok); runloop_msg_queue_push(msg, 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); *coro->settings->arrays.cheevos_token = 0; CHEEVOS_ERR(RCHEEVOS_TAG "login error: %s\n", tok); CHEEVOS_FREE(coro->json); CORO_STOP(); } CHEEVOS_FREE(coro->json); if (coro->settings->bools.cheevos_verbose_enable) { char msg[256]; snprintf(msg, sizeof(msg), "RetroAchievements: Logged in as \"%s\".", 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 "%s logged in successfully\n", rcheevos_locals.username); strlcpy(rcheevos_locals.token, tok, sizeof(rcheevos_locals.token)); /* Save token to config and clear pass on success */ strlcpy(coro->settings->arrays.cheevos_token, tok, sizeof(coro->settings->arrays.cheevos_token)); *coro->settings->arrays.cheevos_password = 0; CORO_RET(); } /************************************************************************** * Info Pauses execution for five seconds *************************************************************************/ CORO_SUB(RCHEEVOS_DELAY) { retro_time_t t1; coro->t0 = cpu_features_get_time_usec(); do { CORO_YIELD(); t1 = cpu_features_get_time_usec(); } while((t1 - coro->t0) < 3000000); } CORO_RET(); /************************************************************************** * Info Makes a HTTP GET request * Inputs CHEEVOS_VAR_URL * Outputs CHEEVOS_VAR_JSON *************************************************************************/ CORO_SUB(RCHEEVOS_HTTP_GET) for (coro->k = 0; coro->k < 5; coro->k++) { if (coro->k != 0) CHEEVOS_LOG(RCHEEVOS_TAG "Retrying HTTP request: %u of 5\n", coro->k + 1); coro->json = NULL; coro->conn = net_http_connection_new( coro->url, "GET", NULL); if (!coro->conn) { CORO_GOSUB(RCHEEVOS_DELAY); continue; } /* Don't bother with timeouts here, it's just a string scan. */ while (!net_http_connection_iterate(coro->conn)) {} /* Error finishing the connection descriptor. */ if (!net_http_connection_done(coro->conn)) { net_http_connection_free(coro->conn); continue; } rcheevos_get_user_agent(&rcheevos_locals, buffer, sizeof(buffer)); net_http_connection_set_user_agent(coro->conn, buffer); coro->http = net_http_new(coro->conn); /* Error connecting to the endpoint. */ if (!coro->http) { net_http_connection_free(coro->conn); CORO_GOSUB(RCHEEVOS_DELAY); continue; } while (!net_http_update(coro->http, NULL, NULL)) CORO_YIELD(); { size_t length; uint8_t *data = net_http_data(coro->http, &length, false); if (data) { coro->json = (char*)malloc(length + 1); if (coro->json) { memcpy((void*)coro->json, (void*)data, length); CHEEVOS_FREE(data); coro->json[length] = 0; } coro->k = (unsigned)length; net_http_delete(coro->http); net_http_connection_free(coro->conn); CORO_RET(); } } net_http_delete(coro->http); net_http_connection_free(coro->conn); } CHEEVOS_LOG(RCHEEVOS_TAG "Couldn't connect to server after 5 tries\n"); rcheevos_locals.network_error = true; CORO_RET(); /************************************************************************** * Info Deactivates the achievements already awarded * Inputs CHEEVOS_VAR_GAMEID * Outputs *************************************************************************/ CORO_SUB(RCHEEVOS_DEACTIVATE) CORO_GOSUB(RCHEEVOS_LOGIN); { int ret; unsigned mode; /* Two calls - one for softcore and one for hardcore */ for (coro->i = 0; coro->i < 2; coro->i++) { ret = rc_url_get_unlock_list(coro->url, sizeof(coro->url), rcheevos_locals.username, rcheevos_locals.token, coro->gameid, coro->i); if (ret < 0) { CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); CORO_STOP(); } rcheevos_log_url("rc_url_get_unlock_list", coro->url); CORO_GOSUB(RCHEEVOS_HTTP_GET); if (coro->json) { mode = coro->i == 0 ? RCHEEVOS_ACTIVE_SOFTCORE : RCHEEVOS_ACTIVE_HARDCORE; rcheevos_deactivate_unlocks(coro->json, rcheevos_unlock_cb, &mode); CHEEVOS_FREE(coro->json); } else CHEEVOS_ERR(RCHEEVOS_TAG "error retrieving list of unlocked achievements in softcore mode\n"); } } CORO_RET(); /************************************************************************** * Info Posts the "playing" activity to Retro Achievements * Inputs CHEEVOS_VAR_GAMEID * Outputs *************************************************************************/ CORO_SUB(RCHEEVOS_PLAYING) { int ret = rc_url_post_playing(coro->url, sizeof(coro->url), rcheevos_locals.username, rcheevos_locals.token, coro->gameid); if (ret < 0) { CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n"); CORO_STOP(); } } rcheevos_log_url("rc_url_post_playing", coro->url); CORO_GOSUB(RCHEEVOS_HTTP_GET); if (coro->json) { CHEEVOS_LOG(RCHEEVOS_TAG "Posted playing activity\n"); CHEEVOS_FREE(coro->json); } else CHEEVOS_ERR(RCHEEVOS_TAG "error posting playing activity\n"); CORO_RET(); CORO_LEAVE(); } static void rcheevos_task_handler(retro_task_t *task) { rcheevos_coro_t *coro = (rcheevos_coro_t*)task->state; if (!coro) return; if (!rcheevos_iterate(coro) || task_get_cancelled(task)) { task_set_finished(task, true); CHEEVOS_LOCK(rcheevos_locals.task_lock); rcheevos_locals.task = NULL; CHEEVOS_UNLOCK(rcheevos_locals.task_lock); if (task_get_cancelled(task)) { CHEEVOS_LOG(RCHEEVOS_TAG "Load task cancelled\n"); } else { CHEEVOS_LOG(RCHEEVOS_TAG "Load task finished\n"); } CHEEVOS_FREE(coro->data); CHEEVOS_FREE(coro->path); CHEEVOS_FREE(coro); } } /* hooks for rc_hash library */ static void* rc_hash_handle_file_open(const char* path) { return intfstream_open_file(path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); } static void rc_hash_handle_file_seek(void* file_handle, int64_t offset, int origin) { intfstream_seek((intfstream_t*)file_handle, offset, origin); } static int64_t rc_hash_handle_file_tell(void* file_handle) { return intfstream_tell((intfstream_t*)file_handle); } static size_t rc_hash_handle_file_read(void* file_handle, void* buffer, size_t requested_bytes) { return intfstream_read((intfstream_t*)file_handle, buffer, requested_bytes); } static void rc_hash_handle_file_close(void* file_handle) { intfstream_close((intfstream_t*)file_handle); CHEEVOS_FREE(file_handle); } static void* rc_hash_handle_cd_open_track(const char* path, uint32_t track) { cdfs_track_t* cdfs_track; switch (track) { case RC_HASH_CDTRACK_FIRST_DATA: cdfs_track = cdfs_open_data_track(path); break; case RC_HASH_CDTRACK_LAST: #ifdef HAVE_CHD if (string_is_equal_noncase(path_get_extension(path), "chd")) { cdfs_track = cdfs_open_track(path, CHDSTREAM_TRACK_LAST); break; } #endif CHEEVOS_LOG(RCHEEVOS_TAG "Last track only supported for CHD\n"); cdfs_track = NULL; break; case RC_HASH_CDTRACK_LARGEST: #ifdef HAVE_CHD if (string_is_equal_noncase(path_get_extension(path), "chd")) { cdfs_track = cdfs_open_track(path, CHDSTREAM_TRACK_PRIMARY); break; } #endif CHEEVOS_LOG(RCHEEVOS_TAG "Largest track only supported for CHD, using first data track\n"); cdfs_track = cdfs_open_data_track(path); break; default: cdfs_track = cdfs_open_track(path, track); break; } if (cdfs_track) { cdfs_file_t* file = (cdfs_file_t*)malloc(sizeof(cdfs_file_t)); if (cdfs_open_file(file, cdfs_track, NULL)) return file; /* ASSERT: file owns cdfs_track now */ CHEEVOS_FREE(file); cdfs_close_track(cdfs_track); /* ASSERT: this free()s cdfs_track */ } return NULL; } static size_t rc_hash_handle_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) { cdfs_file_t* file = (cdfs_file_t*)track_handle; cdfs_seek_sector(file, sector); return cdfs_read_file(file, buffer, requested_bytes); } static void rc_hash_handle_cd_close_track(void* track_handle) { cdfs_file_t* file = (cdfs_file_t*)track_handle; if (file) { cdfs_close_track(file->track); cdfs_close_file(file); /* ASSERT: this does not free() file */ CHEEVOS_FREE(file); } } /* end hooks */ bool rcheevos_load(const void *data) { retro_task_t *task = NULL; const struct retro_game_info *info = NULL; rcheevos_coro_t *coro = NULL; settings_t *settings = config_get_ptr(); bool cheevos_enable = settings && settings->bools.cheevos_enable; struct rc_hash_filereader filereader; struct rc_hash_cdreader cdreader; rcheevos_locals.loaded = false; #ifdef HAVE_THREADS rcheevos_locals.queued_command = CMD_EVENT_NONE; #endif rc_runtime_init(&rcheevos_locals.runtime); if (!cheevos_enable || !rcheevos_locals.core_supports || !data) { rcheevos_pause_hardcore(); return false; } /* reset hardcore mode and leaderboard settings based on configs */ rcheevos_hardcore_enabled_changed(); rcheevos_validate_config_settings(); rcheevos_leaderboards_enabled_changed(); coro = (rcheevos_coro_t*)calloc(1, sizeof(*coro)); if (!coro) return false; /* provide hooks for reading files */ memset(&filereader, 0, sizeof(filereader)); filereader.open = rc_hash_handle_file_open; filereader.seek = rc_hash_handle_file_seek; filereader.tell = rc_hash_handle_file_tell; filereader.read = rc_hash_handle_file_read; filereader.close = rc_hash_handle_file_close; rc_hash_init_custom_filereader(&filereader); memset(&cdreader, 0, sizeof(cdreader)); cdreader.open_track = rc_hash_handle_cd_open_track; cdreader.read_sector = rc_hash_handle_cd_read_sector; cdreader.close_track = rc_hash_handle_cd_close_track; rc_hash_init_custom_cdreader(&cdreader); rc_hash_init_error_message_callback(rcheevos_handle_log_message); #ifndef DEBUG /* in DEBUG mode, always initialize the verbose message handler */ if (settings->bools.cheevos_verbose_enable) #endif { rc_hash_init_verbose_message_callback(rcheevos_handle_log_message); } task = task_init(); if (!task) { CHEEVOS_FREE(coro); return false; } CORO_SETUP(); info = (const struct retro_game_info*)data; coro->path = strdup(info->path); if (info->data) { coro->len = info->size; /* size limit */ if (coro->len > CHEEVOS_MB(64)) coro->len = CHEEVOS_MB(64); coro->data = malloc(coro->len); if (!coro->data) { CHEEVOS_FREE(task); CHEEVOS_FREE(coro); return false; } memcpy(coro->data, info->data, coro->len); } else { coro->data = NULL; } task->handler = rcheevos_task_handler; task->state = (void*)coro; task->mute = true; task->callback = NULL; task->user_data = NULL; task->progress = 0; task->title = NULL; #ifdef HAVE_THREADS if (!rcheevos_locals.task_lock) rcheevos_locals.task_lock = slock_new(); #endif CHEEVOS_LOCK(rcheevos_locals.task_lock); rcheevos_locals.task = task; CHEEVOS_UNLOCK(rcheevos_locals.task_lock); task_queue_push(task); return true; }