/* RetroArch - A frontend for libretro. * Copyright (C) 2015-2018 - Andre Leiradella * Copyright (C) 2019-2023 - 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 #include #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_menu.h" #include "cheevos_locals.h" #include "../network/netplay/netplay.h" #include "../audio/audio_driver.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 "../runtime_file.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_runtime_types.h" #include "../deps/rcheevos/include/rc_hash.h" #include "../deps/rcheevos/src/rc_libretro.h" /* Define this macro to prevent cheevos from being deactivated when they trigger. */ #undef CHEEVOS_DONT_DEACTIVATE static rcheevos_locals_t rcheevos_locals = { #ifdef HAVE_RC_CLIENT NULL, /* client */ #else {0}, /* runtime */ {0}, /* game */ #endif {{0}},/* memory */ #ifdef HAVE_THREADS CMD_EVENT_NONE, /* queued_command */ false, /* game_placard_requested */ #endif #ifndef HAVE_RC_CLIENT "", /* displayname */ "", /* username */ "", /* token */ #endif "", /* user_agent_prefix */ "", /* user_agent_core */ #ifdef HAVE_MENU NULL, /* menuitems */ 0, /* menuitem_capacity */ 0, /* menuitem_count */ #endif #ifdef HAVE_RC_CLIENT true, /* hardcore_allowed */ false,/* hardcore_being_enabled */ #else #ifdef HAVE_GFX_WIDGETS 0, /* active_lboard_trackers */ NULL, /* tracker_achievement */ 0.0, /* tracker_progress */ #endif {RCHEEVOS_LOAD_STATE_NONE, 0, 0 }, /* load_info */ 0, /* unpaused_frames */ false,/* hardcore_active */ false,/* loaded */ #ifdef HAVE_GFX_WIDGETS false,/* assign_new_trackers */ #endif #endif true /* core_supports */ }; rcheevos_locals_t* get_rcheevos_locals(void) { return &rcheevos_locals; } #define CHEEVOS_MB(x) ((x) * 1024 * 1024) /***************************************************************************** Supporting functions. *****************************************************************************/ #define CMD_CHEEVOS_NON_COMMAND -1 static void rcheevos_show_game_placard(void); #ifndef CHEEVOS_VERBOSE void rcheevos_log(const char* fmt, ...) { (void)fmt; } #endif #ifndef HAVE_RC_CLIENT 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; cheevo->active |= RCHEEVOS_ACTIVE_UNSUPPORTED; } 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; } #endif /* HAVE_RC_CLIENT */ static void rcheevos_handle_log_message(const char* message) { CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message); } static void rcheevos_get_core_memory_info(uint32_t 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) { unsigned i; int result; struct retro_memory_map mmap; #ifdef HAVE_RC_CLIENT const rc_client_game_t* game; #endif rarch_system_info_t *sys_info = &runloop_state_get_ptr()->system; rarch_memory_map_t *mmaps = &sys_info->mmaps; struct retro_memory_descriptor* descriptors; unsigned console_id; #ifdef HAVE_RC_CLIENT /* if no game is loaded, fallback to a default mapping (SYSTEM RAM followed by SAVE RAM) */ game = rc_client_get_game_info(locals->client); console_id = game ? game->console_id : 0; #else console_id = locals->game.console_id; #endif 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, console_id); free(descriptors); return result; } uint8_t* rcheevos_patch_address(unsigned address) { /* Memory map was not previously initialized * (no achievements for this game?), try now */ if (rcheevos_locals.memory.count == 0) rcheevos_init_memory(&rcheevos_locals); return rc_libretro_memory_find(&rcheevos_locals.memory, address); } #ifndef HAVE_RC_CLIENT static uint32_t rcheevos_peek(uint32_t address, uint32_t num_bytes, void* ud) { uint32_t avail; uint8_t* data = rc_libretro_memory_find_avail( &rcheevos_locals.memory, address, &avail); if (data && avail >= num_bytes) { 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(void) { unsigned i; int result; rcheevos_racheevo_t* achievement = rcheevos_locals.game.achievements; settings_t* settings = config_get_ptr(); const uint8_t active_flag = rcheevos_locals.hardcore_active ? RCHEEVOS_ACTIVE_HARDCORE : RCHEEVOS_ACTIVE_SOFTCORE; for (i = 0; i < rcheevos_locals.game.achievement_count; i++, achievement++) { if ((achievement->active & active_flag) != 0) { result = rc_runtime_activate_achievement(&rcheevos_locals.runtime, achievement->id, achievement->memaddr, NULL, 0); if (result != RC_OK) { char buffer[256]; buffer[0] = '\0'; /* TODO/FIXME - localize */ snprintf(buffer, sizeof(buffer), "Could not activate achievement %u \"%s\": %s", achievement->id, achievement->title, rc_error_str(result)); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "%s: mem %s\n", buffer, achievement->memaddr); achievement->active &= ~(RCHEEVOS_ACTIVE_HARDCORE | RCHEEVOS_ACTIVE_SOFTCORE); achievement->active |= RCHEEVOS_ACTIVE_UNSUPPORTED; CHEEVOS_FREE(achievement->memaddr); achievement->memaddr = NULL; } } } } static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id) { rcheevos_racheevo_t* cheevo = rcheevos_locals.game.achievements; rcheevos_racheevo_t* stop = cheevo + rcheevos_locals.game.achievement_count; for(; cheevo < stop; ++cheevo) { if (cheevo->id == id) return cheevo; } return NULL; } #endif static bool rcheevos_is_game_loaded(void) { #ifdef HAVE_RC_CLIENT return rc_client_is_game_loaded(rcheevos_locals.client); #else return rcheevos_locals.loaded; #endif } static bool rcheevos_is_player_active(void) { if (netplay_is_spectating()) return false; /* TODO: disallow player slots other than player one unless it's a [Multi] set */ return true; } void rcheevos_spectating_changed(void) { #ifdef HAVE_RC_CLIENT /* don't update spectator mode while a game is loading - it prevents being able to change it later */ if (rcheevos_is_game_loaded()) { const bool spectating = !rcheevos_is_player_active(); if (spectating != rc_client_get_spectator_mode_enabled(rcheevos_locals.client)) rc_client_set_spectator_mode_enabled(rcheevos_locals.client, !rcheevos_is_player_active()); } #endif } bool rcheevos_is_pause_allowed(void) { #ifdef HAVE_RC_CLIENT return rc_client_can_pause(rcheevos_locals.client, NULL); #else return (rcheevos_locals.unpaused_frames == 0); #endif } #ifdef HAVE_RC_CLIENT static void rcheevos_show_mastery_placard(void) { const settings_t* settings = config_get_ptr(); if (settings->bools.cheevos_visibility_mastery) { const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals.client); char title[256]; snprintf(title, sizeof(title), msg_hash_to_str(rc_client_get_hardcore_enabled(rcheevos_locals.client) ? MSG_CHEEVOS_MASTERED_GAME : MSG_CHEEVOS_COMPLETED_GAME), game->title); title[sizeof(title) - 1] = '\0'; #if defined (HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) { const char* displayname = rc_client_get_user_info(rcheevos_locals.client)->display_name; const bool content_runtime_log = settings->bools.content_runtime_log; const bool content_runtime_log_aggr = settings->bools.content_runtime_log_aggregate; char badge_name[32]; char msg[128]; size_t len = strlcpy(msg, displayname, sizeof(msg)); if (len < sizeof(msg) - 12 && (content_runtime_log || content_runtime_log_aggr)) { const char* content_path = path_get(RARCH_PATH_CONTENT); const char* core_path = path_get(RARCH_PATH_CORE); runtime_log_t* runtime_log = runtime_log_init( content_path, core_path, settings->paths.directory_runtime_log, settings->paths.directory_playlist, !content_runtime_log_aggr); if (runtime_log) { const runloop_state_t* runloop_state = runloop_state_get_ptr(); runtime_log_add_runtime_usec(runtime_log, runloop_state->core_runtime_usec); len += snprintf(msg + len, sizeof(msg) - len, " | "); runtime_log_get_runtime_str(runtime_log, msg + len, sizeof(msg) - len); msg[sizeof(msg) - 1] = '\0'; free(runtime_log); } } snprintf(badge_name, sizeof(badge_name), "i%s", game->badge_name); gfx_widgets_push_achievement(title, msg, badge_name); } else #endif runloop_msg_queue_push(title, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } static void rcheevos_award_achievement(const rc_client_achievement_t* cheevo) { const settings_t* settings = config_get_ptr(); if (!cheevo) return; /* Show the on screen message. */ if (settings->bools.cheevos_visibility_unlock) { #if defined(HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) { gfx_widgets_push_achievement(msg_hash_to_str(MSG_ACHIEVEMENT_UNLOCKED), cheevo->title, cheevo->badge_name); } 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); } } #ifdef HAVE_AUDIOMIXER /* Play the unlock sound */ if (settings->bools.cheevos_unlock_sound_enable) audio_driver_mixer_play_menu_sound( AUDIO_MIXER_SYSTEM_SLOT_ACHIEVEMENT_UNLOCK); #endif #ifdef HAVE_SCREENSHOTS /* Take a screenshot of the achievement. */ if (settings->bools.cheevos_auto_screenshot) { size_t shotname_len = sizeof(char) * 8192; char* shotname = (char*)malloc(shotname_len); if (shotname) { video_driver_state_t* video_st = video_state_get_ptr();; snprintf(shotname, shotname_len, "%s/%s-cheevo-%u", settings->paths.directory_screenshot, path_basename(path_get(RARCH_PATH_BASENAME)), (unsigned)cheevo->id); shotname[shotname_len - 1] = '\0'; if (take_screenshot(settings->paths.directory_screenshot, shotname, true, video_st->frame_cache_data && (video_st->frame_cache_data == RETRO_HW_FRAME_BUFFER_VALID), 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_lboard_submit(const rc_client_leaderboard_t* lboard) { const settings_t* settings = config_get_ptr(); if (lboard && settings->bools.cheevos_visibility_lboard_submit) { char buffer[256]; snprintf(buffer, sizeof(buffer), msg_hash_to_str(MSG_LEADERBOARD_SUBMISSION), lboard->tracker_value, lboard->title); runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } static void rcheevos_lboard_canceled(const rc_client_leaderboard_t* lboard) { const settings_t* settings = config_get_ptr(); if (lboard && settings->bools.cheevos_visibility_lboard_cancel) { char buffer[256]; snprintf(buffer, sizeof(buffer), "%s: %s", msg_hash_to_str(MSG_LEADERBOARD_FAILED), 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(const rc_client_leaderboard_t* lboard) { const settings_t* settings = config_get_ptr(); if (settings->bools.cheevos_visibility_lboard_start) { char buffer[256]; if (lboard->description && *lboard->description) snprintf(buffer, sizeof(buffer), "%s: %s - %s", msg_hash_to_str(MSG_LEADERBOARD_STARTED), lboard->title, lboard->description); else snprintf(buffer, sizeof(buffer), "%s: %s", msg_hash_to_str(MSG_LEADERBOARD_STARTED), 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_update_tracker(const rc_client_leaderboard_tracker_t* tracker) { const settings_t* settings = config_get_ptr(); if (tracker && gfx_widgets_ready() && settings->bools.cheevos_visibility_lboard_trackers) gfx_widgets_set_leaderboard_display(tracker->id, tracker->display); } static void rcheevos_lboard_hide_tracker(const rc_client_leaderboard_tracker_t* tracker) { const settings_t* settings = config_get_ptr(); if (tracker && gfx_widgets_ready() && settings->bools.cheevos_visibility_lboard_trackers) gfx_widgets_set_leaderboard_display(tracker->id, NULL); } static void rcheevos_challenge_started(const rc_client_achievement_t* cheevo) { settings_t* settings = config_get_ptr(); if (cheevo && gfx_widgets_ready() && settings->bools.cheevos_challenge_indicators) gfx_widgets_set_challenge_display(cheevo->id, cheevo->badge_name); } static void rcheevos_challenge_ended(const rc_client_achievement_t* cheevo) { if (cheevo && gfx_widgets_ready()) gfx_widgets_set_challenge_display(cheevo->id, NULL); } static void rcheevos_progress_updated(rcheevos_locals_t* locals, const rc_client_achievement_t* cheevo) { settings_t* settings = config_get_ptr(); if (cheevo && gfx_widgets_ready() && settings->bools.cheevos_visibility_progress_tracker) gfx_widget_set_achievement_progress(cheevo->badge_name, cheevo->measured_progress); } static void rcheevos_progress_hide(rcheevos_locals_t* locals) { settings_t* settings = config_get_ptr(); if (gfx_widgets_ready() && settings->bools.cheevos_visibility_progress_tracker) gfx_widget_set_achievement_progress(NULL, NULL); } #endif static void rcheevos_client_log_message(const char* message, const rc_client_t* client) { CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message); } static void rcheevos_server_error(const char* api_name, const char* message) { char buffer[256]; snprintf(buffer, sizeof(buffer), "%s failed: %s", api_name, message); runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); } static void rcheevos_server_disconnected(void) { CHEEVOS_LOG(RCHEEVOS_TAG "Unable to communicate with RetroAchievements server\n"); /* always show message - even with widget. it helps the user understand what the widget is for */ { const char* message = msg_hash_to_str(MENU_ENUM_LABEL_CHEEVOS_SERVER_DISCONNECTED); runloop_msg_queue_push(message, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); } #if defined(HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) gfx_widget_set_cheevos_disconnect(true); #endif } static void rcheevos_server_reconnected(void) { CHEEVOS_LOG(RCHEEVOS_TAG "All pending requests synced to RetroAchievements server\n"); { const char* message = msg_hash_to_str(MENU_ENUM_LABEL_CHEEVOS_SERVER_RECONNECTED); runloop_msg_queue_push(message, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); } #if defined(HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) gfx_widget_set_cheevos_disconnect(false); #endif } static void rcheevos_client_event_handler(const rc_client_event_t* event, rc_client_t* client) { switch (event->type) { #ifdef HAVE_GFX_WIDGETS case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: rcheevos_lboard_update_tracker(event->leaderboard_tracker); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: rcheevos_challenge_started(event->achievement); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: rcheevos_challenge_ended(event->achievement); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: rcheevos_progress_updated(&rcheevos_locals, event->achievement); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: rcheevos_progress_updated(&rcheevos_locals, event->achievement); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: rcheevos_progress_hide(&rcheevos_locals); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: rcheevos_lboard_update_tracker(event->leaderboard_tracker); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: rcheevos_lboard_hide_tracker(event->leaderboard_tracker); break; case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: /* not supported */ break; #endif case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: rcheevos_award_achievement(event->achievement); break; case RC_CLIENT_EVENT_LEADERBOARD_STARTED: rcheevos_lboard_started(event->leaderboard); break; case RC_CLIENT_EVENT_LEADERBOARD_FAILED: rcheevos_lboard_canceled(event->leaderboard); break; case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: rcheevos_lboard_submit(event->leaderboard); break; case RC_CLIENT_EVENT_RESET: command_event(CMD_EVENT_RESET, NULL); /* reset the game */ break; case RC_CLIENT_EVENT_GAME_COMPLETED: rcheevos_show_mastery_placard(); break; case RC_CLIENT_EVENT_SERVER_ERROR: rcheevos_server_error(event->server_error->api, event->server_error->error_message); break; case RC_CLIENT_EVENT_DISCONNECTED: rcheevos_server_disconnected(); break; case RC_CLIENT_EVENT_RECONNECTED: rcheevos_server_reconnected(); break; default: #ifndef NDEBUG CHEEVOS_LOG(RCHEEVOS_TAG "Unsupported rc_client event %u\n", event->type); #endif break; } } int rcheevos_get_richpresence(char* s, size_t len) { if (!rcheevos_is_player_active()) { if (!rcheevos_is_game_loaded()) return 0; /* TODO/FIXME - localize */ { size_t _len = strlcpy(s, "Spectating ", len); return (int)strlcpy(s + _len, rc_client_get_game_info(rcheevos_locals.client)->title, len - _len); } } return (int)rc_client_get_rich_presence_message(rcheevos_locals.client, s, (size_t)len); } #else /* !HAVE_RC_CLIENT */ 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; /* 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(); if (!rcheevos_is_player_active()) { CHEEVOS_LOG(RCHEEVOS_TAG "Not awarding achievement %u, player not active\n", cheevo->id); return; } CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n", cheevo->id, cheevo->title, cheevo->description); /* Show the on screen message. */ if (settings->bools.cheevos_visibility_unlock) { #if defined(HAVE_GFX_WIDGETS) if (widgets_ready) gfx_widgets_push_achievement(msg_hash_to_str(MSG_ACHIEVEMENT_UNLOCKED), 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); #ifdef HAVE_AUDIOMIXER /* Play the unlock sound */ if (settings->bools.cheevos_unlock_sound_enable) audio_driver_mixer_play_menu_sound( AUDIO_MIXER_SYSTEM_SLOT_ACHIEVEMENT_UNLOCK); #endif #ifdef HAVE_SCREENSHOTS /* Take a screenshot of the achievement. */ if (settings->bools.cheevos_auto_screenshot) { size_t shotname_len = sizeof(char) * 8192; char *shotname = (char*)malloc(shotname_len); if (shotname) { video_driver_state_t *video_st = video_state_get_ptr();; 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_st->frame_cache_data && (video_st->frame_cache_data == RETRO_HW_FRAME_BUFFER_VALID), 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.game.leaderboards; rcheevos_ralboard_t* stop = lboard + rcheevos_locals.game.leaderboard_count; for (; lboard < stop; ++lboard) { if (lboard->id == id) return lboard; } return NULL; } #if defined(HAVE_GFX_WIDGETS) static void rcheevos_assign_leaderboard_tracker_ids(rcheevos_locals_t* locals) { rcheevos_ralboard_t* lboard = locals->game.leaderboards; rcheevos_ralboard_t* scan; rcheevos_ralboard_t* end = lboard + locals->game.leaderboard_count; unsigned tracker_id; char buffer[32]; for (; lboard < end; ++lboard) { if (lboard->active_tracker_id != 0xFF) continue; tracker_id = 0; if (locals->active_lboard_trackers != 0 && lboard->value_hash != 0) { scan = locals->game.leaderboards; for (; scan < end; ++scan) { if (scan->active_tracker_id == 0 || scan->active_tracker_id == 0xFF) continue; /* value_hash match indicates the values have the same definition, but if the leaderboard * is tracking hits, it could have a different value depending on when it was started. * also require the current value to match. */ if (scan->value_hash == lboard->value_hash && scan->value == lboard->value) { tracker_id = scan->active_tracker_id; break; } } } if (!tracker_id) { unsigned active_trackers = locals->active_lboard_trackers >> 1; tracker_id++; while ((active_trackers & 1) != 0) { tracker_id++; active_trackers >>= 1; } if (tracker_id <= 31) { locals->active_lboard_trackers |= (1 << tracker_id); rc_runtime_format_lboard_value(buffer, sizeof(buffer), lboard->value, lboard->format); gfx_widgets_set_leaderboard_display(tracker_id, buffer); } } lboard->active_tracker_id = tracker_id; } } static void rcheevos_hide_leaderboard_tracker(rcheevos_locals_t* locals, rcheevos_ralboard_t* lboard) { unsigned i; uint8_t tracker_id = lboard->active_tracker_id; if (!tracker_id) return; lboard->active_tracker_id = 0; for (i = 0; i < locals->game.leaderboard_count; ++i) { if (locals->game.leaderboards[i].active_tracker_id == tracker_id) return; } if (tracker_id <= 31) { locals->active_lboard_trackers &= ~(1 << tracker_id); gfx_widgets_set_leaderboard_display(tracker_id, NULL); } } #endif 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]; const settings_t *settings = config_get_ptr(); #if defined(HAVE_GFX_WIDGETS) /* Hide the tracker */ if (gfx_widgets_ready()) rcheevos_hide_leaderboard_tracker(locals, lboard); #endif rc_runtime_format_lboard_value(formatted_value, sizeof(formatted_value), value, lboard->format); if (!rcheevos_is_player_active()) { CHEEVOS_LOG(RCHEEVOS_TAG "Not submitting %s for leaderboard %u, player not active\n", formatted_value, lboard->id); return; } CHEEVOS_LOG(RCHEEVOS_TAG "Submitting %s for leaderboard %u\n", formatted_value, lboard->id); /* Show the on-screen message. */ if (settings->bools.cheevos_visibility_lboard_submit) { snprintf(buffer, sizeof(buffer), msg_hash_to_str(MSG_LEADERBOARD_SUBMISSION), formatted_value, lboard->title); runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } /* Start the submit task */ rcheevos_client_submit_lboard_entry(lboard->id, value); } static void rcheevos_lboard_canceled(rcheevos_ralboard_t * lboard, bool widgets_ready) { const settings_t *settings = config_get_ptr(); 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) rcheevos_hide_leaderboard_tracker(&rcheevos_locals, lboard); #endif if (settings->bools.cheevos_visibility_lboard_cancel) { snprintf(buffer, sizeof(buffer), "%s: %s", msg_hash_to_str(MSG_LEADERBOARD_FAILED), 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) { const settings_t *settings = config_get_ptr(); char buffer[256]; if (!lboard) return; CHEEVOS_LOG(RCHEEVOS_TAG "Leaderboard %u started: %s\n", lboard->id, lboard->title); if (!rcheevos_is_player_active()) return; #if defined(HAVE_GFX_WIDGETS) lboard->value = value; if (settings->bools.cheevos_visibility_lboard_trackers) { /* mark the leaderboard as needing a tracker assigned so we can check for merging later */ lboard->active_tracker_id = 0xFF; rcheevos_locals.assign_new_trackers = true; } #endif if (settings->bools.cheevos_visibility_lboard_start) { if (lboard->description && *lboard->description) snprintf(buffer, sizeof(buffer), "%s: %s - %s", msg_hash_to_str(MSG_LEADERBOARD_STARTED), lboard->title, lboard->description); else snprintf(buffer, sizeof(buffer), "%s: %s", msg_hash_to_str(MSG_LEADERBOARD_STARTED), 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) { const settings_t *settings = config_get_ptr(); if (!lboard) return; lboard->value = value; if (widgets_ready && settings->bools.cheevos_visibility_lboard_trackers && lboard->active_tracker_id && lboard->active_tracker_id <= 31) { char buffer[32]; rc_runtime_format_lboard_value(buffer, sizeof(buffer), value, lboard->format); gfx_widgets_set_leaderboard_display(lboard->active_tracker_id, rcheevos_is_player_active() ? buffer : NULL); } } 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 && rcheevos_is_player_active()) 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); } static void rcheevos_progress_updated(rcheevos_locals_t* locals, rcheevos_racheevo_t* cheevo, int value, bool widgets_ready) { settings_t* settings = config_get_ptr(); if ( cheevo && widgets_ready && settings->bools.cheevos_visibility_progress_tracker && rcheevos_is_player_active()) { unsigned measured_value, measured_target; if (rc_runtime_get_achievement_measured(&locals->runtime, cheevo->id, &measured_value, &measured_target)) { const float progress = ((float)measured_value / (float)measured_target); if (progress > locals->tracker_progress) { locals->tracker_progress = progress; locals->tracker_achievement = cheevo; } } } } #endif int rcheevos_get_richpresence(char *s, size_t len) { if (rcheevos_is_player_active()) { int ret = rc_runtime_get_richpresence( &rcheevos_locals.runtime, s, (unsigned)len, &rcheevos_peek, NULL, NULL); if (ret <= 0 && rcheevos_locals.game.title) { /* TODO/FIXME - localize */ size_t _len = strlcpy(s, "Playing ", len); strlcpy(s + _len, rcheevos_locals.game.title, len - _len); } return ret; } if (rcheevos_locals.game.title) { /* TODO/FIXME - localize */ size_t _len = strlcpy(s, "Spectating ", len); return (int)strlcpy(s + _len, rcheevos_locals.game.title, len - _len); } return 0; } #if defined(HAVE_GFX_WIDGETS) static void rcheevos_hide_leaderboard_trackers(void) { unsigned i = 0; unsigned trackers = rcheevos_locals.active_lboard_trackers; if (trackers == 0) return; do { if ((trackers & 1) != 0) gfx_widgets_set_leaderboard_display(i, NULL); i++; trackers >>= 1; } while (trackers != 0); for (i = 0; i < rcheevos_locals.game.leaderboard_count; i++) rcheevos_locals.game.leaderboards[i].active_tracker_id = 0; } #endif #endif /* HAVE_RC_CLIENT */ #ifdef HAVE_GFX_WIDGETS static void rcheevos_hide_widgets(bool widgets_ready) { /* Hide any visible trackers */ if (widgets_ready) { gfx_widgets_clear_leaderboard_displays(); gfx_widgets_clear_challenge_displays(); gfx_widget_set_achievement_progress(NULL, NULL); } } #endif void rcheevos_reset_game(bool widgets_ready) { #if defined(HAVE_GFX_WIDGETS) /* Hide any visible trackers */ rcheevos_hide_widgets(widgets_ready); #endif #ifdef HAVE_RC_CLIENT rc_client_reset(rcheevos_locals.client); #else rc_runtime_reset(&rcheevos_locals.runtime); #endif /* Some cores reallocate memory on reset, * make sure we update our pointers */ if (rcheevos_locals.memory.total_size > 0) rcheevos_init_memory(&rcheevos_locals); } void rcheevos_refresh_memory(void) { if (rcheevos_locals.memory.total_size > 0) rcheevos_init_memory(&rcheevos_locals); } bool rcheevos_hardcore_active(void) { #ifdef HAVE_RC_CLIENT /* normal hardcore check */ if (rcheevos_locals.client && rc_client_get_hardcore_enabled(rcheevos_locals.client)) return true; /* if we're trying to enable hardcore, pretend it's on so the caller can decide to disable * it (by calling rcheevos_pause_hardcore) before we actually turn it on. */ return rcheevos_locals.hardcore_being_enabled; #else return rcheevos_locals.hardcore_active; #endif } void rcheevos_pause_hardcore(void) { #ifdef HAVE_RC_CLIENT rcheevos_locals.hardcore_allowed = false; #endif if (rcheevos_hardcore_active()) rcheevos_toggle_hardcore_paused(); } #if defined(HAVE_THREADS) && !defined(HAVE_RC_CLIENT) static bool rcheevos_timer_check(void* userdata) { retro_time_t stop_time = *(retro_time_t*)userdata; retro_time_t now = cpu_features_get_time_usec(); return (now < stop_time); } #endif bool rcheevos_unload(void) { settings_t* settings = config_get_ptr(); const bool was_loaded = rcheevos_is_game_loaded(); #ifdef HAVE_GFX_WIDGETS rcheevos_hide_widgets(gfx_widgets_ready()); gfx_widget_set_cheevos_set_loading(false); #endif #ifdef HAVE_RC_CLIENT rc_client_unload_game(rcheevos_locals.client); #else /* Immediately mark the game as unloaded so the ping thread will terminate normally */ rcheevos_locals.game.id = -1; rcheevos_locals.game.console_id = 0; rcheevos_locals.game.hash = NULL; #ifdef HAVE_THREADS if (rcheevos_locals.load_info.state < RCHEEVOS_LOAD_STATE_DONE && rcheevos_locals.load_info.state != RCHEEVOS_LOAD_STATE_NONE) { /* allow up to 5 seconds for pending tasks to run */ retro_time_t stop_time = cpu_features_get_time_usec() + 5000000; rcheevos_locals.load_info.state = RCHEEVOS_LOAD_STATE_ABORTED; CHEEVOS_LOG(RCHEEVOS_TAG "Asked the load tasks to terminate\n"); /* Wait for pending tasks to run */ task_queue_wait(rcheevos_timer_check, &stop_time); /* Clean up after completed tasks */ task_queue_check(); } #endif #endif #ifdef HAVE_THREADS rcheevos_locals.queued_command = CMD_EVENT_NONE; rcheevos_locals.game_placard_requested = false; #endif if (rcheevos_locals.memory.count > 0) rc_libretro_memory_destroy(&rcheevos_locals.memory); if (was_loaded) { #ifndef HAVE_RC_CLIENT unsigned count = 0; #endif #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 #ifndef HAVE_RC_CLIENT count = rcheevos_locals.game.achievement_count; rcheevos_locals.game.achievement_count = 0; if (rcheevos_locals.game.achievements) { rcheevos_racheevo_t* achievement = rcheevos_locals.game.achievements; rcheevos_racheevo_t* end = achievement + count; while (achievement < end) { CHEEVOS_FREE(achievement->title); CHEEVOS_FREE(achievement->description); CHEEVOS_FREE(achievement->badge); CHEEVOS_FREE(achievement->memaddr); ++achievement; } CHEEVOS_FREE(rcheevos_locals.game.achievements); rcheevos_locals.game.achievements = NULL; } count = rcheevos_locals.game.leaderboard_count; rcheevos_locals.game.leaderboard_count = 0; if (rcheevos_locals.game.leaderboards) { rcheevos_ralboard_t* lboard = rcheevos_locals.game.leaderboards; rcheevos_ralboard_t* end = lboard + count; while (lboard < end) { CHEEVOS_FREE(lboard->title); CHEEVOS_FREE(lboard->description); CHEEVOS_FREE(lboard->mem); ++lboard; } CHEEVOS_FREE(rcheevos_locals.game.leaderboards); rcheevos_locals.game.leaderboards = NULL; } if (rcheevos_locals.game.title) { CHEEVOS_FREE(rcheevos_locals.game.title); rcheevos_locals.game.title = NULL; } rcheevos_locals.loaded = false; rcheevos_locals.hardcore_active = false; rc_libretro_hash_set_destroy(&rcheevos_locals.game.hashes); #endif } #ifdef HAVE_THREADS rcheevos_locals.queued_command = CMD_EVENT_NONE; #endif if (!settings->arrays.cheevos_token[0]) { #ifdef HAVE_RC_CLIENT /* If the config-level token has been cleared, we need to re-login on * loading the next game. Easiest way to do that is to destroy the client */ rc_client_t* client = rcheevos_locals.client; rcheevos_locals.client = NULL; rc_client_destroy(client); #else /* If the config-level token has been cleared, * we need to re-login on loading the next game */ rcheevos_locals.token[0] = '\0'; #endif } #ifndef HAVE_RC_CLIENT rc_runtime_destroy(&rcheevos_locals.runtime); rcheevos_locals.load_info.state = RCHEEVOS_LOAD_STATE_NONE; #endif return true; } #ifndef HAVE_RC_CLIENT static void rcheevos_toggle_hardcore_achievements( rcheevos_locals_t *locals) { const unsigned active_mask = RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE | RCHEEVOS_ACTIVE_UNSUPPORTED; rcheevos_racheevo_t* cheevo = locals->game.achievements; rcheevos_racheevo_t* stop = cheevo + locals->game.achievement_count; while (cheevo < stop) { if ((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(void) { unsigned i; int result; rcheevos_ralboard_t* leaderboard = rcheevos_locals.game.leaderboards; const settings_t *settings = config_get_ptr(); for (i = 0; i < rcheevos_locals.game.leaderboard_count; ++i, ++leaderboard) { if (!leaderboard->mem) continue; result = rc_runtime_activate_lboard( &rcheevos_locals.runtime, leaderboard->id, leaderboard->mem, NULL, 0); if (result != RC_OK) { char buffer[256]; buffer[0] = '\0'; /* TODO/FIXME - localize */ snprintf(buffer, sizeof(buffer), "Could not activate leaderboard %u \"%s\": %s", leaderboard->id, leaderboard->title, rc_error_str(result)); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_ERR(RCHEEVOS_TAG "%s: mem %s\n", buffer, leaderboard->mem); CHEEVOS_FREE(leaderboard->mem); leaderboard->mem = NULL; } } } static void rcheevos_deactivate_leaderboards(void) { rcheevos_ralboard_t* lboard = rcheevos_locals.game.leaderboards; rcheevos_ralboard_t* stop = lboard + rcheevos_locals.game.leaderboard_count; #if defined(HAVE_GFX_WIDGETS) /* Hide any visible trackers */ rcheevos_hide_leaderboard_trackers(); #endif for (; lboard < stop; ++lboard) { if (lboard->mem) { rc_runtime_deactivate_lboard(&rcheevos_locals.runtime, lboard->id); } } } void rcheevos_leaderboard_trackers_visibility_changed(void) { const settings_t* settings = config_get_ptr(); if (rcheevos_locals.loaded) { #if defined(HAVE_GFX_WIDGETS) if (!settings->bools.cheevos_visibility_lboard_trackers) { /* Hide any visible trackers */ rcheevos_hide_leaderboard_trackers(); } else { unsigned i; rc_runtime_lboard_t* lboard = rcheevos_locals.runtime.lboards; for (i = 0; i < rcheevos_locals.runtime.lboard_count; ++i, ++lboard) { if (!lboard->lboard) continue; if (lboard->lboard->state == RC_LBOARD_STATE_STARTED) { rcheevos_ralboard_t* ralboard = rcheevos_find_lboard(lboard->id); if (ralboard && !ralboard->active_tracker_id) { /* mark the leaderboard as needing a tracker assigned so we can check for merging later */ ralboard->active_tracker_id = 0xFF; rcheevos_locals.assign_new_trackers = true; } } else { rcheevos_ralboard_t* ralboard = rcheevos_find_lboard(lboard->id); if (ralboard && ralboard->active_tracker_id) rcheevos_hide_leaderboard_tracker(&rcheevos_locals, ralboard); } } } #endif } } #else /* HAVE_RC_CLIENT */ void rcheevos_leaderboard_trackers_visibility_changed(void) { #if defined(HAVE_GFX_WIDGETS) const settings_t* settings = config_get_ptr(); if (!settings->bools.cheevos_visibility_lboard_trackers) { /* Hide any visible trackers */ gfx_widgets_clear_leaderboard_displays(); } else { /* No way to immediately request trackers be reshown, but they * will reappear the next time they're updated */ } #endif } #endif /* HAVE_RC_CLIENT */ static void rcheevos_enforce_hardcore_settings(void) { /* disable slowdown */ runloop_state_get_ptr()->flags &= ~RUNLOOP_FLAG_SLOWMOTION; } static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals) { settings_t* settings = config_get_ptr(); bool rewind_enable = settings->bools.rewind_enable; const bool was_enabled = rcheevos_hardcore_active(); if (!was_enabled) { #ifdef HAVE_RC_CLIENT locals->hardcore_being_enabled = true; locals->hardcore_allowed = true; #else /* Activate hardcore */ locals->hardcore_active = true; #endif /* If one or more invalid settings is enabled, abort*/ rcheevos_validate_config_settings(); #ifdef HAVE_RC_CLIENT if (!locals->hardcore_allowed) { locals->hardcore_being_enabled = false; return; } #else if (!locals->hardcore_active) return; #endif #ifdef HAVE_CHEATS /* If one or more emulator managed cheats is active, abort */ cheat_manager_apply_cheats(); #ifdef HAVE_RC_CLIENT if (!locals->hardcore_allowed) { locals->hardcore_being_enabled = false; return; } #else if (!locals->hardcore_active) return; #endif #endif if (rcheevos_is_game_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); rcheevos_enforce_hardcore_settings(); #ifndef HAVE_RC_CLIENT /* Reactivate leaderboards */ rcheevos_activate_leaderboards(); /* reset the game */ command_event(CMD_EVENT_RESET, NULL); #endif } /* deinit rewind */ if (rewind_enable) { #ifdef HAVE_THREADS if (!task_is_on_main_thread()) { /* 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 #endif command_event(CMD_EVENT_REWIND_DEINIT, NULL); } #ifdef HAVE_RC_CLIENT locals->hardcore_being_enabled = false; rc_client_set_hardcore_enabled(locals->client, 1); #endif } else { /* pause hardcore */ #ifdef HAVE_RC_CLIENT rc_client_set_hardcore_enabled(locals->client, 0); #else locals->hardcore_active = false; if (locals->loaded) { CHEEVOS_LOG(RCHEEVOS_TAG "Hardcore paused\n"); /* deactivate leaderboards */ rcheevos_deactivate_leaderboards(); } #endif /* re-init rewind */ if (rewind_enable) { #ifdef HAVE_THREADS if (!task_is_on_main_thread()) { /* 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 #endif command_event(CMD_EVENT_REWIND_INIT, NULL); } } #ifndef HAVE_RC_CLIENT if (locals->loaded) rcheevos_toggle_hardcore_achievements(locals); #endif } void rcheevos_toggle_hardcore_paused(void) { settings_t* settings = config_get_ptr(); /* if hardcore mode is not enabled, we can't toggle whether its active */ if (settings->bools.cheevos_hardcore_mode_enable) rcheevos_toggle_hardcore_active(&rcheevos_locals); } void rcheevos_hardcore_enabled_changed(void) { /* called whenever a setting that could potentially affect hardcore enabledness changes * (i.e. cheevos_enable, hardcore_mode_enable) to synchronize the internal state to the configs. * also called when a game is first loaded to synchronize the internal state to the configs. */ const settings_t* settings = config_get_ptr(); const bool enabled = settings && settings->bools.cheevos_enable && settings->bools.cheevos_hardcore_mode_enable; const bool was_enabled = rcheevos_hardcore_active(); if (enabled != was_enabled) { rcheevos_toggle_hardcore_active(&rcheevos_locals); } else if (was_enabled && rcheevos_is_game_loaded()) { /* hardcore enabledness didn't change, but hardcore is active, so make * sure to enforce the restrictions. */ rcheevos_enforce_hardcore_settings(); } } void rcheevos_validate_config_settings(void) { int i; const rc_disallowed_setting_t *disallowed_settings = NULL; core_option_manager_t* coreopts = NULL; struct retro_system_info *sysinfo = &runloop_state_get_ptr()->system.info; const settings_t* settings = config_get_ptr(); unsigned console_id; if (!sysinfo->library_name || !rcheevos_hardcore_active()) return; /* this adds a sleep to every frame. if the value is high enough that a * single frame takes more than 1/60th of a second to evaluate, render, * and sleep, then the real framerate is less than 60fps. with vsync on, * it'll wait for the next vsync event, effectively halfing the fps. the * auto setting should achieve the most accurate frame rate anyway, so * disallow any manual values */ if (!settings->bools.video_frame_delay_auto && settings->uints.video_frame_delay != 0) { const char* error = "Hardcore paused. Manual video frame delay setting not allowed."; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", error); rcheevos_pause_hardcore(); runloop_msg_queue_push(error, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); return; } /* this specifies how many vsync events should occur for each rendered * frame. if vsync is on for a 60Hz monitor and swap_interval is 2 (only * update every other vsync), only 30fps will be generated. for a 144Hz * monitor, a value of 2 will generate 72fps, which is still faster than * the expected 60fps, so the user should really be using auto (0). * allow 1 even though that could be potentially abused on monitors * running at less than 60Hz because 1 is the default value - many users * wouldn't know how to change it to auto. */ if (settings->uints.video_swap_interval > 1) { const char* error = "Hardcore paused. vsync swap interval above 1 not allowed."; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", error); rcheevos_pause_hardcore(); runloop_msg_queue_push(error, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); return; } /* this causes N blank frames to be rendered between real frames, thus * can slow down the actual number of rendered frames per second. */ if (settings->uints.video_black_frame_insertion > 0) { const char* error = "Hardcore paused. Black frame insertion not allowed."; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", error); rcheevos_pause_hardcore(); runloop_msg_queue_push(error, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); return; } /* this causes N dupe frames to be rendered between real frames, for the purposes of shaders that update faster than content. Thus * can slow down the actual number of rendered frames per second. */ if (settings->uints.video_shader_subframes > 1) { const char* error = "Hardcore paused. Shader subframes not allowed."; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", error); rcheevos_pause_hardcore(); runloop_msg_queue_push(error, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); return; } if (!(disallowed_settings = rc_libretro_get_disallowed_settings(sysinfo->library_name))) return; if (!retroarch_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[128]; 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; } } #ifdef HAVE_RC_CLIENT { const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals.client); console_id = game ? game->console_id : 0; } #else console_id = rcheevos_locals.game.console_id; #endif if (console_id && !rc_libretro_is_system_allowed(sysinfo->library_name, console_id)) { char buffer[256]; buffer[0] = '\0'; /* TODO/FIXME - localize */ snprintf(buffer, sizeof(buffer), "Hardcore paused. You cannot earn hardcore achievements for %s using %s", rc_console_name(console_id), sysinfo->library_name); 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); return; } } #ifndef HAVE_RC_CLIENT 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; case RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED: rcheevos_progress_updated(&rcheevos_locals, 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(uint32_t address) { return rc_libretro_memory_find( &rcheevos_locals.memory, address) != NULL; } static void rcheevos_validate_memrefs(rcheevos_locals_t* locals) { if (!rcheevos_init_memory(locals)) { const settings_t* settings = config_get_ptr(); /* 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. if we're still loading the game, * just reset the memory count and we'll re-evaluate in * rcheevos_test() */ if (!locals->loaded) { /* If no memory was exposed, report the error now * instead of waiting */ if (locals->memory.total_size != 0) { locals->memory.count = 0; return; } } 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; } rc_runtime_validate_addresses(&locals->runtime, rcheevos_runtime_event_handler, rcheevos_runtime_address_validator); } #endif /* HAVE_RC_CLIENT */ /***************************************************************************** Test all the achievements (call once per frame). *****************************************************************************/ void rcheevos_test(void) { #ifdef HAVE_THREADS if (rcheevos_locals.queued_command != CMD_EVENT_NONE) { if ((int)rcheevos_locals.queued_command != CMD_CHEEVOS_NON_COMMAND) command_event(rcheevos_locals.queued_command, NULL); rcheevos_locals.queued_command = CMD_EVENT_NONE; if (rcheevos_locals.game_placard_requested) { rcheevos_locals.game_placard_requested = false; rcheevos_show_game_placard(); } } #endif #ifdef HAVE_RC_CLIENT if (rcheevos_locals.memory.count != 0) rc_client_do_frame(rcheevos_locals.client); else rc_client_idle(rcheevos_locals.client); #else if (!rcheevos_locals.loaded) return; /* We were unable to initialize memory earlier, try now */ if (rcheevos_locals.memory.count == 0) { rcheevos_validate_memrefs(&rcheevos_locals); /* rcheevos_validate_memrefs may decide the core doesn't support achievements and * disable them. if so, bail. */ if (!rcheevos_locals.loaded) return; } rc_runtime_do_frame(&rcheevos_locals.runtime, &rcheevos_runtime_event_handler, rcheevos_peek, NULL, 0); #ifdef HAVE_GFX_WIDGETS if (rcheevos_locals.assign_new_trackers) { if (gfx_widgets_ready()) rcheevos_assign_leaderboard_tracker_ids(&rcheevos_locals); rcheevos_locals.assign_new_trackers = false; } if (rcheevos_locals.tracker_achievement != NULL) { char buffer[32] = ""; if (rc_runtime_format_achievement_measured(&rcheevos_locals.runtime, rcheevos_locals.tracker_achievement->id, buffer, sizeof(buffer))) { gfx_widget_set_achievement_progress(rcheevos_locals.tracker_achievement->badge, buffer); } rcheevos_locals.tracker_achievement = NULL; rcheevos_locals.tracker_progress = 0.0; } #endif /* We processed a frame - if there's a pause delay in effect, process it */ if (rcheevos_locals.unpaused_frames > 0) rcheevos_locals.unpaused_frames--; #endif /* HAVE_RC_CLIENT */ } void rcheevos_idle(void) { #ifdef HAVE_RC_CLIENT rc_client_idle(rcheevos_locals.client); #endif } size_t rcheevos_get_serialize_size(void) { #ifdef HAVE_RC_CLIENT return rc_client_progress_size(rcheevos_locals.client); #else if (!rcheevos_locals.loaded) return 0; return rc_runtime_progress_size(&rcheevos_locals.runtime, NULL); #endif } bool rcheevos_get_serialized_data(void* buffer) { #ifdef HAVE_RC_CLIENT return (rc_client_serialize_progress(rcheevos_locals.client, (uint8_t*)buffer) == RC_OK); #else if (!rcheevos_locals.loaded) return false; return (rc_runtime_serialize_progress( buffer, &rcheevos_locals.runtime, NULL) == RC_OK); #endif } bool rcheevos_set_serialized_data(void* buffer) { if (rcheevos_is_game_loaded() && buffer) { #ifdef HAVE_RC_CLIENT const int result = rc_client_deserialize_progress( rcheevos_locals.client, (const uint8_t*)buffer); #else const int result = rc_runtime_deserialize_progress( &rcheevos_locals.runtime, (const unsigned char*)buffer, NULL); #if defined(HAVE_GFX_WIDGETS) if (gfx_widgets_ready() && rcheevos_is_player_active()) { settings_t* settings = config_get_ptr(); if (settings->bools.cheevos_visibility_lboard_trackers) rcheevos_leaderboard_trackers_visibility_changed(); if (settings->bools.cheevos_challenge_indicators) { unsigned i; rc_runtime_trigger_t* cheevo = rcheevos_locals.runtime.triggers; for (i = 0; i < rcheevos_locals.runtime.trigger_count; ++i, ++cheevo) { if (!cheevo->trigger) continue; if (cheevo->trigger->state == RC_TRIGGER_STATE_PRIMED) { rcheevos_racheevo_t* racheevo = rcheevos_find_cheevo(cheevo->id); if (racheevo != NULL) gfx_widgets_set_challenge_display(racheevo->id, racheevo->badge); } else { gfx_widgets_set_challenge_display(cheevo->id, NULL); } } } if (settings->bools.cheevos_visibility_progress_tracker) gfx_widget_set_achievement_progress(NULL, NULL); } #endif #endif /* HAVE_RC_CLIENT */ return (result == RC_OK); } 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) { #ifdef HAVE_RC_CLIENT const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals.client); return game ? game->hash : msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_AVAILABLE); #else return (rcheevos_locals.game.hash != NULL) ? rcheevos_locals.game.hash : msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_AVAILABLE); #endif } /* 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); } #ifdef HAVE_CHD static void* rc_hash_handle_chd_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: cdfs_track = cdfs_open_track(path, CHDSTREAM_TRACK_LAST); break; case RC_HASH_CDTRACK_LARGEST: cdfs_track = cdfs_open_track(path, CHDSTREAM_TRACK_PRIMARY); 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_chd_read_sector( void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) { cdfs_file_t* file = (cdfs_file_t*)track_handle; uint32_t track_sectors = cdfs_get_num_sectors(file); sector -= cdfs_get_first_sector(file); if (sector >= track_sectors) return 0; cdfs_seek_sector(file, sector); return cdfs_read_file(file, buffer, requested_bytes); } static uint32_t rc_hash_handle_chd_first_track_sector( void* track_handle) { cdfs_file_t* file = (cdfs_file_t*)track_handle; return cdfs_get_first_sector(file); } static void rc_hash_handle_chd_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); } } #endif static void rc_hash_reset_cdreader_hooks(void); static void* rc_hash_handle_cd_open_track( const char* path, uint32_t track) { struct rc_hash_filereader filereader; struct rc_hash_cdreader cdreader; 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); if (string_is_equal_noncase(path_get_extension(path), "chd")) { #ifdef HAVE_CHD /* special handlers for CHD file */ memset(&cdreader, 0, sizeof(cdreader)); cdreader.open_track = rc_hash_handle_cd_open_track; cdreader.read_sector = rc_hash_handle_chd_read_sector; cdreader.close_track = rc_hash_handle_chd_close_track; cdreader.first_track_sector = rc_hash_handle_chd_first_track_sector; rc_hash_init_custom_cdreader(&cdreader); return rc_hash_handle_chd_open_track(path, track); #else CHEEVOS_LOG(RCHEEVOS_TAG "Cannot generate hash from CHD without HAVE_CHD compile flag\n"); return NULL; #endif } else { /* not a CHD file, use the default handlers */ rc_hash_get_default_cdreader(&cdreader); rc_hash_reset_cdreader_hooks(); return cdreader.open_track(path, track); } } static void rc_hash_reset_cdreader_hooks(void) { struct rc_hash_cdreader cdreader; rc_hash_get_default_cdreader(&cdreader); cdreader.open_track = rc_hash_handle_cd_open_track; rc_hash_init_custom_cdreader(&cdreader); } /* end hooks */ #ifdef HAVE_RC_CLIENT static void rcheevos_show_game_placard(void) { char msg[256], unsupported_clause[64] = ""; const settings_t* settings = config_get_ptr(); rc_client_user_game_summary_t summary; const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals.client); if (!game) /* make sure there's actually a game loaded */ return; rc_client_get_user_game_summary(rcheevos_locals.client, &summary); if (summary.num_unsupported_achievements) { snprintf(unsupported_clause, sizeof(unsupported_clause), " (%d unsupported)", (int)summary.num_unsupported_achievements); } /* TODO/FIXME - localize strings */ if (summary.num_core_achievements == 0) { if (summary.num_unofficial_achievements == 0) strlcpy(msg, "This game has no achievements.", sizeof(msg)); else snprintf(msg, sizeof(msg), "Activated %d unofficial achievements%s.", (int)summary.num_unofficial_achievements, unsupported_clause); } else if (rc_client_get_encore_mode_enabled(rcheevos_locals.client)) { snprintf(msg, sizeof(msg), "All %d achievements activated for this session%s.", (int)summary.num_core_achievements, unsupported_clause); } else { snprintf(msg, sizeof(msg), "You have %d of %d achievements unlocked%s.", (int)summary.num_unlocked_achievements, (int)summary.num_core_achievements, unsupported_clause); } msg[sizeof(msg) - 1] = 0; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); if (settings->uints.cheevos_visibility_summary == RCHEEVOS_SUMMARY_ALLGAMES || (settings->uints.cheevos_visibility_summary == RCHEEVOS_SUMMARY_HASCHEEVOS && (summary.num_core_achievements || summary.num_unofficial_achievements))) { #if defined (HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) { char badge_name[32]; snprintf(badge_name, sizeof(badge_name), "i%s", game->badge_name); gfx_widgets_push_achievement(game->title, msg, badge_name); } else #endif runloop_msg_queue_push(msg, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } static uint32_t rcheevos_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) { return rc_libretro_memory_read(&rcheevos_locals.memory, address, buffer, num_bytes); } static uint32_t rcheevos_client_read_memory_dummy(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) { /* pretend the memory exists */ memset(buffer, 0, num_bytes); return num_bytes; } static uint32_t rcheevos_client_read_memory_unavailable(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) { return 0; } static uint32_t rcheevos_client_read_memory_uninitialized(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) { /* we can't initialize the memory until we know which console the game * is associated to. This happens internally to the load game sequence, * so we have to intercept the first attempt to read memory. */ if (rcheevos_init_memory(&rcheevos_locals)) { rc_client_set_read_memory_function(client, rcheevos_client_read_memory); return rcheevos_client_read_memory(address, buffer, num_bytes, client); } /* 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. if we're still loading the game, return * dummy memory and we'll re-evaluate in rcheevos_client_load_game_callback(). */ if (!rcheevos_is_game_loaded()) { rc_client_set_read_memory_function(client, rcheevos_client_read_memory_dummy); return rcheevos_client_read_memory_dummy(address, buffer, num_bytes, client); } /* game loaded, but no memory available */ rc_client_set_read_memory_function(client, rcheevos_client_read_memory_unavailable); return rcheevos_client_read_memory_unavailable(address, buffer, num_bytes, client); } static void rcheevos_client_login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { const rc_client_user_t* user; char msg[256] = ""; if (result != RC_OK) { snprintf(msg, sizeof(msg), "RetroAchievements login failed: %s", error_message); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); return; } user = rc_client_get_user_info(client); if (!user) { CHEEVOS_LOG(RCHEEVOS_TAG "Login failed without error\n"); } else { settings_t* settings = config_get_ptr(); if (user->token[0]) { /* store the token, clear the password */ strlcpy(settings->arrays.cheevos_token, user->token, sizeof(settings->arrays.cheevos_token)); settings->arrays.cheevos_password[0] = '\0'; } else { CHEEVOS_LOG(RCHEEVOS_TAG "Login did not return token\n"); } /* show notification (if enabled) */ if (settings->bools.cheevos_visibility_account) { /* TODO/FIXME - localize */ snprintf(msg, sizeof(msg), "RetroAchievements: Logged in as \"%s\".", user->display_name); runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } } static void rcheevos_finalize_game_load(rc_client_t* client) { rcheevos_client_download_achievement_badges(client); if (!rc_client_is_processing_required(client)) { CHEEVOS_LOG(RCHEEVOS_TAG "No runtime logic for game, pausing hardcore\n"); rcheevos_pause_hardcore(); } } static void rcheevos_client_load_game_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { const settings_t* settings = config_get_ptr(); const rc_client_game_t* game = rc_client_get_game_info(client); char msg[256]; #if defined(HAVE_GFX_WIDGETS) gfx_widget_set_cheevos_set_loading(false); #endif if (result != RC_OK || !game) { if (result == RC_NO_GAME_LOADED) { CHEEVOS_LOG(RCHEEVOS_TAG "Game not recognized, pausing hardcore\n"); rcheevos_pause_hardcore(); if (!settings->bools.cheevos_verbose_enable) return; snprintf(msg, sizeof(msg), "RetroAchievements: Game could not be identified."); } else { snprintf(msg, sizeof(msg), "RetroAchievements game load failed: %s", error_message ? error_message : "Unknown error"); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); } runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); return; } if (rcheevos_locals.memory.total_size == 0) { /* make one last attempt to initialize memory */ if (!rcheevos_init_memory(&rcheevos_locals)) { 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; } /* have valid memory now. use the real read function */ rc_client_set_read_memory_function(client, rcheevos_client_read_memory); } #ifdef HAVE_THREADS if (!video_driver_is_threaded() && !task_is_on_main_thread()) { /* have to "schedule" this. game image should not be loaded on background thread */ rcheevos_locals.queued_command = CMD_CHEEVOS_NON_COMMAND; rcheevos_locals.game_placard_requested = true; } else #endif rcheevos_show_game_placard(); rcheevos_finalize_game_load(client); if (rcheevos_hardcore_active()) { /* hardcore is active. we're going to start processing * achievements. make sure restrictions are enforced */ rcheevos_enforce_hardcore_settings(); } else { #if HAVE_REWIND /* Re-enable rewind. Additional space will be allocated for the achievement state data */ if (settings->bools.rewind_enable) { #ifdef HAVE_THREADS if (!task_is_on_main_thread()) { /* Have to "schedule" this. CMD_EVENT_REWIND_REINIT should * only be called on the main thread */ rcheevos_locals.queued_command = CMD_EVENT_REWIND_REINIT; } else #endif command_event(CMD_EVENT_REWIND_REINIT, NULL); } #endif } rcheevos_spectating_changed(); /* synchronize spectating state */ } static rc_clock_t rcheevos_client_get_time_millisecs(const rc_client_t* client) { return cpu_features_get_time_usec() / 1000; } #else /* !HAVE_RC_CLIENT */ void rcheevos_show_mastery_placard(void) { char title[256]; const settings_t* settings = config_get_ptr(); if (rcheevos_locals.game.mastery_placard_shown) return; rcheevos_locals.game.mastery_placard_shown = true; snprintf(title, sizeof(title), msg_hash_to_str(rcheevos_locals.hardcore_active ? MSG_CHEEVOS_MASTERED_GAME : MSG_CHEEVOS_COMPLETED_GAME), rcheevos_locals.game.title); title[sizeof(title) - 1] = '\0'; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", title); if (settings->bools.cheevos_visibility_mastery) { #if defined (HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) { const bool content_runtime_log = settings->bools.content_runtime_log; const bool content_runtime_log_aggr = settings->bools.content_runtime_log_aggregate; char msg[128]; size_t len = strlcpy(msg, rcheevos_locals.displayname, sizeof(msg)); if (len < sizeof(msg) - 12 && (content_runtime_log || content_runtime_log_aggr)) { const char* content_path = path_get(RARCH_PATH_CONTENT); const char* core_path = path_get(RARCH_PATH_CORE); runtime_log_t* runtime_log = runtime_log_init( content_path, core_path, settings->paths.directory_runtime_log, settings->paths.directory_playlist, !content_runtime_log_aggr); if (runtime_log) { const runloop_state_t* runloop_state = runloop_state_get_ptr(); runtime_log_add_runtime_usec(runtime_log, runloop_state->core_runtime_usec); len += snprintf(msg + len, sizeof(msg) - len, " | "); runtime_log_get_runtime_str(runtime_log, msg + len, sizeof(msg) - len); msg[sizeof(msg) - 1] = '\0'; free(runtime_log); } } gfx_widgets_push_achievement(title, msg, rcheevos_locals.game.badge_name); } else #endif runloop_msg_queue_push(title, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } static void rcheevos_show_game_placard(void) { char msg[256]; const settings_t* settings = config_get_ptr(); const rcheevos_racheevo_t* cheevo = rcheevos_locals.game.achievements; const rcheevos_racheevo_t* end = cheevo + rcheevos_locals.game.achievement_count; int number_of_active = 0; int number_of_unsupported = 0; int number_of_core = 0; int mode = RCHEEVOS_ACTIVE_SOFTCORE; if (rcheevos_locals.game.id < 0) /* make sure there's actually a game loaded */ return; if (rcheevos_locals.hardcore_active) mode = RCHEEVOS_ACTIVE_HARDCORE; for (; cheevo < end; cheevo++) { if (cheevo->active & RCHEEVOS_ACTIVE_UNOFFICIAL) continue; number_of_core++; if (cheevo->active & RCHEEVOS_ACTIVE_UNSUPPORTED) number_of_unsupported++; else if (cheevo->active & mode) number_of_active++; } /* TODO/FIXME - localize strings */ if (number_of_core == 0) strlcpy(msg, "This game has no achievements.", sizeof(msg)); else if (!number_of_unsupported) { if (settings->bools.cheevos_start_active) snprintf(msg, sizeof(msg), "All %d achievements activated for this session.", number_of_core); else snprintf(msg, sizeof(msg), "You have %d of %d achievements unlocked.", number_of_core - number_of_active, number_of_core); } else { if (settings->bools.cheevos_start_active) snprintf(msg, sizeof(msg), "All %d achievements activated for this session (%d unsupported).", number_of_core, number_of_unsupported); else snprintf(msg, sizeof(msg), "You have %d of %d achievements unlocked (%d unsupported).", number_of_core - number_of_active - number_of_unsupported, number_of_core, number_of_unsupported); } msg[sizeof(msg) - 1] = 0; CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); if (settings->uints.cheevos_visibility_summary == RCHEEVOS_SUMMARY_ALLGAMES || (number_of_core > 0 && settings->uints.cheevos_visibility_summary == RCHEEVOS_SUMMARY_HASCHEEVOS)) { #if defined (HAVE_GFX_WIDGETS) if (gfx_widgets_ready()) gfx_widgets_push_achievement(rcheevos_locals.game.title, msg, rcheevos_locals.game.badge_name); else #endif runloop_msg_queue_push(msg, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } static void rcheevos_end_load(void) { #ifdef HAVE_GFX_WIDGETS rcheevos_ralboard_t* lboard = rcheevos_locals.game.leaderboards; rcheevos_ralboard_t* stop = lboard + rcheevos_locals.game.leaderboard_count; const char* ptr; unsigned hash; for (; lboard < stop; ++lboard) { lboard->value_hash = 0; lboard->active_tracker_id = 0; ptr = lboard->mem; if (!ptr) continue; ptr = strstr(ptr, "VAL:"); if (!ptr) continue; ptr += 4; /* calculate the DJB2 hash of the VAL portion of the string*/ hash = 5381; while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) hash = (hash << 5) + hash + *ptr++; lboard->value_hash = hash; } #endif CHEEVOS_LOG(RCHEEVOS_TAG "Load finished\n"); rcheevos_locals.load_info.state = RCHEEVOS_LOAD_STATE_DONE; } static void rcheevos_fetch_badges_callback(void* userdata) { rcheevos_end_load(); } static void rcheevos_fetch_badges(void) { /* this function manages the * RCHEEVOS_LOAD_STATE_FETCHING_BADGES state */ rcheevos_client_fetch_badges(rcheevos_fetch_badges_callback, NULL); } static void rcheevos_start_session_async(retro_task_t* task) { const bool needs_runtime = ( rcheevos_locals.game.achievement_count > 0 || rcheevos_locals.game.leaderboard_count > 0 || rcheevos_locals.runtime.richpresence); if (rcheevos_load_aborted()) return; /* We don't have to wait for this to complete * to proceed to the next loading state */ rcheevos_client_start_session(rcheevos_locals.game.id); rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_STARTING_SESSION); if (needs_runtime) { /* activate the achievements and leaderboards * (rich presence has already been activated) */ rcheevos_activate_achievements(); if (rcheevos_locals.hardcore_active) rcheevos_activate_leaderboards(); /* disable any unsupported achievements */ rcheevos_validate_memrefs(&rcheevos_locals); /* Let the runtime start processing the achievements */ 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 */ const settings_t* settings = config_get_ptr(); if (settings->bools.rewind_enable) { #ifdef HAVE_THREADS if (!task_is_on_main_thread()) { /* 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 #endif command_event(CMD_EVENT_REWIND_INIT, NULL); } } #endif /* If there's nothing for the runtime to process, * disable hardcore. */ if (!needs_runtime) rcheevos_pause_hardcore(); /* hardcore is active. we're going to start processing * achievements. make sure restrictions are enforced */ else if (rcheevos_locals.hardcore_active) runloop_state_get_ptr()->flags &= ~RUNLOOP_FLAG_SLOWMOTION; task_set_finished(task, true); if (rcheevos_end_load_state() == 0) rcheevos_fetch_badges(); } static void rcheevos_start_session_finish(retro_task_t* task, void* data, void* userdata, const char* error) { (void)task; (void)data; (void)userdata; (void)error; /* this must be called on the main thread */ rcheevos_show_game_placard(); } static void rcheevos_start_session(void) { retro_task_t* task; if (rcheevos_load_aborted()) { CHEEVOS_LOG(RCHEEVOS_TAG "Load aborted before starting session\n"); return; } /* re-validate the config settings now that we know * which console_id is active */ rcheevos_validate_config_settings(); task = task_init(); task->handler = rcheevos_start_session_async; task->callback = rcheevos_start_session_finish; task_queue_push(task); } static void rcheevos_initialize_runtime_callback(void* userdata) { rcheevos_start_session(); } static void rcheevos_fetch_game_data(void) { if (rcheevos_load_aborted()) { rcheevos_locals.game.hash = NULL; rcheevos_pause_hardcore(); return; } if (rcheevos_locals.game.id <= 0) { const settings_t* settings = config_get_ptr(); if (settings->bools.cheevos_verbose_enable) runloop_msg_queue_push( "RetroAchievements: Game could not be identified.", 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); CHEEVOS_LOG(RCHEEVOS_TAG "Game could not be identified\n"); if (rcheevos_locals.load_info.hashes_tried > 1) rcheevos_locals.game.hash = NULL; rcheevos_locals.load_info.state = RCHEEVOS_LOAD_STATE_UNKNOWN_GAME; rcheevos_pause_hardcore(); return; } if (!rcheevos_locals.token[0]) { rcheevos_locals.load_info.state = RCHEEVOS_LOAD_STATE_LOGIN_FAILED; rcheevos_pause_hardcore(); return; } /* fetch the game data and the user unlocks */ rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_FETCHING_GAME_DATA); #if HAVE_REWIND if (!rcheevos_locals.hardcore_active) { /* deactivate rewind while we activate the achievements */ const settings_t* settings = config_get_ptr(); if (settings->bools.rewind_enable) { #ifdef HAVE_THREADS if (!task_is_on_main_thread()) { /* 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) retro_sleep(1); } else #endif command_event(CMD_EVENT_REWIND_DEINIT, NULL); } } #endif rcheevos_client_initialize_runtime(rcheevos_locals.game.id, rcheevos_initialize_runtime_callback, NULL); if (rcheevos_end_load_state() == 0) rcheevos_start_session(); } struct rcheevos_identify_game_data { struct rc_hash_iterator iterator; char* path; uint8_t* datacopy; char hash[33]; }; static void rcheevos_identify_game_callback(void* userdata) { struct rcheevos_identify_game_data* data = (struct rcheevos_identify_game_data*)userdata; rcheevos_locals.load_info.hashes_tried++; if (rcheevos_locals.game.id == 0) { /* previous hash didn't match, try the next one */ char new_hash[33]; int found_new_hash; while ((found_new_hash = rc_hash_iterate(new_hash, &data->iterator)) != 0) { if (!rc_libretro_hash_set_get_game_id(&rcheevos_locals.game.hashes, new_hash)) break; CHEEVOS_LOG(RCHEEVOS_TAG "Ignoring [%s]. Already tried.\n", new_hash); } if (found_new_hash) { memcpy(data->hash, new_hash, sizeof(data->hash)); rcheevos_client_identify_game(data->hash, rcheevos_identify_game_callback, data); return; } } rc_libretro_hash_set_add(&rcheevos_locals.game.hashes, data->path, rcheevos_locals.game.id, data->hash); rcheevos_locals.game.hash = rc_libretro_hash_set_get_hash(&rcheevos_locals.game.hashes, data->path); if (data->iterator.path && strcmp(data->iterator.path, data->path) != 0) { rc_libretro_hash_set_add(&rcheevos_locals.game.hashes, data->iterator.path, rcheevos_locals.game.id, data->hash); rcheevos_locals.game.hash = rc_libretro_hash_set_get_hash(&rcheevos_locals.game.hashes, data->iterator.path); } /* no more hashes generated, free the iterator data */ rc_hash_destroy_iterator(&data->iterator); if (data->datacopy) free(data->datacopy); if (data->path) free(data->path); free(data); /* hash resolution complete, proceed to fetching game data */ if (rcheevos_end_load_state() == 0) rcheevos_fetch_game_data(); } static int rcheevos_get_image_path(uint32_t index, char* buffer, size_t buffer_size) { rarch_system_info_t *sys_info = &runloop_state_get_ptr()->system; if (!sys_info->disk_control.cb.get_image_path) return 0; return sys_info->disk_control.cb.get_image_path(index, buffer, buffer_size); } static bool rcheevos_identify_game(const struct retro_game_info* info) { struct rcheevos_identify_game_data* data; struct rc_hash_filereader filereader; size_t len; #ifndef DEBUG settings_t* settings = config_get_ptr(); #endif data = (struct rcheevos_identify_game_data*) calloc(1, sizeof(struct rcheevos_identify_game_data)); if (!data) { CHEEVOS_LOG(RCHEEVOS_TAG "allocation failed\n"); 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); 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); } rc_hash_reset_cdreader_hooks(); /* fetch the first hash */ rc_hash_initialize_iterator(&data->iterator, info->path, (uint8_t*)info->data, info->size); if (!rc_hash_iterate(data->hash, &data->iterator)) { CHEEVOS_LOG(RCHEEVOS_TAG "no hashes generated\n"); rc_hash_destroy_iterator(&data->iterator); free(data); return false; } rc_libretro_hash_set_init(&rcheevos_locals.game.hashes, info->path, rcheevos_get_image_path); data->path = strdup(info->path); if (data->iterator.consoles[data->iterator.index] != 0) { /* multiple potential matches, clone the data for the next attempt */ if (info->data) { len = info->size; if (len > CHEEVOS_MB(64)) len = CHEEVOS_MB(64); data->datacopy = (uint8_t*)malloc(len); if (!data->datacopy) { CHEEVOS_LOG(RCHEEVOS_TAG "allocation failed\n"); rc_hash_destroy_iterator(&data->iterator); free(data); return false; } memcpy(data->datacopy, info->data, len); data->iterator.buffer = data->datacopy; } } rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_IDENTIFYING_GAME); rcheevos_client_identify_game(data->hash, rcheevos_identify_game_callback, data); return true; } static void rcheevos_login_callback(void* userdata) { if (rcheevos_locals.token[0]) { const settings_t* settings = config_get_ptr(); if (settings->bools.cheevos_visibility_account) { char msg[256]; msg[0] = '\0'; /* TODO/FIXME - localize */ snprintf(msg, sizeof(msg), "RetroAchievements: Logged in as \"%s\".", rcheevos_locals.displayname); runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } if (rcheevos_end_load_state() == 0) rcheevos_fetch_game_data(); } /* Increment the outstanding requests counter and set the load state */ void rcheevos_begin_load_state(enum rcheevos_load_state state) { #ifdef HAVE_THREADS slock_lock(rcheevos_locals.load_info.request_lock); #endif ++rcheevos_locals.load_info.outstanding_requests; rcheevos_locals.load_info.state = state; #ifdef HAVE_THREADS slock_unlock(rcheevos_locals.load_info.request_lock); #endif } /* Decrement and return the outstanding requests counter. * If non-zero, requests are still outstanding */ int rcheevos_end_load_state(void) { int requests = 0; #ifdef HAVE_THREADS slock_lock(rcheevos_locals.load_info.request_lock); #endif if (rcheevos_locals.load_info.outstanding_requests > 0) --rcheevos_locals.load_info.outstanding_requests; requests = rcheevos_locals.load_info.outstanding_requests; #ifdef HAVE_THREADS slock_unlock(rcheevos_locals.load_info.request_lock); #endif return requests; } bool rcheevos_load_aborted(void) { switch (rcheevos_locals.load_info.state) { /* Unload has been called */ case RCHEEVOS_LOAD_STATE_ABORTED: /* Unload quit waiting and ran to completion */ case RCHEEVOS_LOAD_STATE_NONE: /* Login/resolve hash failed after several attempts */ case RCHEEVOS_LOAD_STATE_NETWORK_ERROR: return true; default: break; } return false; } #endif /* HAVE_RC_CLIENT */ bool rcheevos_load(const void *data) { const struct retro_game_info *info = (const struct retro_game_info*)data; settings_t *settings = config_get_ptr(); bool cheevos_enable = settings && settings->bools.cheevos_enable; #ifndef HAVE_RC_CLIENT memset(&rcheevos_locals.load_info, 0, sizeof(rcheevos_locals.load_info)); rcheevos_locals.loaded = false; rcheevos_locals.game.id = -1; rcheevos_locals.game.console_id = 0; rcheevos_locals.game.mastery_placard_shown = false; #ifdef HAVE_GFX_WIDGETS rcheevos_locals.tracker_progress = 0.0; #endif rc_runtime_init(&rcheevos_locals.runtime); #endif /* HAVE_RC_CLIENT */ #ifdef HAVE_THREADS rcheevos_locals.queued_command = CMD_EVENT_NONE; #endif /* If achievements are not enabled, or the core doesn't * support achievements, disable hardcore and bail */ if (!cheevos_enable || !rcheevos_locals.core_supports || !data) { #ifndef HAVE_RC_CLIENT rcheevos_locals.game.id = 0; #endif rcheevos_pause_hardcore(); return false; } if (string_is_empty(settings->arrays.cheevos_username)) { CHEEVOS_LOG(RCHEEVOS_TAG "Cannot login (no username)\n"); runloop_msg_queue_push("Missing RetroAchievements account information.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); #ifndef HAVE_RC_CLIENT rcheevos_locals.game.id = 0; #endif rcheevos_pause_hardcore(); return false; } #ifdef HAVE_RC_CLIENT /* Refresh the user agent in case it's not set or has changed */ rcheevos_get_user_agent(&rcheevos_locals, rcheevos_locals.user_agent_core, sizeof(rcheevos_locals.user_agent_core)); if (rcheevos_locals.client) { rc_client_unload_game(rcheevos_locals.client); } else { rcheevos_locals.client = rc_client_create(rcheevos_client_read_memory, rcheevos_client_server_call); rc_client_enable_logging(rcheevos_locals.client, RC_CLIENT_LOG_LEVEL_VERBOSE, rcheevos_client_log_message); rc_client_set_event_handler(rcheevos_locals.client, rcheevos_client_event_handler); rc_client_set_get_time_millisecs_function(rcheevos_locals.client, rcheevos_client_get_time_millisecs); { const char* host = settings->arrays.cheevos_custom_host; if (!host[0]) { #ifdef HAVE_SSL host = "https://retroachievements.org"; #else host = "http://retroachievements.org"; #endif } rc_client_set_host(rcheevos_locals.client, host); } rcheevos_client_download_placeholder_badge(); } rc_client_set_hardcore_enabled(rcheevos_locals.client, settings->bools.cheevos_hardcore_mode_enable); rc_client_set_unofficial_enabled(rcheevos_locals.client, settings->bools.cheevos_test_unofficial); rc_client_set_encore_mode_enabled(rcheevos_locals.client, settings->bools.cheevos_start_active); rc_client_set_spectator_mode_enabled(rcheevos_locals.client, !rcheevos_is_player_active()); rc_client_set_read_memory_function(rcheevos_locals.client, rcheevos_client_read_memory_uninitialized); rcheevos_validate_config_settings(); CHEEVOS_LOG(RCHEEVOS_TAG "Load started, hardcore %sactive\n", rcheevos_hardcore_active() ? "" : "not "); if (!rc_client_get_user_info(rcheevos_locals.client)) { /* user not logged in, do so now */ if (settings->arrays.cheevos_token[0]) { rc_client_begin_login_with_token(rcheevos_locals.client, settings->arrays.cheevos_username, settings->arrays.cheevos_token, rcheevos_client_login_callback, NULL); } else { rc_client_begin_login_with_password(rcheevos_locals.client, settings->arrays.cheevos_username, settings->arrays.cheevos_password, rcheevos_client_login_callback, NULL); } } if (rcheevos_hardcore_active()) { rcheevos_enforce_hardcore_settings(); } #ifndef HAVE_RC_CLIENT else { #if HAVE_REWIND /* deactivate rewind while we activate the achievements */ const settings_t* settings = config_get_ptr(); if (settings->bools.rewind_enable) { #ifdef HAVE_THREADS if (!task_is_on_main_thread()) { /* 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) retro_sleep(1); } else #endif command_event(CMD_EVENT_REWIND_DEINIT, NULL); } #endif } #endif /* provide hooks for reading files */ rc_hash_reset_cdreader_hooks(); #if defined(HAVE_GFX_WIDGETS) if (settings->bools.cheevos_verbose_enable) gfx_widget_set_cheevos_set_loading(true); #endif rc_client_begin_identify_and_load_game(rcheevos_locals.client, RC_CONSOLE_UNKNOWN, info->path, info->data, info->size, rcheevos_client_load_game_callback, NULL); #else /* !HAVE_RC_CLIENT */ #ifdef HAVE_THREADS if (!rcheevos_locals.load_info.request_lock) rcheevos_locals.load_info.request_lock = slock_new(); #endif rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_IDENTIFYING_GAME); /* reset hardcore mode and leaderboard settings based on configs */ rcheevos_hardcore_enabled_changed(); CHEEVOS_LOG(RCHEEVOS_TAG "Load started, hardcore %sactive\n", rcheevos_hardcore_active() ? "" : "not "); rcheevos_validate_config_settings(); /* Refresh the user agent in case it's not set or has changed */ rcheevos_client_initialize(); rcheevos_get_user_agent(&rcheevos_locals, rcheevos_locals.user_agent_core, sizeof(rcheevos_locals.user_agent_core)); /* === ACHIEVEMENT INITIALIZATION PROCESS === 1. RCHEEVOS_LOAD_STATE_IDENTIFYING_GAME a. iterate possible hashes to identify game [rcheevos_identify_game] i. if game not found, display "no achievements for this game" and abort [rcheevos_identify_game_callback] b. Login i. if already logged in, skip this step ii. start login request [rcheevos_client_login_with_password/rcheevos_client_login_with_token] iii. complete login, store user/token [rcheevos_login_callback] 2. RCHEEVOS_LOAD_STATE_FETCHING_GAME_DATA [rcheevos_client_initialize_runtime] a. begin game data request [rc_api_init_fetch_game_data_request] b. fetch user unlocks i. if encore mode, skip this step ii. begin user unlocks hardcore request [rc_api_init_fetch_user_unlocks_request] iii. begin user unlocks softcore request [rc_api_init_fetch_user_unlocks_request] 3. RCHEEVOS_LOAD_STATE_STARTING_SESSION [rcheevos_initialize_runtime_callback] a. activate achievements [rcheevos_activate_achievements] b. schedule rich presence periodic update [rcheevos_client_start_session] c. start session on server [rcheevos_client_start_session] d. show title card [rcheevos_show_game_placard] 4. RCHEEVOS_LOAD_STATE_FETCHING_BADGES a. download from server [rcheevos_client_fetch_badges] 5. RCHEEVOS_LOAD_STATE_DONE */ /* Identify the game and log the user in. * These will run asynchronously. */ if (!rcheevos_identify_game(info)) { /* No hashes could be generated for the game, * disable hardcore and bail */ rcheevos_locals.game.id = 0; rcheevos_end_load_state(); rcheevos_pause_hardcore(); return false; } if (!rcheevos_locals.token[0]) { rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_IDENTIFYING_GAME); if (!string_is_empty(settings->arrays.cheevos_token)) { CHEEVOS_LOG(RCHEEVOS_TAG "Attempting to login %s (with token)\n", settings->arrays.cheevos_username); rcheevos_client_login_with_token( settings->arrays.cheevos_username, settings->arrays.cheevos_token, rcheevos_login_callback, NULL); } else if (!string_is_empty(settings->arrays.cheevos_password)) { CHEEVOS_LOG(RCHEEVOS_TAG "Attempting to login %s (with password)\n", settings->arrays.cheevos_username); rcheevos_client_login_with_password( settings->arrays.cheevos_username, settings->arrays.cheevos_password, rcheevos_login_callback, NULL); } else { CHEEVOS_LOG(RCHEEVOS_TAG "Cannot login %s (no password or token)\n", settings->arrays.cheevos_username); runloop_msg_queue_push("No password provided for RetroAchievements account", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); rcheevos_unload(); return false; } } if (rcheevos_end_load_state() == 0) rcheevos_fetch_game_data(); #endif /* HAVE_RC_CLIENT */ return true; } #ifdef HAVE_RC_CLIENT static void rcheevos_client_change_media_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { char msg[256]; if (result == RC_OK || result == RC_NO_GAME_LOADED) return; if (result == RC_HARDCORE_DISABLED) { strlcpy(msg, error_message, sizeof(msg)); rcheevos_hardcore_enabled_changed(); } else { if (!error_message) error_message = "Unknown error"; snprintf(msg, sizeof(msg), "RetroAchievements change media failed: %s", error_message); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg); } runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } void rcheevos_change_disc(const char* new_disc_path, bool initial_disc) { if (rcheevos_locals.client) { rc_client_begin_change_media(rcheevos_locals.client, new_disc_path, NULL, 0, rcheevos_client_change_media_callback, NULL); } } #else /* !HAVE_RC_CLIENT */ struct rcheevos_identify_changed_disc_data { int real_game_id; char* path; char hash[33]; }; static void rcheevos_identify_game_disc_callback(void* userdata) { struct rcheevos_identify_changed_disc_data* changed_disc_data = (struct rcheevos_identify_changed_disc_data*)userdata; /* rcheevos_locals.game.id has the game id for the new hash, swap it with the old game id */ const int hash_game_id = rcheevos_locals.game.id; rcheevos_locals.game.id = changed_disc_data->real_game_id; /* rcheevos_client_identify_game will update rcheevos_locals.game.id */ if (rcheevos_locals.game.id == hash_game_id) { CHEEVOS_LOG(RCHEEVOS_TAG "Hash valid for current game\n"); } else if (hash_game_id != 0) { /* when changing discs, if the disc is recognized but belongs to another game, allow it. * this allows loading known game discs for games that leverage user-provided discs. */ CHEEVOS_LOG(RCHEEVOS_TAG "Hash identified for game %d\n", hash_game_id); } else { CHEEVOS_LOG(RCHEEVOS_TAG "Disc not recognized\n"); if (rcheevos_hardcore_active()) { /* don't allow unknown game discs in hardcore. * assume it's a modified version of the base game. */ runloop_msg_queue_push("Hardcore paused. Game disc unrecognized.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); rcheevos_pause_hardcore(); } } /* disc is valid, add it to the known disk list */ rc_libretro_hash_set_add(&rcheevos_locals.game.hashes, changed_disc_data->path, hash_game_id, changed_disc_data->hash); rcheevos_locals.game.hash = rc_libretro_hash_set_get_hash(&rcheevos_locals.game.hashes, changed_disc_data->hash); free(changed_disc_data->path); free(changed_disc_data); } static void rcheevos_identify_initial_disc_callback(void* userdata) { struct rcheevos_identify_changed_disc_data* changed_disc_data = (struct rcheevos_identify_changed_disc_data*)userdata; /* rcheevos_client_identify_game will update rcheevos_locals.game.id */ if (rcheevos_locals.game.id != changed_disc_data->real_game_id) { if (rcheevos_locals.game.id == 0) { CHEEVOS_LOG(RCHEEVOS_TAG "Disc not recognized\n"); runloop_msg_queue_push("Disabling achievements. Game disc unrecognized.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); } else { CHEEVOS_LOG(RCHEEVOS_TAG "Initial disc for game %d\n", rcheevos_locals.game.id); runloop_msg_queue_push("Disabling achievements. Not for loaded game.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); } rcheevos_locals.game.hash = NULL; rcheevos_unload(); } else { /* disc is valid, add it to the known disk list */ CHEEVOS_LOG(RCHEEVOS_TAG "Hash valid for current game\n"); rc_libretro_hash_set_add(&rcheevos_locals.game.hashes, changed_disc_data->path, rcheevos_locals.game.id, changed_disc_data->hash); rcheevos_locals.game.hash = rc_libretro_hash_set_get_hash(&rcheevos_locals.game.hashes, changed_disc_data->hash); } free(changed_disc_data->path); free(changed_disc_data); } static void rcheevos_validate_initial_disc_handler(retro_task_t* task) { char* new_disc_path = (char*)task->user_data; if (rcheevos_locals.game.id == 0) { /* could not identify game. don't bother identifying initial disc */ } else { if (rcheevos_locals.game.console_id == 0) { /* not ready yet. try again in another 500ms */ task->when = cpu_features_get_time_usec() + 500 * 1000; return; } /* game ready. attempt to validate the initial disc */ rcheevos_change_disc(new_disc_path, true); } free(new_disc_path); task_set_finished(task, true); } void rcheevos_change_disc(const char* new_disc_path, bool initial_disc) { struct rcheevos_identify_changed_disc_data* data; char hash[33]; int hash_game_id; /* no game loaded */ if (rcheevos_locals.game.id == 0) return; /* see if we've already identified this file */ rcheevos_locals.game.hash = rc_libretro_hash_set_get_hash(&rcheevos_locals.game.hashes, new_disc_path); if (rcheevos_locals.game.hash) { CHEEVOS_LOG(RCHEEVOS_TAG "Switched to known hash: %s\n", rcheevos_locals.game.hash); return; } /* don't check the disc until the game is done loading */ if (rcheevos_locals.game.console_id == 0) { retro_task_t* task = task_init(); task->handler = rcheevos_validate_initial_disc_handler; task->user_data = strdup(new_disc_path); task->progress = -1; task->when = cpu_features_get_time_usec() + 500 * 1000; /* 500ms */ task_queue_push(task); return; } /* attempt to identify the file */ if (rc_hash_generate_from_file(hash, rcheevos_locals.game.console_id, new_disc_path)) { /* check to see if the hash is already known */ hash_game_id = rc_libretro_hash_set_get_game_id(&rcheevos_locals.game.hashes, hash); if (hash_game_id) { /* hash identical to some other file - probably the first disc matching the m3u. */ CHEEVOS_LOG(RCHEEVOS_TAG "Hash valid for current game\n"); } } else { /* when changing discs, if the disc is not supported by the system, allow it. this is * primarily for games that support user-provided audio CDs, but does allow using discs * from other systems for games that leverage user-provided discs. */ CHEEVOS_LOG(RCHEEVOS_TAG "No hash generated\n"); hash_game_id = -1; strlcpy(hash, "[NO HASH]", sizeof(hash)); } if (hash_game_id) { /* we know how to handle this disc. no need to call the server */ rc_libretro_hash_set_add(&rcheevos_locals.game.hashes, new_disc_path, hash_game_id, hash); rcheevos_locals.game.hash = rc_libretro_hash_set_get_hash(&rcheevos_locals.game.hashes, new_disc_path); return; } /* call the server to make sure the hash is valid for the loaded game */ data = (struct rcheevos_identify_changed_disc_data*) calloc(1, sizeof(struct rcheevos_identify_changed_disc_data)); if (data) { data->real_game_id = rcheevos_locals.game.id; data->path = strdup(new_disc_path); memcpy(data->hash, hash, sizeof(data->hash)); rcheevos_client_identify_game(data->hash, initial_disc ? rcheevos_identify_initial_disc_callback : rcheevos_identify_game_disc_callback, data); } } #endif /* HAVE_RC_CLIENT */