diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index 4ab541553d..764d8052d1 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -1,5 +1,6 @@ /* RetroArch - A frontend for libretro. - * Copyright (C) 2015-2016 - Andre Leiradella + * 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- @@ -82,15 +83,21 @@ 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 */ #endif +#ifndef HAVE_RC_CLIENT "", /* displayname */ "", /* username */ "", /* token */ +#endif "", /* user_agent_prefix */ "", /* user_agent_core */ #ifdef HAVE_MENU @@ -98,16 +105,20 @@ static rcheevos_locals_t rcheevos_locals = 0, /* menuitem_capacity */ 0, /* menuitem_count */ #endif -#ifdef HAVE_GFX_WIDGETS +#ifdef HAVE_RC_CLIENT + true,/* hardcore_allowed */ +#else + #ifdef HAVE_GFX_WIDGETS 0, /* active_lboard_trackers */ NULL, /* tracker_achievement */ 0.0, /* tracker_progress */ -#endif + #endif {RCHEEVOS_LOAD_STATE_NONE, 0, 0 }, /* load_info */ false,/* hardcore_active */ false,/* loaded */ -#ifdef HAVE_GFX_WIDGETS + #ifdef HAVE_GFX_WIDGETS false,/* assign_new_trackers */ + #endif #endif true /* core_supports */ }; @@ -124,12 +135,13 @@ Supporting functions. *****************************************************************************/ #ifndef CHEEVOS_VERBOSE -void rcheevos_log(const char *fmt, ...) +void rcheevos_log(const char* fmt, ...) { (void)fmt; } #endif +#ifndef HAVE_RC_CLIENT static void rcheevos_achievement_disabled( rcheevos_racheevo_t* cheevo, unsigned address) @@ -158,6 +170,8 @@ static void rcheevos_lboard_disabled( lboard->mem = NULL; } +#endif /* HAVE_RC_CLIENT */ + static void rcheevos_handle_log_message(const char* message) { CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message); @@ -188,9 +202,25 @@ 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 = (struct retro_memory_descriptor*)malloc(mmaps->num_descriptors * sizeof(*descriptors)); + struct retro_memory_descriptor* descriptors; + unsigned console_id; + +#ifdef HAVE_RC_CLIENT + /* we can't initialize memory without knowing which console to initialize for */ + game = rc_client_get_game_info(locals->client); + if (!game || !game->console_id) + return 0; + console_id = game->console_id; +#else + console_id = locals->game.console_id; +#endif + + descriptors = (struct retro_memory_descriptor*)malloc(mmaps->num_descriptors * sizeof(*descriptors)); if (!descriptors) return 0; @@ -205,7 +235,7 @@ static int rcheevos_init_memory(rcheevos_locals_t* locals) rc_libretro_init_verbose_message_callback(rcheevos_handle_log_message); result = rc_libretro_memory_init(&locals->memory, &mmap, - rcheevos_get_core_memory_info, locals->game.console_id); + rcheevos_get_core_memory_info, console_id); free(descriptors); return result; @@ -220,8 +250,10 @@ uint8_t* rcheevos_patch_address(unsigned address) 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 num_bytes, void* ud) { uint32_t avail; uint8_t* data = rc_libretro_memory_find_avail( @@ -299,9 +331,21 @@ static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id) return NULL; } +#endif + +static bool rcheevos_is_game_loaded(void) +{ +#ifdef HAVE_RC_CLIENT + const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals.client); + return (game && game->id); +#else + return rcheevos_locals.loaded; +#endif +} + static bool rcheevos_is_player_active(void) { - if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_SPECTATING, NULL)) + if (netplay_is_spectating()) return false; /* TODO: disallow player slots other than player one unless it's a [Multi] set */ @@ -309,6 +353,342 @@ static bool rcheevos_is_player_active(void) 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 +} + +#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); +} + +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_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: + CHEEVOS_LOG(RCHEEVOS_TAG "Unable to communicate with RetroAchievements server"); + break; + case RC_CLIENT_EVENT_RECONNECTED: + CHEEVOS_LOG(RCHEEVOS_TAG "All pending requests synced to RetroAchievements server"); + break; + default: +#ifndef NDEBUG + CHEEVOS_LOG(RCHEEVOS_TAG "Unsupported rc_client event %u\n", event->type); +#endif + break; + } +} + +#endif + +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 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) { @@ -719,27 +1099,35 @@ static void rcheevos_hide_leaderboard_trackers(void) } #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 */ - if (widgets_ready) - { - unsigned i; - rcheevos_racheevo_t* cheevo; - - rcheevos_hide_leaderboard_trackers(); - - cheevo = rcheevos_locals.game.achievements; - for (i = 0; i < rcheevos_locals.game.achievement_count; - ++i, ++cheevo) - gfx_widgets_set_challenge_display(cheevo->id, NULL); - - gfx_widget_set_achievement_progress(NULL, NULL); - } + 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 */ @@ -755,16 +1143,24 @@ void rcheevos_refresh_memory(void) bool rcheevos_hardcore_active(void) { +#ifdef HAVE_RC_CLIENT + return rcheevos_locals.client && rc_client_get_hardcore_enabled(rcheevos_locals.client); +#else return rcheevos_locals.hardcore_active; +#endif } void rcheevos_pause_hardcore(void) { - if (rcheevos_locals.hardcore_active) +#ifdef HAVE_RC_CLIENT + rcheevos_locals.hardcore_allowed = false; +#endif + + if (rcheevos_hardcore_active()) rcheevos_toggle_hardcore_paused(); } -#ifdef HAVE_THREADS +#if defined(HAVE_THREADS) && !defined(HAVE_RC_CLIENT) static bool rcheevos_timer_check(void* userdata) { retro_time_t stop_time = *(retro_time_t*)userdata; @@ -776,14 +1172,22 @@ static bool rcheevos_timer_check(void* userdata) 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()); +#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 + #ifdef HAVE_THREADS if (rcheevos_locals.load_info.state < RCHEEVOS_LOAD_STATE_DONE && rcheevos_locals.load_info.state != RCHEEVOS_LOAD_STATE_NONE) { @@ -800,14 +1204,18 @@ bool rcheevos_unload(void) } rcheevos_locals.queued_command = CMD_EVENT_NONE; + #endif #endif if (rcheevos_locals.memory.count > 0) rc_libretro_memory_destroy(&rcheevos_locals.memory); - if (rcheevos_locals.loaded) + if (was_loaded) { +#ifndef HAVE_RC_CLIENT unsigned count = 0; +#endif + #ifdef HAVE_MENU rcheevos_menu_reset_badges(); @@ -820,6 +1228,7 @@ bool rcheevos_unload(void) } #endif +#ifndef HAVE_RC_CLIENT count = rcheevos_locals.game.achievement_count; rcheevos_locals.game.achievement_count = 0; if (rcheevos_locals.game.achievements) @@ -869,23 +1278,39 @@ bool rcheevos_unload(void) 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 - rc_runtime_destroy(&rcheevos_locals.runtime); - - /* If the config-level token has been cleared, - * we need to re-login on loading the next game */ if (!settings->arrays.cheevos_token[0]) - rcheevos_locals.token[0] = '\0'; + { +#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) { @@ -1017,29 +1442,71 @@ void rcheevos_leaderboard_trackers_visibility_changed(void) } } +#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 (!locals->hardcore_active) + if (!was_enabled) { +#ifdef HAVE_RC_CLIENT + 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(); - if (!locals->hardcore_active) +#ifdef HAVE_RC_CLIENT + if (!locals->hardcore_allowed) return; - -#ifdef HAVE_CHEATS - /* If one or more emulator managed cheats is active, abort */ - cheat_manager_apply_cheats(); +#else if (!locals->hardcore_active) return; #endif - if (locals->loaded) +#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) + 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); @@ -1047,13 +1514,15 @@ static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals) runloop_msg_queue_push(msg, 0, 3 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); - runloop_state_get_ptr()->flags &= ~RUNLOOP_FLAG_SLOWMOTION; + 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 */ @@ -1071,10 +1540,17 @@ static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals) #endif command_event(CMD_EVENT_REWIND_DEINIT, NULL); } + +#ifdef HAVE_RC_CLIENT + 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) @@ -1084,6 +1560,7 @@ static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals) /* deactivate leaderboards */ rcheevos_deactivate_leaderboards(); } +#endif /* re-init rewind */ if (rewind_enable) @@ -1102,8 +1579,10 @@ static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals) } } +#ifndef HAVE_RC_CLIENT if (locals->loaded) rcheevos_toggle_hardcore_achievements(locals); +#endif } void rcheevos_toggle_hardcore_paused(void) @@ -1123,21 +1602,18 @@ void rcheevos_hardcore_enabled_changed(void) const bool enabled = settings && settings->bools.cheevos_enable && settings->bools.cheevos_hardcore_mode_enable; + const bool was_enabled = rcheevos_hardcore_active(); - if (enabled != rcheevos_locals.hardcore_active) + if (enabled != was_enabled) { rcheevos_toggle_hardcore_active(&rcheevos_locals); - - /* update leaderboard state */ - if (rcheevos_locals.hardcore_active) - rcheevos_activate_leaderboards(); - else - rcheevos_deactivate_leaderboards(); } - /* hardcore enabledness didn't change, but hardcore is active, so make - * sure to enforce the restrictions. */ - else if (rcheevos_locals.hardcore_active && rcheevos_locals.loaded) - runloop_state_get_ptr()->flags &= ~RUNLOOP_FLAG_SLOWMOTION; + 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) @@ -1149,8 +1625,9 @@ void rcheevos_validate_config_settings(void) 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_locals.hardcore_active) + if (!sysinfo->library_name || !rcheevos_hardcore_active()) return; /* this adds a sleep to every frame. if the value is high enough that a @@ -1224,15 +1701,23 @@ void rcheevos_validate_config_settings(void) } } - if (rcheevos_locals.game.console_id && - !rc_libretro_is_system_allowed(sysinfo->library_name, rcheevos_locals.game.console_id)) +#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(rcheevos_locals.game.console_id), sysinfo->library_name); + rc_console_name(console_id), sysinfo->library_name); CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", buffer); rcheevos_pause_hardcore(); @@ -1242,6 +1727,8 @@ void rcheevos_validate_config_settings(void) } } +#ifndef HAVE_RC_CLIENT + static void rcheevos_runtime_event_handler( const rc_runtime_event_t* runtime_event) { @@ -1369,6 +1856,8 @@ static void rcheevos_validate_memrefs(rcheevos_locals_t* locals) rcheevos_runtime_address_validator); } +#endif /* HAVE_RC_CLIENT */ + /***************************************************************************** Test all the achievements (call once per frame). *****************************************************************************/ @@ -1382,6 +1871,12 @@ void rcheevos_test(void) } #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; @@ -1399,7 +1894,7 @@ void rcheevos_test(void) rc_runtime_do_frame(&rcheevos_locals.runtime, &rcheevos_runtime_event_handler, rcheevos_peek, NULL, 0); -#ifdef HAVE_GFX_WIDGETS + #ifdef HAVE_GFX_WIDGETS if (rcheevos_locals.assign_new_trackers) { if (gfx_widgets_ready()) @@ -1420,32 +1915,52 @@ void rcheevos_test(void) rcheevos_locals.tracker_achievement = NULL; rcheevos_locals.tracker_progress = 0.0; } + #endif +#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_locals.loaded && 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 defined(HAVE_GFX_WIDGETS) if (gfx_widgets_ready() && rcheevos_is_player_active()) { settings_t* settings = config_get_ptr(); @@ -1478,7 +1993,8 @@ bool rcheevos_set_serialized_data(void* buffer) if (settings->bools.cheevos_visibility_progress_tracker) gfx_widget_set_achievement_progress(NULL, NULL); } -#endif + #endif +#endif /* HAVE_RC_CLIENT */ return (result == RC_OK); } @@ -1498,9 +2014,14 @@ bool rcheevos_get_support_cheevos(void) 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 */ @@ -1613,8 +2134,17 @@ 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 @@ -1651,6 +2181,279 @@ static void rc_hash_reset_cdreader_hooks(void) /* 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 (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); + } + + 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_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 + } + + 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]; @@ -2226,14 +3029,16 @@ bool rcheevos_load_aborted(void) return false; } +#endif /* HAVE_RC_CLIENT */ + bool rcheevos_load(const void *data) { - const struct retro_game_info *info = (const struct retro_game_info*) - 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)); @@ -2241,19 +3046,23 @@ bool rcheevos_load(const void *data) rcheevos_locals.game.id = -1; rcheevos_locals.game.console_id = 0; rcheevos_locals.game.mastery_placard_shown = false; -#ifdef HAVE_THREADS - rcheevos_locals.queued_command = CMD_EVENT_NONE; -#endif -#ifdef HAVE_GFX_WIDGETS + #ifdef HAVE_GFX_WIDGETS rcheevos_locals.tracker_progress = 0.0; -#endif + #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; } @@ -2263,15 +3072,113 @@ bool rcheevos_load(const void *data) 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(); + } + 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 + } + + /* provide hooks for reading files */ + rc_hash_reset_cdreader_hooks(); + + 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 + #endif rcheevos_begin_load_state(RCHEEVOS_LOAD_STATE_IDENTIFYING_GAME); /* reset hardcore mode and leaderboard settings based on configs */ @@ -2358,10 +3265,48 @@ bool rcheevos_load(const void *data) 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) +{ + 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; @@ -2551,3 +3496,5 @@ void rcheevos_change_disc(const char* new_disc_path, bool initial_disc) rcheevos_identify_game_disc_callback, data); } } + +#endif /* HAVE_RC_CLIENT */ diff --git a/cheevos/cheevos.h b/cheevos/cheevos.h index 0d7ddfb0cd..2d6b6ff4dc 100644 --- a/cheevos/cheevos.h +++ b/cheevos/cheevos.h @@ -35,6 +35,7 @@ bool rcheevos_set_serialized_data(void* buffer); bool rcheevos_unload(void); void rcheevos_test(void); +void rcheevos_idle(void); void rcheevos_reset_game(bool widgets_ready); void rcheevos_refresh_memory(void); @@ -44,6 +45,8 @@ void rcheevos_hardcore_enabled_changed(void); void rcheevos_toggle_hardcore_paused(void); bool rcheevos_hardcore_active(void); +void rcheevos_spectating_changed(void); + void rcheevos_validate_config_settings(void); void rcheevos_leaderboard_trackers_visibility_changed(void); @@ -53,7 +56,7 @@ bool rcheevos_get_support_cheevos(void); const char* rcheevos_get_hash(void); int rcheevos_get_richpresence(char *s, size_t len); -uintptr_t rcheevos_get_badge_texture(const char *badge, bool locked); +uintptr_t rcheevos_get_badge_texture(const char* badge, bool locked, bool download_if_missing); uint8_t* rcheevos_patch_address(unsigned address); diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index d02d0f1dca..62e9fd01b2 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -1,5 +1,5 @@ /* RetroArch - A frontend for libretro. - * Copyright (C) 2019-2021 - Brian Weiss + * 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- @@ -46,8 +46,8 @@ * THIS WILL DISCLOSE THE USER'S PASSWORD, TAKE CARE! */ #undef CHEEVOS_LOG_PASSWORD -/* Define this macro to load a JSON file from disk instead of downloading - * from retroachievements.org. */ + /* Define this macro with a string to load a JSON file from disk with + * that name instead of downloading the game data from retroachievements.org. */ #undef CHEEVOS_JSON_OVERRIDE /* Define this macro with a string to save the JSON file to disk with @@ -57,6 +57,8 @@ /* Define this macro to log downloaded badge images. */ #undef CHEEVOS_LOG_BADGES +#ifndef HAVE_RC_CLIENT + /* Number of usecs to wait between posting rich presence to the site. */ /* Keep consistent with SERVER_PING_FREQUENCY from RAIntegration. */ #define CHEEVOS_PING_FREQUENCY 2 * 60 * 1000000 @@ -99,12 +101,16 @@ typedef struct rcheevos_async_io_request char type; } rcheevos_async_io_request; +#endif /* HAVE_RC_CLIENT */ + #ifdef HAVE_THREADS #define RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS 2 #else #define RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS 1 #endif +#ifndef HAVE_RC_CLIENT + typedef struct rcheevos_fetch_badge_state { unsigned badge_fetch_index; @@ -141,6 +147,8 @@ static void rcheevos_async_fetch_badge_callback( struct rcheevos_async_io_request* request, http_transfer_data_t* data, char buffer[], size_t buffer_size); +#endif /* HAVE_RC_CLIENT */ + /**************************** * user agent construction * ****************************/ @@ -218,6 +226,10 @@ void rcheevos_get_user_agent(rcheevos_locals_t *locals, *ptr = '\0'; } +/**************************** + * server interaction * + ****************************/ + #ifdef CHEEVOS_LOG_URLS #ifndef CHEEVOS_LOG_PASSWORD static void rcheevos_filter_url_param(char* url, char* param) @@ -255,7 +267,7 @@ static void rcheevos_filter_url_param(char* url, char* param) #endif #endif -void rcheevos_log_url(const char* api, const char* url) +void rcheevos_log_url(const char* url) { #ifdef CHEEVOS_LOG_URLS #ifdef CHEEVOS_LOG_PASSWORD @@ -268,7 +280,6 @@ void rcheevos_log_url(const char* api, const char* url) CHEEVOS_LOG(RCHEEVOS_TAG "GET %s\n", copy); #endif #else - (void)api; (void)url; #endif } @@ -305,11 +316,428 @@ static void rcheevos_log_post_url(const char* url, const char* post) #endif } - /**************************** * dispatch * ****************************/ +#ifdef HAVE_RC_CLIENT + +typedef struct rc_client_http_task_data_t +{ + rc_client_server_callback_t callback; + void* callback_data; +} rc_client_http_task_data_t; + +static void rcheevos_client_http_task_callback(retro_task_t* task, + void* task_data, void* user_data, const char* error) +{ + rc_client_http_task_data_t* callback_data = (rc_client_http_task_data_t*)user_data; + http_transfer_data_t* http_data = (http_transfer_data_t*)task_data; + rc_api_server_response_t server_response; + memset(&server_response, 0, sizeof(server_response)); + + if (!http_data) + { + callback_data->callback(&server_response, callback_data->callback_data); + } + else + { + server_response.body = http_data->data; + server_response.body_length = http_data->len; + server_response.http_status_code = http_data->status; + + callback_data->callback(&server_response, callback_data->callback_data); + } + + free(callback_data); +} + +#ifdef CHEEVOS_SAVE_JSON +static void rcheevos_client_http_task_save_callback(retro_task_t* task, + void* task_data, void* user_data, const char* error) +{ + http_transfer_data_t* http_data = (http_transfer_data_t*)task_data; + + if (http_data) + { + filestream_write_file(CHEEVOS_SAVE_JSON, http_data->data, http_data->len); + CHEEVOS_LOG(RCHEEVOS_TAG "Captured game info. Wrote %u bytes to %s\n", http_data->len, CHEEVOS_SAVE_JSON); + } + + rcheevos_client_http_task_callback(task, task_data, user_data, error); +} +#endif + +#ifdef CHEEVOS_JSON_OVERRIDE +void rcheevos_client_http_load_response(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data) +{ + size_t size = 0; + char* contents; + FILE* file = fopen(CHEEVOS_JSON_OVERRIDE, "rb"); + + fseek(file, 0, SEEK_END); + size = ftell(file); + fseek(file, 0, SEEK_SET); + + contents = (char*)malloc(size + 1); + fread((void*)contents, 1, size, file); + fclose(file); + + contents[size] = 0; + CHEEVOS_LOG(RCHEEVOS_TAG "Loaded game info. Read %u bytes to %s\n", size, CHEEVOS_JSON_OVERRIDE); + + callback(contents, 200, callback_data); +} +#endif + +void rcheevos_client_server_call(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + rc_client_http_task_data_t* taskdata = malloc(sizeof(rc_client_http_task_data_t)); + taskdata->callback = callback; + taskdata->callback_data = callback_data; + + if (request->post_data) + { + rcheevos_log_post_url(request->url, request->post_data); + +#ifdef CHEEVOS_JSON_OVERRIDE + if (strstr(request->post_data, "r=patch")) + { + rcheevos_client_http_load_response(request, callback, callback_data); + return; + } +#endif + +#ifdef CHEEVOS_SAVE_JSON + if (strstr(request->post_data, "r=patch")) + { + task_push_http_post_transfer_with_user_agent(request->url, + request->post_data, true, "POST", rcheevos_locals->user_agent_core, + rcheevos_client_http_task_save_callback, taskdata); + return; + } +#endif + + task_push_http_post_transfer_with_user_agent(request->url, + request->post_data, true, "POST", rcheevos_locals->user_agent_core, + rcheevos_client_http_task_callback, taskdata); + +#ifdef HAVE_PRESENCE + if (strstr(request->post_data, "r=ping")) + presence_update(PRESENCE_RETROACHIEVEMENTS); +#endif + } + else + { + rcheevos_log_url(request->url); + task_push_http_transfer_with_user_agent(request->url, + true, "GET", rcheevos_locals->user_agent_core, + rcheevos_client_http_task_callback, taskdata); + } +} + +/**************************** + * downloading badges * + ****************************/ + +typedef struct rc_client_download_queue_t +{ + const rc_client_t* client; + const rc_client_game_t* game; + +#ifdef HAVE_THREADS + slock_t* lock; +#endif + + rc_client_achievement_list_t* list; + uint32_t pass; + uint32_t bucket_index; + uint32_t achievement_index; + uint32_t count; + uint32_t outstanding_requests; +} rc_client_download_queue_t; + +static void rcheevos_client_fetch_next_badge(rc_client_download_queue_t* queue); + +typedef struct rc_client_download_task_data_t +{ + rc_client_download_queue_t* queue; + char badge_fullpath[PATH_MAX_LENGTH]; + char badge_name[32]; +} rc_client_download_task_data_t; + +static void rcheevos_client_download_task_callback(retro_task_t* task, + void* task_data, void* user_data, const char* error) +{ + rc_client_download_task_data_t* callback_data = (rc_client_download_task_data_t*)user_data; + http_transfer_data_t* http_data = (http_transfer_data_t*)task_data; + + if (!http_data) + { + CHEEVOS_LOG(RCHEEVOS_TAG "No data received for badge %s\n", callback_data->badge_name); + } + else if (http_data->status != 200) + { + CHEEVOS_LOG(RCHEEVOS_TAG "HTTP status code %d for badge %s\n", http_data->status, callback_data->badge_name); + } + else if (!filestream_write_file(callback_data->badge_fullpath, http_data->data, http_data->len)) + { + CHEEVOS_LOG(RCHEEVOS_TAG "Error writing %s\n", callback_data->badge_fullpath); + } + + if (callback_data->queue) + { +#ifdef HAVE_THREADS + slock_lock(callback_data->queue->lock); +#endif + callback_data->queue->count++; +#ifdef HAVE_THREADS + slock_unlock(callback_data->queue->lock); +#endif + + rcheevos_client_fetch_next_badge(callback_data->queue); + } + + free(callback_data); +} + +static bool rcheevos_client_download_badge(rc_client_download_queue_t* queue, + const char* url, const char* badge_name) +{ + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + rc_client_download_task_data_t* taskdata; + char badge_fullpath[512] = ""; + char* badge_fullname; + size_t badge_fullname_size; + + /* make sure the directory exists */ + fill_pathname_application_special(badge_fullpath, sizeof(badge_fullpath), + APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES); + + if (!path_is_directory(badge_fullpath)) + { + CHEEVOS_LOG(RCHEEVOS_TAG "Creating %s\n", badge_fullpath); + path_mkdir(badge_fullpath); + } + + fill_pathname_slash(badge_fullpath, sizeof(badge_fullpath)); + badge_fullname = badge_fullpath + strlen(badge_fullpath); + badge_fullname_size = sizeof(badge_fullpath) - (badge_fullname - badge_fullpath); + snprintf(badge_fullname, badge_fullname_size, "%s" FILE_PATH_PNG_EXTENSION, badge_name); + + if (path_is_valid(badge_fullpath)) + return false; + +#ifdef CHEEVOS_LOG_BADGES + CHEEVOS_LOG(RCHEEVOS_TAG "Downloading %s from %s\n", badge_name, url); +#else + rcheevos_log_url(url); +#endif + + taskdata = (rc_client_download_task_data_t*)malloc(sizeof(*taskdata)); + taskdata->queue = queue; + strlcpy(taskdata->badge_fullpath, badge_fullpath, sizeof(taskdata->badge_fullpath)); + strlcpy(taskdata->badge_name, badge_name, sizeof(taskdata->badge_name)); + + task_push_http_transfer_with_user_agent(url, + true, "GET", rcheevos_locals->user_agent_core, + rcheevos_client_download_task_callback, taskdata); + + return true; +} + +void rcheevos_client_download_badge_from_url(const char* url, const char* badge_name) +{ + rcheevos_client_download_badge(NULL, url, badge_name); +} + +static void rcheevos_client_fetch_next_badge(rc_client_download_queue_t* queue) +{ + rc_client_achievement_bucket_t* bucket; + rc_client_achievement_t* achievement; + const char* next_badge; + char badge_name[32]; + char url[256]; + bool done = false; + + do + { + next_badge = NULL; + +#ifdef HAVE_THREADS + slock_lock(queue->lock); +#endif + /* if the game is no longer loaded, stop processing the queue */ + if (queue->game != rc_client_get_game_info(queue->client)) + queue->pass = 2; + + while (queue->pass < 2) + { + if (queue->bucket_index >= queue->list->num_buckets) + { + queue->bucket_index = 0; + queue->pass++; + continue; + } + + bucket = &queue->list->buckets[queue->bucket_index]; + + if (queue->achievement_index >= bucket->num_achievements) + { + queue->achievement_index = 0; + queue->bucket_index++; + continue; + } + + achievement = bucket->achievements[queue->achievement_index++]; + if (!achievement->badge_name[0]) + continue; + + if (queue->pass == 0) + { + /* first pass - get all unlocked badges */ + if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) != RC_OK) + continue; + + next_badge = achievement->badge_name; + } + else if (achievement->unlock_time) + { + /* second pass - don't need locked badge for achievement player has already unlocked */ + continue; + } + else + { + /* second pass - get locked badge */ + if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) != RC_OK) + continue; + + snprintf(badge_name, sizeof(badge_name), "%s_lock", achievement->badge_name); + next_badge = badge_name; + } + + break; + } + + if (!next_badge) + { + if (--queue->outstanding_requests == 0) + done = true; + } + +#ifdef HAVE_THREADS + slock_unlock(queue->lock); +#endif + + if (next_badge) + { + /* if the badge already exists (download_badge returns false), continue + * looping to the next item. otherwise, a download was queued, so break + * out of the loop. */ + if (rcheevos_client_download_badge(queue, url, next_badge)) + break; + } + } while (next_badge); + + if (done) + { + /* queue complete */ + if (queue->count) + { + CHEEVOS_LOG(RCHEEVOS_TAG "Downloaded %u badges\n", queue->count); + } + rc_client_destroy_achievement_list(queue->list); + +#ifdef HAVE_THREADS + slock_free(queue->lock); +#endif + + free(queue); + } +} + +void rcheevos_client_download_placeholder_badge(void) +{ + char url[256] = ""; + + if (rc_client_achievement_get_image_url(NULL, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) == RC_OK) + rcheevos_client_download_badge(NULL, url, "00000"); +} + +void rcheevos_client_download_game_badge(const rc_client_game_t* game) +{ + char url[256] = ""; + char badge_name[16]; + + if (game && rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK) + { + snprintf(badge_name, sizeof(badge_name), "i%s", game->badge_name); + rcheevos_client_download_badge(NULL, url, badge_name); + } +} + +void rcheevos_client_download_achievement_badges(rc_client_t* client) +{ + rc_client_download_queue_t* queue; + uint32_t i; + +#if !defined(HAVE_GFX_WIDGETS) /* we always want badges if widgets are enabled */ + settings_t* settings = config_get_ptr(); + /* User has explicitly disabled badges */ + if (!settings->bools.cheevos_badges_enable) + return; + + /* badges are only needed for xmb and ozone menus */ + if (!string_is_equal(settings->arrays.menu_driver, "xmb") && + !string_is_equal(settings->arrays.menu_driver, "ozone")) + return; +#endif /* !defined(HAVE_GFX_WIDGETS) */ + + queue = (rc_client_download_queue_t*)calloc(1, sizeof(*queue)); + queue->client = client; + queue->game = rc_client_get_game_info(client); + queue->list = rc_client_create_achievement_list(client, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + queue->outstanding_requests = RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS; + +#ifdef HAVE_THREADS + queue->lock = slock_new(); +#endif + + for (i = 0; i < RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS; i++) + rcheevos_client_fetch_next_badge(queue); +} + +#undef RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS + +void rcheevos_client_download_achievement_badge(const char* badge_name, bool locked) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + char locked_badge_name[32]; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; + image_request.image_name = badge_name; + + if (locked) + { + snprintf(locked_badge_name, sizeof(locked_badge_name), "%s_lock", badge_name); + badge_name = locked_badge_name; + } + + if (rc_api_init_fetch_image_request(&request, &image_request) == RC_OK) + rcheevos_client_download_badge(NULL, request.url, badge_name); + + rc_api_destroy_request(&request); +} + +#else /* !HAVE_RC_CLIENT */ + static void rcheevos_async_begin_http_request(rcheevos_async_io_request* request) { if (request->request.post_data) @@ -1858,3 +2286,5 @@ void rcheevos_client_submit_lboard_entry(unsigned leaderboard_id, "Error submitting leaderboard"); } } + +#endif /* HAVE_RC_CLIENT */ diff --git a/cheevos/cheevos_client.h b/cheevos/cheevos_client.h index 062369e99f..96aba4a002 100644 --- a/cheevos/cheevos_client.h +++ b/cheevos/cheevos_client.h @@ -1,5 +1,5 @@ /* RetroArch - A frontend for libretro. - * Copyright (C) 2019-2021 - Brian Weiss + * 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- @@ -20,6 +20,19 @@ RETRO_BEGIN_DECLS +#ifdef HAVE_RC_CLIENT + +void rcheevos_client_download_placeholder_badge(void); +void rcheevos_client_download_game_badge(const rc_client_game_t* game); +void rcheevos_client_download_achievement_badges(rc_client_t* client); +void rcheevos_client_download_achievement_badge(const char* badge_name, bool locked); +void rcheevos_client_download_badge_from_url(const char* url, const char* badge_name); + +void rcheevos_client_server_call(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +#else + typedef void (*rcheevos_client_callback)(void* userdata); void rcheevos_client_initialize(void); @@ -39,9 +52,11 @@ void rcheevos_client_submit_lboard_entry(unsigned leaderboard_id, int value); void rcheevos_client_fetch_badges(rcheevos_client_callback callback, void* userdata); -void rcheevos_log_url(const char* api, const char* url); -void rcheevos_get_user_agent(rcheevos_locals_t *locals, char *buffer, size_t len); +void rcheevos_log_url(const char* url); +#endif /* HAVE_RC_CLIENT */ + +void rcheevos_get_user_agent(rcheevos_locals_t* locals, char* buffer, size_t len); RETRO_END_DECLS diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 97cefb7f75..72077cbab2 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -1,6 +1,6 @@ /* RetroArch - A frontend for libretro. * Copyright (C) 2015-2018 - Andre Leiradella - * Copyright (C) 2019-2021 - Brian Weiss + * 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- @@ -17,6 +17,9 @@ #ifndef __RARCH_CHEEVOS_LOCALS_H #define __RARCH_CHEEVOS_LOCALS_H +#define HAVE_RC_CLIENT 1 + +#include "../deps/rcheevos/include/rc_client.h" #include "../deps/rcheevos/include/rc_runtime.h" #include "../deps/rcheevos/src/rc_libretro.h" @@ -57,6 +60,7 @@ RETRO_BEGIN_DECLS * State * ************************************************************************/ +#ifndef HAVE_RC_CLIENT enum { RCHEEVOS_ACTIVE_SOFTCORE = 1 << 0, @@ -117,6 +121,8 @@ enum rcheevos_load_state RCHEEVOS_LOAD_STATE_ABORTED }; +#endif /* HAVE_RC_CLIENT */ + enum rcheevos_summary_notif { RCHEEVOS_SUMMARY_ALLGAMES = 0, @@ -125,6 +131,8 @@ enum rcheevos_summary_notif RCHEEVOS_SUMMARY_LAST }; +#ifndef HAVE_RC_CLIENT + typedef struct rcheevos_load_info_t { enum rcheevos_load_state state; @@ -164,19 +172,42 @@ typedef struct rcheevos_menuitem_t #endif +#else /* HAVE_RC_CLIENT */ + +#ifdef HAVE_MENU + +typedef struct rcheevos_menuitem_t +{ + rc_client_achievement_t* achievement; + uintptr_t menu_badge_texture; + uint32_t subset_id; + uint8_t menu_badge_grayscale; + enum msg_hash_enums state_label_idx; +} rcheevos_menuitem_t; + +#endif + +#endif /* HAVE_RC_CLIENT */ + typedef struct rcheevos_locals_t { +#ifdef HAVE_RC_CLIENT + rc_client_t* client; /* rcheevos client state */ +#else rc_runtime_t runtime; /* rcheevos runtime state */ rcheevos_game_info_t game; /* information about the current game */ +#endif rc_libretro_memory_regions_t memory;/* achievement addresses to core memory mappings */ #ifdef HAVE_THREADS enum event_command queued_command; /* action queued by background thread to be run on main thread */ #endif +#ifndef HAVE_RC_CLIENT char displayname[32]; /* name to display in messages */ char username[32]; /* case-corrected username */ char token[32]; /* user's session token */ +#endif char user_agent_prefix[128]; /* RetroArch/OS version information */ char user_agent_core[256]; /* RetroArch/OS/Core version information */ @@ -186,6 +217,10 @@ typedef struct rcheevos_locals_t unsigned menuitem_count; /* current number of items in the menuitems array */ #endif +#ifdef HAVE_RC_CLIENT + bool hardcore_allowed; /* prevents enabling hardcore if illegal settings detected */ +#else + #ifdef HAVE_GFX_WIDGETS unsigned active_lboard_trackers; /* bit mask of active leaderboard tracker ids */ rcheevos_racheevo_t* tracker_achievement; @@ -199,15 +234,20 @@ typedef struct rcheevos_locals_t #ifdef HAVE_GFX_WIDGETS bool assign_new_trackers; /* a new leaderboard was started and needs a tracker assigned */ #endif + +#endif + bool core_supports; /* false if core explicitly disables achievements */ } rcheevos_locals_t; rcheevos_locals_t* get_rcheevos_locals(void); + +#ifndef HAVE_RC_CLIENT void rcheevos_begin_load_state(enum rcheevos_load_state state); int rcheevos_end_load_state(void); bool rcheevos_load_aborted(void); - void rcheevos_show_mastery_placard(void); +#endif RETRO_END_DECLS diff --git a/cheevos/cheevos_menu.c b/cheevos/cheevos_menu.c index d3047a9d0f..e92d1a7013 100644 --- a/cheevos/cheevos_menu.c +++ b/cheevos/cheevos_menu.c @@ -1,5 +1,5 @@ /* RetroArch - A frontend for libretro. - * Copyright (C) 2019-2021 - Brian Weiss + * 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- @@ -17,6 +17,7 @@ #include #include "cheevos_locals.h" +#include "cheevos_client.h" #include "../gfx/gfx_display.h" #include "../file_path_special.h" @@ -26,10 +27,487 @@ #include "cheevos.h" #include "../deps/rcheevos/include/rc_runtime_types.h" +#include "../deps/rcheevos/include/rc_api_runtime.h" #include "../menu/menu_driver.h" #include "../menu/menu_entries.h" +#include +#include + + /* if menu_badge_grayscale is set to a value other than 1 or 0, it's a counter for the number of + * frames since the last time we checked for the file. When the counter reaches this value, we'll + * check for the file again. */ +#define MENU_BADGE_RETRY_RELOAD_FRAMES 64 + +#ifdef HAVE_RC_CLIENT + +bool rcheevos_menu_get_state(unsigned menu_offset, char* buffer, size_t buffer_size) +{ + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + if (menu_offset < rcheevos_locals->menuitem_count) + { + const rcheevos_menuitem_t* menuitem = &rcheevos_locals->menuitems[menu_offset]; + const rc_client_achievement_t* cheevo = menuitem->achievement; + if (cheevo) + { + if (cheevo->measured_progress[0]) + { + snprintf(buffer, buffer_size, "%s - %s", + msg_hash_to_str(menuitem->state_label_idx), cheevo->measured_progress); + } + else + strlcpy(buffer, msg_hash_to_str(menuitem->state_label_idx), buffer_size); + + return true; + } + } + + if (buffer) + buffer[0] = '\0'; + + return false; +} + +bool rcheevos_menu_get_sublabel(unsigned menu_offset, char* buffer, size_t buffer_size) +{ + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + if (menu_offset < rcheevos_locals->menuitem_count && buffer) + { + const rcheevos_menuitem_t* menuitem = &rcheevos_locals->menuitems[menu_offset]; + if (menuitem->achievement) + { + strlcpy(buffer, menuitem->achievement->description, buffer_size); + return true; + } + } + + if (buffer) + buffer[0] = '\0'; + + return false; +} + +void rcheevos_menu_reset_badges(void) +{ + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + rcheevos_menuitem_t* menuitem = rcheevos_locals->menuitems; + rcheevos_menuitem_t* stop = menuitem + rcheevos_locals->menuitem_count; + + while (menuitem < stop) + { + if (menuitem->menu_badge_texture) + { + video_driver_texture_unload(&menuitem->menu_badge_texture); + menuitem->menu_badge_texture = 0; + menuitem->menu_badge_grayscale = MENU_BADGE_RETRY_RELOAD_FRAMES; + } + ++menuitem; + } +} + +static rcheevos_menuitem_t* rcheevos_menu_allocate( + rcheevos_locals_t* rcheevos_locals) +{ + rcheevos_menuitem_t* menuitem; + + if (rcheevos_locals->menuitem_count == rcheevos_locals->menuitem_capacity) + { + if (rcheevos_locals->menuitems) + { + rcheevos_menuitem_t* new_menuitems; + rcheevos_locals->menuitem_capacity += 32; + new_menuitems = (rcheevos_menuitem_t*)realloc(rcheevos_locals->menuitems, + rcheevos_locals->menuitem_capacity * sizeof(rcheevos_menuitem_t)); + + if (new_menuitems) + rcheevos_locals->menuitems = new_menuitems; + else + { + /* realloc failed */ + CHEEVOS_ERR(RCHEEVOS_TAG " could not allocate space for %u menu items\n", + rcheevos_locals->menuitem_capacity); + rcheevos_locals->menuitem_capacity -= 32; + return NULL; + } + } + else + { + rcheevos_locals->menuitem_capacity = 64; + rcheevos_locals->menuitems = (rcheevos_menuitem_t*) + malloc(rcheevos_locals->menuitem_capacity * sizeof(rcheevos_menuitem_t)); + + if (!rcheevos_locals->menuitems) + { + /* malloc failed */ + CHEEVOS_ERR(RCHEEVOS_TAG " could not allocate space for %u menu items\n", + rcheevos_locals->menuitem_capacity); + rcheevos_locals->menuitem_capacity = 0; + return NULL; + } + } + } + + menuitem = &rcheevos_locals->menuitems[rcheevos_locals->menuitem_count++]; + memset(menuitem, 0, sizeof(*menuitem)); + return menuitem; +} + +static void rcheevos_menu_append_header(rcheevos_locals_t* rcheevos_locals, + enum msg_hash_enums label, uint32_t subset_id) +{ + rcheevos_menuitem_t* menuitem = rcheevos_menu_allocate(rcheevos_locals); + if (menuitem) + { + menuitem->state_label_idx = label; + menuitem->subset_id = subset_id; + } +} + +static void rcheevos_menu_update_badge(rcheevos_menuitem_t* menuitem, bool download_if_missing) +{ + const char* badge_name = "00000"; + bool badge_grayscale = false; + + if (menuitem->achievement) + badge_name = menuitem->achievement->badge_name; + + switch (menuitem->state_label_idx) + { + case MENU_ENUM_LABEL_VALUE_CHEEVOS_LOCKED_ENTRY: + case MENU_ENUM_LABEL_VALUE_CHEEVOS_UNOFFICIAL_ENTRY: + case MENU_ENUM_LABEL_VALUE_CHEEVOS_UNSUPPORTED_ENTRY: + case MENU_ENUM_LABEL_VALUE_CHEEVOS_ALMOST_THERE_ENTRY: + case MENU_ENUM_LABEL_VALUE_CHEEVOS_ACTIVE_CHALLENGES_ENTRY: + badge_grayscale = true; + break; + + default: + badge_grayscale = false; + break; + } + + if (!menuitem->menu_badge_texture || menuitem->menu_badge_grayscale != badge_grayscale) + { + uintptr_t new_badge_texture = + rcheevos_get_badge_texture(badge_name, badge_grayscale, download_if_missing); + + if (new_badge_texture) + { + if (menuitem->menu_badge_texture) + video_driver_texture_unload(&menuitem->menu_badge_texture); + + menuitem->menu_badge_texture = new_badge_texture; + menuitem->menu_badge_grayscale = badge_grayscale; + } + /* menu_badge_grayscale is overloaded such + * that any value greater than 1 indicates + * the server default image is being used */ + else if (menuitem->menu_badge_grayscale < 2) + { + if (menuitem->menu_badge_texture) + video_driver_texture_unload(&menuitem->menu_badge_texture); + + /* requested badge is not available, check for server default */ + menuitem->menu_badge_texture = + rcheevos_get_badge_texture("00000", false, false); + + if (menuitem->menu_badge_texture) + menuitem->menu_badge_grayscale = 2; + } + } +} + +uintptr_t rcheevos_menu_get_badge_texture(unsigned menu_offset) +{ + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + if (menu_offset < rcheevos_locals->menuitem_count) + { + rcheevos_menuitem_t* menuitem = &rcheevos_locals->menuitems[menu_offset]; + + /* if we're using the placeholder badge, check to see if the real badge + * has become available (do this roughly once a second) */ + if (menuitem->menu_badge_grayscale >= 2) + { + if (++menuitem->menu_badge_grayscale >= MENU_BADGE_RETRY_RELOAD_FRAMES) + { + menuitem->menu_badge_grayscale = 2; + rcheevos_menu_update_badge(menuitem, false); + } + } + + return menuitem->menu_badge_texture; + } + + return 0; +} + +void rcheevos_menu_populate_hardcore_pause_submenu(void* data) +{ + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + menu_displaylist_info_t* info = (menu_displaylist_info_t*)data; + const settings_t* settings = config_get_ptr(); + const bool cheevos_hardcore_mode_enable = settings->bools.cheevos_hardcore_mode_enable; + + if (cheevos_hardcore_mode_enable && rc_client_get_game_info(rcheevos_locals->client)) + { + if (rc_client_get_hardcore_enabled(rcheevos_locals->client)) + { + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE_CANCEL), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_CANCEL), + MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_CANCEL, + MENU_SETTING_ACTION_CLOSE, 0, 0, NULL); + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE), + MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE, + MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS, 0, 0, NULL); + } + else + { + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME_CANCEL), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_CANCEL), + MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_CANCEL, + MENU_SETTING_ACTION_CLOSE, 0, 0, NULL); + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_RESUME), + MENU_ENUM_LABEL_ACHIEVEMENT_RESUME, + MENU_SETTING_ACTION_RESUME_ACHIEVEMENTS, 0, 0, NULL); + } + } +} + +void rcheevos_menu_populate(void* data) +{ + menu_displaylist_info_t* info = (menu_displaylist_info_t*)data; + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals->client); + const settings_t* settings = config_get_ptr(); + + rc_client_achievement_list_t* list = rc_client_create_achievement_list(rcheevos_locals->client, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + uint32_t i, j; + + rcheevos_menu_reset_badges(); + rcheevos_locals->menuitem_count = 0; + + if (game && game->id != 0) + { + /* first menu item is the Pause/Resume Hardcore option (unless hardcore is completely disabled) */ + if (settings->bools.cheevos_enable && settings->bools.cheevos_hardcore_mode_enable) + { + if (rc_client_get_hardcore_enabled(rcheevos_locals->client)) + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_MENU), + MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_MENU, + MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS, 0, 0, NULL); + else + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_MENU), + MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_MENU, + MENU_SETTING_ACTION_RESUME_ACHIEVEMENTS, 0, 0, NULL); + } + } + + for (i = 0; i < list->num_buckets; i++) + { + if (list->num_buckets > 1) + { + enum msg_hash_enums label; + switch (list->buckets[i].bucket_type) + { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_LOCKED_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNSUPPORTED_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNOFFICIAL_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_RECENTLY_UNLOCKED_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_ACTIVE_CHALLENGES_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + label = MENU_ENUM_LABEL_VALUE_CHEEVOS_ALMOST_THERE_ENTRY; + break; + default: + continue; + } + rcheevos_menu_append_header(rcheevos_locals, label, list->buckets[i].subset_id); + } + + for (j = 0; j < list->buckets[i].num_achievements; j++) + { + rcheevos_menuitem_t* menuitem = rcheevos_menu_allocate(rcheevos_locals); + if (!menuitem) + break; + + menuitem->achievement = list->buckets[i].achievements[j]; + + switch (list->buckets[i].bucket_type) + { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: + if (menuitem->achievement->unlocked & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) + menuitem->state_label_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY_HARDCORE; + else + menuitem->state_label_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: + menuitem->state_label_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNSUPPORTED_ENTRY; + break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: + menuitem->state_label_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNOFFICIAL_ENTRY; + break; + default: + menuitem->state_label_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_LOCKED_ENTRY; + break; + } + + rcheevos_menu_update_badge(menuitem, true); + } + } + + rc_client_destroy_achievement_list(list); + + if (rcheevos_locals->menuitem_count > 0) + { + char buffer[128]; + unsigned idx = 0; + /* convert to menu entries */ + rcheevos_menuitem_t* menuitem = rcheevos_locals->menuitems; + rcheevos_menuitem_t* stop = menuitem + + rcheevos_locals->menuitem_count; + + do + { + if (menuitem->achievement) + menu_entries_append(info->list, menuitem->achievement->title, + menuitem->achievement->description, + MENU_ENUM_LABEL_CHEEVOS_LOCKED_ENTRY, + MENU_SETTINGS_CHEEVOS_START + idx, 0, 0, NULL); + else + { + if (menuitem->subset_id) + { + const rc_client_subset_t* subset = + rc_client_get_subset_info(rcheevos_locals->client, menuitem->subset_id); + + snprintf(buffer, sizeof(buffer), "----- %s - %s -----", + subset ? subset->title : "Unknown Subset", + msg_hash_to_str(menuitem->state_label_idx)); + } + else + { + snprintf(buffer, sizeof(buffer), "----- %s -----", + msg_hash_to_str(menuitem->state_label_idx)); + } + + menu_entries_append(info->list, buffer, "", + MENU_ENUM_LABEL_CHEEVOS_LOCKED_ENTRY, + MENU_SETTINGS_CHEEVOS_START + idx, 0, 0, NULL); + } + + ++idx; + ++menuitem; + } while (menuitem != stop); + } + else + { + /* no achievements found */ + if (!rcheevos_locals->core_supports) + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE), + msg_hash_to_str(MENU_ENUM_LABEL_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE), + MENU_ENUM_LABEL_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE, + FILE_TYPE_NONE, 0, 0, NULL); + else if (!rc_client_get_game_info(rcheevos_locals->client)) + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN_GAME), + msg_hash_to_str(MENU_ENUM_LABEL_UNKNOWN_GAME), + MENU_ENUM_LABEL_UNKNOWN_GAME, + FILE_TYPE_NONE, 0, 0, NULL); + else if (!rc_client_get_user_info(rcheevos_locals->client)) + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_LOGGED_IN), + msg_hash_to_str(MENU_ENUM_LABEL_NOT_LOGGED_IN), + MENU_ENUM_LABEL_NOT_LOGGED_IN, + FILE_TYPE_NONE, 0, 0, NULL); + else + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_ACHIEVEMENTS_TO_DISPLAY), + msg_hash_to_str(MENU_ENUM_LABEL_NO_ACHIEVEMENTS_TO_DISPLAY), + MENU_ENUM_LABEL_NO_ACHIEVEMENTS_TO_DISPLAY, + FILE_TYPE_NONE, 0, 0, NULL); + } +} + + +uintptr_t rcheevos_get_badge_texture(const char* badge, bool locked, bool download_if_missing) +{ + char badge_file[24]; + char fullpath[PATH_MAX_LENGTH]; + uintptr_t tex = 0; + + if (!badge || !badge[0]) + return 0; + + /* OpenGL driver crashes if gfx_display_reset_textures_list is called on a background thread */ + retro_assert(task_is_on_main_thread()); + + snprintf(badge_file, sizeof(badge_file), "%s%s%s", badge, + locked ? "_lock" : "", FILE_PATH_PNG_EXTENSION); + + fill_pathname_application_special(fullpath, sizeof(fullpath), + APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES); + + if (!gfx_display_reset_textures_list(badge_file, fullpath, + &tex, TEXTURE_FILTER_MIPMAP_LINEAR, NULL, NULL)) + { + if (download_if_missing) + { + if (badge[0] == 'i') + { + /* rcheevos_client_download_game_badge expects a rc_client_game_t, not the badge name. + * call rc_api_init_fetch_image_request directly */ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = RC_IMAGE_TYPE_GAME; + image_request.image_name = &badge[1]; + result = rc_api_init_fetch_image_request(&request, &image_request); + + if (result == RC_OK) + rcheevos_client_download_badge_from_url(request.url, badge); + } + else + { + rcheevos_client_download_achievement_badge(badge, locked); + } + } + return 0; + } + + return tex; +} + +#else /* !HAVE_RC_CLIENT */ + enum rcheevos_menuitem_bucket { RCHEEVOS_MENUITEM_BUCKET_UNKNOWN = 0, @@ -42,11 +520,6 @@ enum rcheevos_menuitem_bucket RCHEEVOS_MENUITEM_BUCKET_ALMOST_THERE }; -/* if menu_badge_grayscale is set to a value other than 1 or 0, it's a counter for the number of - * frames since the last time we checked for the file. When the counter reaches this value, we'll - * check for the file again. */ -#define MENU_BADGE_RETRY_RELOAD_FRAMES 64 - static void rcheevos_menu_update_bucket(rcheevos_racheevo_t* cheevo) { cheevo->menu_progress = 0; @@ -78,7 +551,7 @@ static void rcheevos_menu_update_bucket(rcheevos_racheevo_t* cheevo) trigger = rc_runtime_get_achievement(&rcheevos_locals->runtime, cheevo->id); if (trigger) { - if (trigger->measured_value && trigger->measured_target) + if (trigger->measured_value && trigger->measured_value != 0xFFFFFFFF && trigger->measured_target) { const unsigned long clamped_value = (unsigned long) MIN(trigger->measured_value, trigger->measured_target); @@ -108,7 +581,7 @@ static void rcheevos_menu_update_buckets(void) } } -bool rcheevos_menu_get_state(unsigned menu_offset, char *buffer, size_t len) +bool rcheevos_menu_get_state(unsigned menu_offset, char *buffer, size_t buffer_size) { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); if (menu_offset < rcheevos_locals->menuitem_count) @@ -119,14 +592,14 @@ bool rcheevos_menu_get_state(unsigned menu_offset, char *buffer, size_t len) { if (cheevo->menu_progress) { - const int written = snprintf(buffer, len, "%s - ", + const int written = snprintf(buffer, buffer_size, "%s - ", msg_hash_to_str(menuitem->state_label_idx)); - if (len - written > 0) + if (buffer_size - written > 0) rc_runtime_format_achievement_measured(&rcheevos_locals->runtime, - cheevo->id, buffer + written, len - written); + cheevo->id, buffer + written, buffer_size - written); } else - strlcpy(buffer, msg_hash_to_str(menuitem->state_label_idx), len); + strlcpy(buffer, msg_hash_to_str(menuitem->state_label_idx), buffer_size); return true; } @@ -138,7 +611,7 @@ bool rcheevos_menu_get_state(unsigned menu_offset, char *buffer, size_t len) return false; } -bool rcheevos_menu_get_sublabel(unsigned menu_offset, char *buffer, size_t len) +bool rcheevos_menu_get_sublabel(unsigned menu_offset, char *buffer, size_t buffer_size) { const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); if (menu_offset < rcheevos_locals->menuitem_count) @@ -146,7 +619,7 @@ bool rcheevos_menu_get_sublabel(unsigned menu_offset, char *buffer, size_t len) const rcheevos_racheevo_t* cheevo = rcheevos_locals->menuitems[menu_offset].cheevo; if (cheevo && buffer) { - strlcpy(buffer, cheevo->description, len); + strlcpy(buffer, cheevo->description, buffer_size); return true; } } @@ -251,7 +724,7 @@ static void rcheevos_menu_update_badge(rcheevos_racheevo_t* cheevo) if (!cheevo->menu_badge_texture || cheevo->menu_badge_grayscale != badge_grayscale) { uintptr_t new_badge_texture = - rcheevos_get_badge_texture(cheevo->badge, badge_grayscale); + rcheevos_get_badge_texture(cheevo->badge, badge_grayscale, false); if (new_badge_texture) { @@ -271,7 +744,7 @@ static void rcheevos_menu_update_badge(rcheevos_racheevo_t* cheevo) /* requested badge is not available, check for server default */ cheevo->menu_badge_texture = - rcheevos_get_badge_texture("00000", false); + rcheevos_get_badge_texture("00000", false, false); if (cheevo->menu_badge_texture) cheevo->menu_badge_grayscale = 2; @@ -654,9 +1127,7 @@ void rcheevos_menu_populate(void* data) } } -#endif /* HAVE_MENU */ - -uintptr_t rcheevos_get_badge_texture(const char *badge, bool locked) +uintptr_t rcheevos_get_badge_texture(const char *badge, bool locked, bool download_if_missing) { if (badge) { @@ -683,3 +1154,7 @@ uintptr_t rcheevos_get_badge_texture(const char *badge, bool locked) } return 0; } + +#endif /* HAVE_RC_CLIENT */ + +#endif /* HAVE_MENU */ diff --git a/deps/rcheevos/src/rapi/rc_api_common.c b/deps/rcheevos/src/rapi/rc_api_common.c index 481aee07da..96c32b32e3 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.c +++ b/deps/rcheevos/src/rapi/rc_api_common.c @@ -18,7 +18,7 @@ static char* g_imagehost = NULL; /* --- rc_json --- */ -static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen); static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); static int rc_json_match_char(rc_json_iterator_t* iterator, char c) @@ -182,7 +182,7 @@ static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* return RC_OK; } -static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen) { size_t i; uint32_t num_fields = 0; rc_json_field_t field; @@ -437,14 +437,14 @@ int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, r rc_json_iterator_t iterator; rc_json_field_t array; rc_json_field_t value; - unsigned* entry; + uint32_t* entry; memset(&array, 0, sizeof(array)); if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) return RC_MISSING_VALUE; if (*num_entries) { - *entries = (unsigned*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(unsigned)); + *entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t)); if (!*entries) return RC_OUT_OF_MEMORY; diff --git a/deps/rcheevos/src/rcheevos/alloc.c b/deps/rcheevos/src/rcheevos/alloc.c index a4ebdbf66b..7b43a2c759 100644 --- a/deps/rcheevos/src/rcheevos/alloc.c +++ b/deps/rcheevos/src/rcheevos/alloc.c @@ -78,7 +78,7 @@ void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment void** scratch_object_pointer = (void**)((char*)&scratch->objs + scratch_object_pointer_offset); ptr = *scratch_object_pointer; if (!ptr) { - int used; + int32_t used; ptr = *scratch_object_pointer = rc_alloc_scratch(NULL, &used, size, alignment, scratch, -1); } } diff --git a/deps/rcheevos/src/rcheevos/runtime.c b/deps/rcheevos/src/rcheevos/runtime.c index d663b7a959..25e9885dcf 100644 --- a/deps/rcheevos/src/rcheevos/runtime.c +++ b/deps/rcheevos/src/rcheevos/runtime.c @@ -128,7 +128,7 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* rc_runtime_trigger_t* runtime_trigger; rc_parse_state_t parse; uint8_t md5[16]; - int size; + int32_t size; uint32_t i; if (memaddr == NULL) diff --git a/deps/rcheevos/src/rhash/hash.c b/deps/rcheevos/src/rhash/hash.c index a953d525cb..06fc03f225 100644 --- a/deps/rcheevos/src/rhash/hash.c +++ b/deps/rcheevos/src/rhash/hash.c @@ -1612,7 +1612,7 @@ static int rc_hash_dreamcast(char hash[33], const char* path) } static int rc_hash_find_playstation_executable(void* track_handle, const char* boot_key, const char* cdrom_prefix, - char exe_name[], uint32_t exe_name_size, unsigned* exe_size) + char exe_name[], uint32_t exe_name_size, uint32_t* exe_size) { uint8_t buffer[2048]; uint32_t size; @@ -1626,7 +1626,7 @@ static int rc_hash_find_playstation_executable(void* track_handle, const char* b if (!sector) return 0; - size = (unsigned)rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer) - 1); + size = (uint32_t)rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer) - 1); buffer[size] = '\0'; sector = 0; @@ -1653,7 +1653,7 @@ static int rc_hash_find_playstation_executable(void* track_handle, const char* b while (!isspace((unsigned char)*ptr) && *ptr != ';') ++ptr; - size = (unsigned)(ptr - start); + size = (uint32_t)(ptr - start); if (size >= exe_name_size) size = exe_name_size - 1; diff --git a/gfx/gfx_widgets.h b/gfx/gfx_widgets.h index 65bb9e9909..f61e456b7a 100644 --- a/gfx/gfx_widgets.h +++ b/gfx/gfx_widgets.h @@ -380,7 +380,9 @@ void gfx_widgets_ai_service_overlay_unload(void); void gfx_widgets_update_cheevos_appearance(void); void gfx_widgets_push_achievement(const char *title, const char* subtitle, const char *badge); void gfx_widgets_set_leaderboard_display(unsigned id, const char* value); +void gfx_widgets_clear_leaderboard_displays(void); void gfx_widgets_set_challenge_display(unsigned id, const char* badge); +void gfx_widgets_clear_challenge_displays(void); void gfx_widget_set_achievement_progress(const char* badge, const char* progress); #endif diff --git a/gfx/widgets/gfx_widget_achievement_popup.c b/gfx/widgets/gfx_widget_achievement_popup.c index fb1836b8b9..171ea7e9bc 100644 --- a/gfx/widgets/gfx_widget_achievement_popup.c +++ b/gfx/widgets/gfx_widget_achievement_popup.c @@ -28,7 +28,9 @@ typedef struct cheevo_popup { char* title; char* subtitle; + char* badge_name; uintptr_t badge; + retro_time_t badge_retry; } cheevo_popup; enum @@ -211,6 +213,21 @@ static void gfx_widget_achievement_popup_frame(void* data, void* userdata) gfx_display_set_alpha(p_dispwidget->backdrop_orig, DEFAULT_BACKDROP); gfx_display_set_alpha(pure_white, 1.0f); + /* badge wasn't ready, periodically see if it's become available */ + if (!state->queue[state->queue_read_index].badge && + state->queue[state->queue_read_index].badge_name) + { + const retro_time_t next_try = state->queue[state->queue_read_index].badge_retry; + const retro_time_t now = cpu_features_get_time_usec(); + if (next_try == 0 || now > next_try) + { + /* try again in 250ms */ + state->queue[state->queue_read_index].badge_retry = now + 250000; + state->queue[state->queue_read_index].badge = + rcheevos_get_badge_texture(state->queue[state->queue_read_index].badge_name, false, false); + } + } + /* Default Badge */ if (!state->queue[state->queue_read_index].badge) { @@ -365,6 +382,12 @@ static void gfx_widget_achievement_popup_free_current( state->queue[state->queue_read_index].subtitle = NULL; } + if (state->queue[state->queue_read_index].badge_name) + { + free(state->queue[state->queue_read_index].badge_name); + state->queue[state->queue_read_index].badge_name = NULL; + } + if (state->queue[state->queue_read_index].badge) { video_driver_texture_unload(&state->queue[state->queue_read_index].badge); @@ -565,7 +588,7 @@ void gfx_widgets_push_achievement(const char* title, const char* subtitle, const /* important - this must be done outside the lock because it has the potential to need to * lock the video thread, which may be waiting for the popup queue lock to render popups */ - uintptr_t badge_id = rcheevos_get_badge_texture(badge, 0); + uintptr_t badge_id = rcheevos_get_badge_texture(badge, false, true); if (state->queue_read_index < 0) { @@ -601,6 +624,8 @@ void gfx_widgets_push_achievement(const char* title, const char* subtitle, const state->queue[state->queue_write_index].badge = badge_id; state->queue[state->queue_write_index].title = strdup(title); state->queue[state->queue_write_index].subtitle = strdup(subtitle); + state->queue[state->queue_write_index].badge_name = badge_id ? NULL : strdup(badge); + state->queue[state->queue_write_index].badge_retry = 0; state->queue_write_index = (state->queue_write_index + 1) % ARRAY_SIZE(state->queue); diff --git a/gfx/widgets/gfx_widget_leaderboard_display.c b/gfx/widgets/gfx_widget_leaderboard_display.c index eb911b2f9c..5fabf5bc34 100644 --- a/gfx/widgets/gfx_widget_leaderboard_display.c +++ b/gfx/widgets/gfx_widget_leaderboard_display.c @@ -342,6 +342,21 @@ static void gfx_widget_leaderboard_display_frame(void* data, void* userdata) #endif } +void gfx_widgets_clear_leaderboard_displays(void) +{ + gfx_widget_leaderboard_display_state_t* state = &p_w_leaderboard_display_st; + +#ifdef HAVE_THREADS + slock_lock(state->array_lock); +#endif + + state->tracker_count = 0; + +#ifdef HAVE_THREADS + slock_unlock(state->array_lock); +#endif +} + void gfx_widgets_set_leaderboard_display(unsigned id, const char* value) { unsigned i; @@ -420,6 +435,21 @@ void gfx_widgets_set_leaderboard_display(unsigned id, const char* value) #endif } +void gfx_widgets_clear_challenge_displays(void) +{ + gfx_widget_leaderboard_display_state_t* state = &p_w_leaderboard_display_st; + +#ifdef HAVE_THREADS + slock_lock(state->array_lock); +#endif + + state->challenge_count = 0; + +#ifdef HAVE_THREADS + slock_unlock(state->array_lock); +#endif +} + void gfx_widgets_set_challenge_display(unsigned id, const char* badge) { unsigned i; @@ -427,7 +457,7 @@ void gfx_widgets_set_challenge_display(unsigned id, const char* badge) /* important - this must be done outside the lock because it has the potential to need to * lock the video thread, which may be waiting for the popup queue lock to render popups */ - uintptr_t badge_id = badge ? rcheevos_get_badge_texture(badge, 0) : 0; + uintptr_t badge_id = badge ? rcheevos_get_badge_texture(badge, false, true) : 0; uintptr_t old_badge_id = 0; #ifdef HAVE_THREADS @@ -500,7 +530,7 @@ void gfx_widget_set_achievement_progress(const char* badge, const char* progress { /* show indicator */ state->progress_tracker.show_until = cpu_features_get_time_usec() + CHEEVO_PROGRESS_TRACKER_DURATION * 1000; - state->progress_tracker.image = rcheevos_get_badge_texture(badge, 1); + state->progress_tracker.image = rcheevos_get_badge_texture(badge, true, true); snprintf(state->progress_tracker.display, sizeof(state->progress_tracker.display), "%s", progress); state->progress_tracker.width = (uint16_t)font_driver_get_message_width( diff --git a/network/netplay/netplay.h b/network/netplay/netplay.h index 48e146fe77..ea027f5f8f 100644 --- a/network/netplay/netplay.h +++ b/network/netplay/netplay.h @@ -195,6 +195,8 @@ void deinit_netplay(void); bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data); +bool netplay_is_spectating(void); + #ifdef HAVE_NETPLAYDISCOVERY /** Initialize Netplay discovery */ bool init_netplay_discovery(void); diff --git a/network/netplay/netplay_frontend.c b/network/netplay/netplay_frontend.c index c329eba228..7ff6522c88 100644 --- a/network/netplay/netplay_frontend.c +++ b/network/netplay/netplay_frontend.c @@ -75,6 +75,10 @@ #include "../discord.h" #endif +#ifdef HAVE_CHEEVOS +#include "../cheevos/cheevos.h" +#endif + #include "netplay_private.h" #ifdef TCP_NODELAY @@ -1953,6 +1957,11 @@ static bool netplay_handshake_pre_sync(netplay_t *netplay, if (!settings->bools.netplay_start_as_spectator) return netplay_cmd_mode(netplay, NETPLAY_CONNECTION_PLAYING); +#ifdef HAVE_CHEEVOS + /* Not going to be promoted to player - let achievement system know that we're spectating */ + rcheevos_spectating_changed(); +#endif + return true; } @@ -4645,6 +4654,10 @@ static void netplay_announce_play_spectate(netplay_t *netplay, return; } +#ifdef HAVE_CHEEVOS + rcheevos_spectating_changed(); +#endif + RARCH_LOG("[Netplay] %s\n", dmsg); runloop_msg_queue_push(dmsg, 1, 180, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); @@ -5754,6 +5767,9 @@ static bool netplay_get_cmd(netplay_t *netplay, } else /* YOU && !PLAYING */ { +#ifdef HAVE_CHEEVOS + rcheevos_spectating_changed(); /* should be a no-op, but synchronize anyway */ +#endif /* I'm no longer playing, but I should already know this */ if (netplay->self_mode != NETPLAY_CONNECTION_SPECTATING) { @@ -5858,6 +5874,9 @@ static bool netplay_get_cmd(netplay_t *netplay, runloop_msg_queue_push(dmsg, 1, 180, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } +#ifdef HAVE_CHEEVOS + rcheevos_spectating_changed(); /* synchronize mode */ +#endif break; case NETPLAY_CMD_DISCONNECT: @@ -9028,6 +9047,14 @@ static bool netplay_have_any_active_connection(netplay_t *netplay) return false; } +bool netplay_is_spectating(void) +{ + /* helper function to check the spectating flag without being blocked by the netplay_driver_ctl guard */ + net_driver_state_t* net_st = &networking_driver_st; + netplay_t* netplay = net_st->data; + return (netplay && (netplay->self_mode == NETPLAY_CONNECTION_SPECTATING)); +} + /** * netplay_driver_ctl * diff --git a/runloop.c b/runloop.c index a84ae27e20..2156564be1 100644 --- a/runloop.c +++ b/runloop.c @@ -6961,6 +6961,10 @@ int runloop_iterate(void) } else runloop_set_frame_limit(&video_st->av_info, settings->floats.fastforward_ratio); +#endif +#ifdef HAVE_CHEEVOS + if (cheevos_enable) + rcheevos_idle(); #endif goto end; case RUNLOOP_STATE_ITERATE: