RetroArch/cheevos/cheevos.c
Ophidon 7b711214a7
Slang Subframe Shaders Feature (#16209)
Adds support for sub-frame shaders to vulkan/glcore/dx10-11-12.

Builds on the concept already present for frame duplication in use for BFI, to present multiple 'sub' frames per real frame to the shaders, so they can run at a higher framerate than the content framerate. Must be enabled via subframe shaders setting under synchronization settings to be active.

Will allow BFI to be implemented inside of the shaders, among any other use for the higher framerate shader authors can devise.

CurrentSubFrame and TotalSubFrames have been available inside the shaders to track what they want to do on an given subframe. TotalSubFrames will always be 1 when the setting is disabled (and when in menu/ff/pause). Framecount will not increment on sub-frames, as it does not for injected bfi frames now. Should not interfere with any existing shaders that do not check for subframes.
2024-02-09 03:12:55 -08:00

3592 lines
109 KiB
C

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