mirror of
https://github.com/libretro/RetroArch.git
synced 2024-11-27 18:20:27 +00:00
2762 lines
83 KiB
C
2762 lines
83 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2015-2016 - Andre Leiradella
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <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 <net/net_http.h>
|
|
#include <libretro.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "../config.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_MENU
|
|
#include "../menu/menu_driver.h"
|
|
#include "../menu/menu_entries.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
#include "../gfx/gfx_widgets.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_THREADS
|
|
#include <rthreads/rthreads.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_DISCORD
|
|
#include "../network/discord.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_CHEATS
|
|
#include "../cheat_manager.h"
|
|
#endif
|
|
|
|
#include "badges.h"
|
|
#include "cheevos.h"
|
|
#include "cheevos_memory.h"
|
|
#include "cheevos_parser.h"
|
|
#include "util.h"
|
|
|
|
#include "../file_path_special.h"
|
|
#include "../paths.h"
|
|
#include "../command.h"
|
|
#include "../dynamic.h"
|
|
#include "../configuration.h"
|
|
#include "../performance_counters.h"
|
|
#include "../msg_hash.h"
|
|
#include "../retroarch.h"
|
|
#include "../core.h"
|
|
#include "../core_option_manager.h"
|
|
#include "../version.h"
|
|
|
|
#include "../frontend/frontend_driver.h"
|
|
#include "../network/net_http_special.h"
|
|
#include "../tasks/tasks_internal.h"
|
|
|
|
#include "../deps/rcheevos/include/rcheevos.h"
|
|
#include "../deps/rcheevos/include/rurl.h"
|
|
#include "../deps/rcheevos/include/rc_hash.h"
|
|
|
|
/* Define this macro to prevent cheevos from being deactivated. */
|
|
#undef CHEEVOS_DONT_DEACTIVATE
|
|
|
|
/* Define this macro to load a JSON file from disk instead of downloading
|
|
* from retroachievements.org. */
|
|
#undef CHEEVOS_JSON_OVERRIDE
|
|
|
|
/* Define this macro with a string to save the JSON file to disk with
|
|
* that name. */
|
|
#undef CHEEVOS_SAVE_JSON
|
|
|
|
/* Define this macro to log URLs. */
|
|
#undef CHEEVOS_LOG_URLS
|
|
|
|
/* Define this macro to have the password and token logged. THIS WILL DISCLOSE
|
|
* THE USER'S PASSWORD, TAKE CARE! */
|
|
#undef CHEEVOS_LOG_PASSWORD
|
|
|
|
/* Define this macro to log downloaded badge images. */
|
|
#undef CHEEVOS_LOG_BADGES
|
|
|
|
/* Define this macro to capture how long it takes to generate a hash */
|
|
#undef CHEEVOS_TIME_HASH
|
|
|
|
/* Number of usecs to wait between posting rich presence to the site. */
|
|
/* Keep consistent with SERVER_PING_FREQUENCY from RAIntegration. */
|
|
#define CHEEVOS_PING_FREQUENCY 2 * 60 * 1000000
|
|
|
|
enum rcheevos_async_io_type
|
|
{
|
|
CHEEVOS_ASYNC_RICHPRESENCE = 0,
|
|
CHEEVOS_ASYNC_AWARD_ACHIEVEMENT,
|
|
CHEEVOS_ASYNC_SUBMIT_LBOARD
|
|
};
|
|
|
|
typedef struct rcheevos_async_io_request
|
|
{
|
|
const char* success_message;
|
|
const char* failure_message;
|
|
int id;
|
|
int value;
|
|
int attempt_count;
|
|
char user_agent[256];
|
|
char type;
|
|
char hardcore;
|
|
} rcheevos_async_io_request;
|
|
|
|
typedef struct
|
|
{
|
|
rc_runtime_t runtime;
|
|
rcheevos_rapatchdata_t patchdata; /* ptr alignment */
|
|
rcheevos_memory_regions_t memory; /* ptr alignment */
|
|
|
|
retro_task_t* task;
|
|
#ifdef HAVE_THREADS
|
|
slock_t* task_lock;
|
|
#endif
|
|
|
|
char token[32];
|
|
char hash[33];
|
|
char user_agent_prefix[128];
|
|
|
|
bool hardcore_active;
|
|
bool loaded;
|
|
bool core_supports;
|
|
bool leaderboards_enabled;
|
|
bool leaderboard_notifications;
|
|
bool leaderboard_trackers;
|
|
} rcheevos_locals_t;
|
|
|
|
static rcheevos_locals_t rcheevos_locals =
|
|
{
|
|
{0}, /* runtime */
|
|
{0}, /* patchdata */
|
|
{{0}},/* memory */
|
|
NULL, /* task */
|
|
#ifdef HAVE_THREADS
|
|
NULL, /* task_lock */
|
|
#endif
|
|
{0}, /* token */
|
|
"N/A",/* hash */
|
|
"", /* user_agent_prefix */
|
|
false,/* hardcore_active */
|
|
false,/* loaded */
|
|
true, /* core_supports */
|
|
false,/* leaderboards_enabled */
|
|
false,/* leaderboard_notifications */
|
|
false /* leaderboard_trackers */
|
|
};
|
|
|
|
#ifdef HAVE_THREADS
|
|
#define CHEEVOS_LOCK(l) do { slock_lock(l); } while (0)
|
|
#define CHEEVOS_UNLOCK(l) do { slock_unlock(l); } while (0)
|
|
#else
|
|
#define CHEEVOS_LOCK(l)
|
|
#define CHEEVOS_UNLOCK(l)
|
|
#endif
|
|
|
|
#define CHEEVOS_MB(x) ((x) * 1024 * 1024)
|
|
|
|
/* Forward declaration */
|
|
static void rcheevos_async_task_callback(
|
|
retro_task_t* task, void* task_data, void* user_data, const char* error);
|
|
static void rcheevos_async_submit_lboard(rcheevos_locals_t *locals,
|
|
rcheevos_async_io_request* request);
|
|
|
|
/*****************************************************************************
|
|
Supporting functions.
|
|
*****************************************************************************/
|
|
|
|
#ifndef CHEEVOS_VERBOSE
|
|
void rcheevos_log(const char *fmt, ...)
|
|
{
|
|
(void)fmt;
|
|
}
|
|
#endif
|
|
|
|
static void rcheevos_get_user_agent(
|
|
rcheevos_locals_t *locals,
|
|
char *buffer, size_t len)
|
|
{
|
|
struct retro_system_info *system = runloop_get_libretro_system_info();
|
|
const char* scan;
|
|
char* ptr;
|
|
|
|
if (!locals->user_agent_prefix[0])
|
|
{
|
|
const frontend_ctx_driver_t *frontend = frontend_get_ptr();
|
|
int major, minor;
|
|
char tmp[64];
|
|
|
|
ptr = locals->user_agent_prefix + snprintf(locals->user_agent_prefix, sizeof(locals->user_agent_prefix), "RetroArch/%s", PACKAGE_VERSION);
|
|
|
|
if (frontend && frontend->get_os)
|
|
{
|
|
frontend->get_os(tmp, sizeof(tmp), &major, &minor);
|
|
ptr += sprintf(ptr, " (%s %d.%d)", tmp, major, minor);
|
|
}
|
|
}
|
|
|
|
ptr = buffer + snprintf(buffer, len, "%s", locals->user_agent_prefix);
|
|
|
|
if (system && !string_is_empty(system->library_name))
|
|
{
|
|
const char* path = path_get(RARCH_PATH_CORE);
|
|
if (!string_is_empty(path))
|
|
{
|
|
sprintf(ptr, " %s", path_basename(path));
|
|
path_remove_extension(ptr);
|
|
ptr += strlen(ptr);
|
|
}
|
|
else
|
|
{
|
|
*ptr++ = ' ';
|
|
|
|
scan = system->library_name;
|
|
while (*scan)
|
|
{
|
|
if (*scan == ' ')
|
|
{
|
|
*ptr++ = '_';
|
|
++scan;
|
|
}
|
|
else
|
|
*ptr++ = *scan++;
|
|
}
|
|
}
|
|
|
|
if (system->library_version)
|
|
{
|
|
*ptr++ = '/';
|
|
|
|
scan = system->library_version;
|
|
while (*scan)
|
|
{
|
|
if (*scan == ' ')
|
|
{
|
|
*ptr++ = '_';
|
|
++scan;
|
|
}
|
|
else
|
|
*ptr++ = *scan++;
|
|
}
|
|
}
|
|
}
|
|
|
|
*ptr = '\0';
|
|
}
|
|
|
|
#ifdef CHEEVOS_LOG_URLS
|
|
static void rcheevos_filter_url_param(char* url, char* param)
|
|
{
|
|
char *next;
|
|
size_t param_len = strlen(param);
|
|
char *start = strchr(url, '?');
|
|
if (!start)
|
|
start = url;
|
|
else
|
|
++start;
|
|
|
|
do
|
|
{
|
|
next = strchr(start, '&');
|
|
|
|
if (start[param_len] == '=' && memcmp(start, param, param_len) == 0)
|
|
{
|
|
if (next)
|
|
strcpy(start, next + 1);
|
|
else if (start > url)
|
|
start[-1] = '\0';
|
|
else
|
|
*start = '\0';
|
|
|
|
return;
|
|
}
|
|
|
|
if (!next)
|
|
return;
|
|
|
|
start = next + 1;
|
|
} while (1);
|
|
}
|
|
#endif
|
|
|
|
static void rcheevos_log_url(const char* api, const char* url)
|
|
{
|
|
#ifdef CHEEVOS_LOG_URLS
|
|
#ifdef CHEEVOS_LOG_PASSWORD
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url);
|
|
#else
|
|
char copy[256];
|
|
strlcpy(copy, url, sizeof(copy));
|
|
rcheevos_filter_url_param(copy, "p");
|
|
rcheevos_filter_url_param(copy, "t");
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, copy);
|
|
#endif
|
|
#else
|
|
(void)api;
|
|
(void)url;
|
|
#endif
|
|
}
|
|
|
|
static void rcheevos_log_post_url(
|
|
const char* api,
|
|
const char* url,
|
|
const char* post)
|
|
{
|
|
#ifdef CHEEVOS_LOG_URLS
|
|
#ifdef CHEEVOS_LOG_PASSWORD
|
|
if (post && post[0])
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s&%s\n", api, url, post);
|
|
else
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url);
|
|
#else
|
|
if (post && post[0])
|
|
{
|
|
char post_copy[2048];
|
|
strlcpy(post_copy, post, sizeof(post_copy));
|
|
rcheevos_filter_url_param(post_copy, "p");
|
|
rcheevos_filter_url_param(post_copy, "t");
|
|
|
|
if (post_copy[0])
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s&%s\n", api, url, post_copy);
|
|
else
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url);
|
|
}
|
|
else
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s: %s\n", api, url);
|
|
}
|
|
#endif
|
|
#else
|
|
(void)api;
|
|
(void)url;
|
|
(void)post;
|
|
#endif
|
|
}
|
|
|
|
static bool rcheevos_condset_contains_memref(const rc_condset_t* condset, const rc_memref_value_t* memref)
|
|
{
|
|
if (condset)
|
|
{
|
|
rc_condition_t* cond = NULL;
|
|
for (cond = condset->conditions; cond; cond = cond->next)
|
|
{
|
|
if ( cond->operand1.value.memref == memref
|
|
|| cond->operand2.value.memref == memref)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool rcheevos_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_value_t* memref)
|
|
{
|
|
rc_condset_t* condset;
|
|
if (!trigger)
|
|
return false;
|
|
|
|
if (rcheevos_condset_contains_memref(trigger->requirement, memref))
|
|
return true;
|
|
|
|
for (condset = trigger->alternative; condset; condset = condset->next)
|
|
{
|
|
if (rcheevos_condset_contains_memref(condset, memref))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void rcheevos_invalidate_address(unsigned address)
|
|
{
|
|
unsigned i, count;
|
|
rcheevos_racheevo_t* cheevo = NULL;
|
|
rcheevos_ralboard_t* lboard = NULL;
|
|
/* remove the invalid memref from the chain so we don't try to evaluate it in the future.
|
|
* it's still there, so anything referencing it will continue to fetch 0.
|
|
*/
|
|
rc_memref_value_t **last_memref = &rcheevos_locals.runtime.memrefs;
|
|
rc_memref_value_t *memref = *last_memref;
|
|
|
|
do
|
|
{
|
|
if (memref->memref.address == address && !memref->memref.is_indirect)
|
|
{
|
|
*last_memref = memref->next;
|
|
break;
|
|
}
|
|
|
|
last_memref = &memref->next;
|
|
memref = *last_memref;
|
|
} while (memref);
|
|
|
|
/* If the address is only used indirectly,
|
|
* don't disable anything dependent on it */
|
|
if (!memref)
|
|
return;
|
|
|
|
/* Disable any achievements dependent on the address */
|
|
for (i = 0; i < 2; ++i)
|
|
{
|
|
if (i == 0)
|
|
{
|
|
cheevo = rcheevos_locals.patchdata.core;
|
|
count = rcheevos_locals.patchdata.core_count;
|
|
}
|
|
else
|
|
{
|
|
cheevo = rcheevos_locals.patchdata.unofficial;
|
|
count = rcheevos_locals.patchdata.unofficial_count;
|
|
}
|
|
|
|
while (count--)
|
|
{
|
|
if (cheevo->memaddr)
|
|
{
|
|
const rc_trigger_t* trigger = rc_runtime_get_achievement(&rcheevos_locals.runtime, cheevo->id);
|
|
if (trigger && rcheevos_trigger_contains_memref(trigger, memref))
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "Achievement disabled (invalid address %06X): %s\n", address, cheevo->title);
|
|
rc_runtime_deactivate_achievement(&rcheevos_locals.runtime, cheevo->id);
|
|
|
|
CHEEVOS_FREE(cheevo->memaddr);
|
|
cheevo->memaddr = NULL;
|
|
}
|
|
}
|
|
|
|
++cheevo;
|
|
}
|
|
}
|
|
|
|
/* disable any leaderboards dependent on the address */
|
|
lboard = rcheevos_locals.patchdata.lboards;
|
|
for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard)
|
|
{
|
|
if (lboard->mem)
|
|
{
|
|
const rc_lboard_t* rc_lboard = rc_runtime_get_lboard(&rcheevos_locals.runtime, lboard->id);
|
|
if (rc_lboard &&
|
|
(rcheevos_trigger_contains_memref(&rc_lboard->start, memref) ||
|
|
rcheevos_trigger_contains_memref(&rc_lboard->cancel, memref) ||
|
|
rcheevos_trigger_contains_memref(&rc_lboard->submit, memref) ||
|
|
rcheevos_condset_contains_memref(rc_lboard->value.conditions, memref)))
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "Leaderboard disabled (invalid address %06X): %s\n", address, lboard->title);
|
|
rc_runtime_deactivate_lboard(&rcheevos_locals.runtime, lboard->id);
|
|
|
|
CHEEVOS_FREE(lboard->mem);
|
|
lboard->mem = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
uint8_t* rcheevos_patch_address(unsigned address)
|
|
{
|
|
return rcheevos_memory_find(&rcheevos_locals.memory, address);
|
|
}
|
|
|
|
static unsigned rcheevos_peek(unsigned address, unsigned num_bytes, void* ud)
|
|
{
|
|
uint8_t* data = rcheevos_memory_find(&rcheevos_locals.memory, address);
|
|
if (data)
|
|
{
|
|
switch (num_bytes)
|
|
{
|
|
case 4:
|
|
return (data[3] << 24) | (data[2] << 16) |
|
|
(data[1] << 8) | (data[0]);
|
|
case 3:
|
|
return (data[2] << 16) | (data[1] << 8) | (data[0]);
|
|
case 2:
|
|
return (data[1] << 8) | (data[0]);
|
|
case 1:
|
|
return data[0];
|
|
}
|
|
}
|
|
|
|
rcheevos_invalidate_address(address);
|
|
return 0;
|
|
}
|
|
|
|
static void rcheevos_async_award_achievement(
|
|
rcheevos_locals_t *locals,
|
|
rcheevos_async_io_request* request)
|
|
{
|
|
char buffer[256];
|
|
settings_t *settings = config_get_ptr();
|
|
int ret = rc_url_award_cheevo(buffer, sizeof(buffer), settings->arrays.cheevos_username, locals->token, request->id, request->hardcore, locals->hash);
|
|
|
|
if (ret != 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "Buffer too small to create URL\n");
|
|
free(request);
|
|
return;
|
|
}
|
|
|
|
rcheevos_log_url("rc_url_award_cheevo", buffer);
|
|
task_push_http_transfer_with_user_agent(buffer, true, NULL, request->user_agent, rcheevos_async_task_callback, request);
|
|
|
|
#ifdef HAVE_AUDIOMIXER
|
|
if (settings->bools.cheevos_unlock_sound_enable)
|
|
audio_driver_mixer_play_menu_sound(AUDIO_MIXER_SYSTEM_SLOT_ACHIEVEMENT_UNLOCK);
|
|
#endif
|
|
}
|
|
|
|
static retro_time_t rcheevos_async_send_rich_presence(
|
|
rcheevos_locals_t *locals,
|
|
rcheevos_async_io_request* request)
|
|
{
|
|
char url[256], post_data[1024];
|
|
settings_t *settings = config_get_ptr();
|
|
const char *cheevos_username = settings->arrays.cheevos_username;
|
|
bool cheevos_richpresence_enable = settings->bools.cheevos_richpresence_enable;
|
|
int ret = rc_url_ping(
|
|
url, sizeof(url), post_data, sizeof(post_data),
|
|
cheevos_username, locals->token, locals->patchdata.game_id,
|
|
cheevos_richpresence_enable
|
|
? rc_runtime_get_richpresence(&locals->runtime)
|
|
: "");
|
|
|
|
if (ret < 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n");
|
|
}
|
|
else
|
|
{
|
|
rcheevos_log_post_url("rc_url_ping", url, post_data);
|
|
|
|
rcheevos_get_user_agent(locals,
|
|
request->user_agent, sizeof(request->user_agent));
|
|
task_push_http_post_transfer_with_user_agent(url, post_data, true, "POST", request->user_agent, NULL, NULL);
|
|
}
|
|
|
|
#ifdef HAVE_DISCORD
|
|
if (locals->runtime.richpresence_display_buffer)
|
|
{
|
|
if (settings->bools.discord_enable
|
|
&& discord_is_ready())
|
|
discord_update(DISCORD_PRESENCE_RETROACHIEVEMENTS);
|
|
}
|
|
#endif
|
|
|
|
/* Update rich presence every two minutes */
|
|
if (cheevos_richpresence_enable)
|
|
return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY;
|
|
|
|
/* Send ping every four minutes */
|
|
return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY * 2;
|
|
}
|
|
|
|
static void rcheevos_async_task_handler(retro_task_t* task)
|
|
{
|
|
rcheevos_async_io_request* request = (rcheevos_async_io_request*)
|
|
task->user_data;
|
|
|
|
switch (request->type)
|
|
{
|
|
case CHEEVOS_ASYNC_RICHPRESENCE:
|
|
/* update the task to fire again in two minutes */
|
|
if (request->id == (int)rcheevos_locals.patchdata.game_id)
|
|
task->when = rcheevos_async_send_rich_presence(&rcheevos_locals,
|
|
request);
|
|
else
|
|
{
|
|
/* game changed; stop the recurring task - a new one will
|
|
* be scheduled for the next game */
|
|
task_set_finished(task, 1);
|
|
free(request);
|
|
}
|
|
break;
|
|
|
|
case CHEEVOS_ASYNC_AWARD_ACHIEVEMENT:
|
|
rcheevos_async_award_achievement(&rcheevos_locals, request);
|
|
task_set_finished(task, 1);
|
|
break;
|
|
|
|
case CHEEVOS_ASYNC_SUBMIT_LBOARD:
|
|
rcheevos_async_submit_lboard(&rcheevos_locals, request);
|
|
task_set_finished(task, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void rcheevos_async_schedule(
|
|
rcheevos_async_io_request* request, retro_time_t delay)
|
|
{
|
|
retro_task_t* task = task_init();
|
|
task->when = cpu_features_get_time_usec() + delay;
|
|
task->handler = rcheevos_async_task_handler;
|
|
task->user_data = request;
|
|
task->progress = -1;
|
|
task_queue_push(task);
|
|
}
|
|
|
|
static void rcheevos_async_task_callback(
|
|
retro_task_t* task, void* task_data, void* user_data, const char* error)
|
|
{
|
|
rcheevos_async_io_request* request = (rcheevos_async_io_request*)user_data;
|
|
|
|
if (!error)
|
|
{
|
|
char buffer[224];
|
|
const http_transfer_data_t* data = (http_transfer_data_t*)task->task_data;
|
|
if (rcheevos_get_json_error(data->data, buffer, sizeof(buffer)) == RC_OK)
|
|
{
|
|
char errbuf[256];
|
|
snprintf(errbuf, sizeof(errbuf), "%s %u: %s", request->failure_message, request->id, buffer);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", errbuf);
|
|
|
|
switch (request->type)
|
|
{
|
|
case CHEEVOS_ASYNC_RICHPRESENCE:
|
|
/* don't bother informing user when rich presence update fails */
|
|
break;
|
|
|
|
case CHEEVOS_ASYNC_AWARD_ACHIEVEMENT:
|
|
/* ignore already unlocked */
|
|
if (string_starts_with_size(buffer, "User already has ",
|
|
STRLEN_CONST("User already has ")))
|
|
break;
|
|
/* fallthrough to default */
|
|
|
|
default:
|
|
runloop_msg_queue_push(errbuf, 0, 5 * 60, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s %u\n", request->success_message, request->id);
|
|
}
|
|
|
|
free(request);
|
|
}
|
|
else
|
|
{
|
|
/* double the wait between each attempt until we hit a maximum delay of two minutes
|
|
* 250ms -> 500ms -> 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s... */
|
|
retro_time_t retry_delay = (request->attempt_count > 8) ? (120 * 1000 * 1000) : ((250 * 1000) << request->attempt_count);
|
|
|
|
request->attempt_count++;
|
|
rcheevos_async_schedule(request, retry_delay);
|
|
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s\n", request->failure_message, request->id, error);
|
|
}
|
|
}
|
|
|
|
static void rcheevos_activate_achievements(rcheevos_locals_t *locals,
|
|
rcheevos_racheevo_t* cheevo, unsigned count, unsigned flags)
|
|
{
|
|
settings_t *settings = config_get_ptr();
|
|
char buffer[256];
|
|
unsigned i;
|
|
int res;
|
|
|
|
for (i = 0; i < count; i++, cheevo++)
|
|
{
|
|
res = rc_runtime_activate_achievement(&locals->runtime, cheevo->id, cheevo->memaddr, NULL, 0);
|
|
|
|
if (res < 0)
|
|
{
|
|
snprintf(buffer, sizeof(buffer),
|
|
"Could not activate achievement %d \"%s\": %s",
|
|
cheevo->id, cheevo->title, rc_error_str(res));
|
|
|
|
if (settings->bools.cheevos_verbose_enable)
|
|
runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "%s: mem %s\n", buffer, cheevo->memaddr);
|
|
CHEEVOS_FREE(cheevo->memaddr);
|
|
cheevo->memaddr = NULL;
|
|
continue;
|
|
}
|
|
|
|
cheevo->active = RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE | flags;
|
|
}
|
|
}
|
|
|
|
static int rcheevos_parse(rcheevos_locals_t *locals, const char* json)
|
|
{
|
|
char buffer[256];
|
|
settings_t *settings = config_get_ptr();
|
|
unsigned j = 0;
|
|
unsigned count = 0;
|
|
rcheevos_ralboard_t* lboard = NULL;
|
|
int res = rcheevos_get_patchdata(
|
|
json, &locals->patchdata);
|
|
|
|
if (res != 0)
|
|
{
|
|
char* ptr = buffer + snprintf(buffer, sizeof(buffer), "Error retrieving achievement data: ");
|
|
|
|
/* extract the Error field from the JSON. if not found, remove the colon from the message */
|
|
if (rcheevos_get_json_error(json, ptr, sizeof(buffer) - (ptr - buffer)) == -1)
|
|
{
|
|
ptr[-2] = '\0'; /* TODO/FIXME - writing 1 byte into a region of size 0 [-Wstringop-overflow=] at offset -2 to object 'buffer' with size 256 declared here */
|
|
}
|
|
|
|
runloop_msg_queue_push(buffer, 0, 5 * 60, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING);
|
|
|
|
RARCH_ERR(RCHEEVOS_TAG "%s", buffer);
|
|
return -1;
|
|
}
|
|
|
|
if ( locals->patchdata.core_count == 0
|
|
&& locals->patchdata.unofficial_count == 0
|
|
&& locals->patchdata.lboard_count == 0)
|
|
{
|
|
rcheevos_free_patchdata(&locals->patchdata);
|
|
return 0;
|
|
}
|
|
|
|
if (!rcheevos_memory_init(&locals->memory, locals->patchdata.console_id))
|
|
{
|
|
/* some cores (like Mupen64-Plus) don't expose the
|
|
* memory until the first call to retro_run.
|
|
* in that case, there will be a total_size of
|
|
* memory reported by the core, but init will return
|
|
* false, as all of the pointers were null.
|
|
*/
|
|
|
|
/* reset the memory count and we'll re-evaluate in rcheevos_test() */
|
|
if (locals->memory.total_size != 0)
|
|
locals->memory.count = 0;
|
|
else
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "No memory exposed by core.\n");
|
|
rcheevos_locals.core_supports = false;
|
|
|
|
if (settings->bools.cheevos_verbose_enable)
|
|
{
|
|
runloop_msg_queue_push(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE),
|
|
0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING);
|
|
}
|
|
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
/* Initialize. */
|
|
rcheevos_activate_achievements(locals, locals->patchdata.core, locals->patchdata.core_count, 0);
|
|
if (settings->bools.cheevos_test_unofficial)
|
|
{
|
|
rcheevos_activate_achievements(locals, locals->patchdata.unofficial,
|
|
locals->patchdata.unofficial_count, RCHEEVOS_ACTIVE_UNOFFICIAL);
|
|
}
|
|
|
|
if (locals->hardcore_active && locals->leaderboards_enabled)
|
|
{
|
|
lboard = locals->patchdata.lboards;
|
|
count = locals->patchdata.lboard_count;
|
|
|
|
for (j = 0; j < count; j++, lboard++)
|
|
{
|
|
res = rc_runtime_activate_lboard(&locals->runtime, lboard->id, lboard->mem, NULL, 0);
|
|
|
|
if (res < 0)
|
|
{
|
|
snprintf(buffer, sizeof(buffer),
|
|
"Could not activate leaderboard %d \"%s\": %s",
|
|
lboard->id, lboard->title, rc_error_str(res));
|
|
|
|
if (settings->bools.cheevos_verbose_enable)
|
|
runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "%s mem: %s\n", buffer, lboard->mem);
|
|
CHEEVOS_FREE(lboard->mem);
|
|
lboard->mem = NULL;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
res = RC_MISSING_DISPLAY_STRING;
|
|
if ( locals->patchdata.richpresence_script
|
|
&& *locals->patchdata.richpresence_script)
|
|
{
|
|
res = rc_runtime_activate_richpresence(&locals->runtime, locals->patchdata.richpresence_script, NULL, 0);
|
|
|
|
if (res < 0)
|
|
{
|
|
snprintf(buffer, sizeof(buffer),
|
|
"Could not activate rich presence: %s",
|
|
rc_error_str(res));
|
|
|
|
if (settings->bools.cheevos_verbose_enable)
|
|
runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "%s\n", buffer);
|
|
}
|
|
}
|
|
|
|
if (res < 0 && locals->patchdata.title)
|
|
{
|
|
size_t len = strlen(locals->patchdata.title);
|
|
|
|
if (!locals->runtime.richpresence_display_buffer)
|
|
locals->runtime.richpresence_display_buffer = (char*)malloc(len + 9);
|
|
|
|
if (locals->runtime.richpresence_display_buffer)
|
|
{
|
|
memcpy(locals->runtime.richpresence_display_buffer, "Playing ", 8);
|
|
memcpy(&locals->runtime.richpresence_display_buffer[8], locals->patchdata.title, len + 1);
|
|
}
|
|
}
|
|
|
|
/* schedule the first rich presence call in 30 seconds */
|
|
{
|
|
rcheevos_async_io_request* request = (rcheevos_async_io_request*)
|
|
calloc(1, sizeof(rcheevos_async_io_request));
|
|
request->id = locals->patchdata.game_id;
|
|
request->type = CHEEVOS_ASYNC_RICHPRESENCE;
|
|
rcheevos_async_schedule(request, CHEEVOS_PING_FREQUENCY / 4);
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
rcheevos_free_patchdata(&locals->patchdata);
|
|
rcheevos_memory_destroy(&locals->memory);
|
|
return -1;
|
|
}
|
|
|
|
static rcheevos_racheevo_t* rcheevos_find_cheevo(unsigned id)
|
|
{
|
|
unsigned i;
|
|
rcheevos_racheevo_t* cheevo;
|
|
|
|
cheevo = rcheevos_locals.patchdata.core;
|
|
for (i = 0; i < rcheevos_locals.patchdata.core_count; i++, cheevo++)
|
|
{
|
|
if (cheevo->id == id)
|
|
return cheevo;
|
|
}
|
|
|
|
cheevo = rcheevos_locals.patchdata.unofficial;
|
|
for (i = 0; i < rcheevos_locals.patchdata.unofficial_count; i++, cheevo++)
|
|
{
|
|
if (cheevo->id == id)
|
|
return cheevo;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void rcheevos_award_achievement(rcheevos_locals_t *locals, rcheevos_racheevo_t* cheevo)
|
|
{
|
|
char buffer[256] = "";
|
|
|
|
if (!cheevo)
|
|
return;
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Awarding achievement %u: %s (%s)\n",
|
|
cheevo->id, cheevo->title, cheevo->description);
|
|
|
|
/* Deactivates the cheevo. */
|
|
rc_runtime_deactivate_achievement(&locals->runtime, cheevo->id);
|
|
|
|
cheevo->active &= ~RCHEEVOS_ACTIVE_SOFTCORE;
|
|
if (locals->hardcore_active)
|
|
cheevo->active &= ~RCHEEVOS_ACTIVE_HARDCORE;
|
|
|
|
/* Show the OSD message. */
|
|
{
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
bool widgets_ready = gfx_widgets_ready();
|
|
if (widgets_ready)
|
|
gfx_widgets_push_achievement(cheevo->title, cheevo->badge);
|
|
else
|
|
#endif
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "Achievement Unlocked: %s", cheevo->title);
|
|
runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
runloop_msg_queue_push(cheevo->description, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
}
|
|
|
|
/* Start the award task (unofficial achievement unlocks are not submitted). */
|
|
if (!(cheevo->active & RCHEEVOS_ACTIVE_UNOFFICIAL))
|
|
{
|
|
rcheevos_async_io_request *request = (rcheevos_async_io_request*)calloc(1, sizeof(rcheevos_async_io_request));
|
|
request->type = CHEEVOS_ASYNC_AWARD_ACHIEVEMENT;
|
|
request->id = cheevo->id;
|
|
request->hardcore = locals->hardcore_active ? 1 : 0;
|
|
request->success_message = "Awarded achievement";
|
|
request->failure_message = "Error awarding achievement";
|
|
rcheevos_get_user_agent(locals,
|
|
request->user_agent, sizeof(request->user_agent));
|
|
rcheevos_async_award_achievement(locals, request);
|
|
}
|
|
|
|
#ifdef HAVE_SCREENSHOTS
|
|
{
|
|
settings_t *settings = config_get_ptr();
|
|
/* Take a screenshot of the achievement. */
|
|
if (settings && settings->bools.cheevos_auto_screenshot)
|
|
{
|
|
char shotname[8192];
|
|
|
|
snprintf(shotname, sizeof(shotname), "%s/%s-cheevo-%u",
|
|
settings->paths.directory_screenshot,
|
|
path_basename(path_get(RARCH_PATH_BASENAME)),
|
|
cheevo->id);
|
|
shotname[sizeof(shotname) - 1] = '\0';
|
|
|
|
if (take_screenshot(settings->paths.directory_screenshot,
|
|
shotname, true,
|
|
video_driver_cached_frame_has_valid_framebuffer(), false, true))
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Captured screenshot for achievement %u\n", cheevo->id);
|
|
else
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Failed to capture screenshot for achievement %u\n", cheevo->id);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void rcheevos_async_submit_lboard(rcheevos_locals_t *locals,
|
|
rcheevos_async_io_request* request)
|
|
{
|
|
char buffer[256];
|
|
settings_t *settings = config_get_ptr();
|
|
int ret = rc_url_submit_lboard(buffer, sizeof(buffer),
|
|
settings->arrays.cheevos_username,
|
|
locals->token, request->id, request->value);
|
|
|
|
if (ret != 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "Buffer too small to create URL\n");
|
|
free(request);
|
|
return;
|
|
}
|
|
|
|
rcheevos_log_url("rc_url_submit_lboard", buffer);
|
|
task_push_http_transfer_with_user_agent(buffer, true, NULL, request->user_agent, rcheevos_async_task_callback, request);
|
|
}
|
|
|
|
static rcheevos_ralboard_t* rcheevos_find_lboard(unsigned id)
|
|
{
|
|
rcheevos_ralboard_t* lboard = rcheevos_locals.patchdata.lboards;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard)
|
|
{
|
|
if (lboard->id == id)
|
|
return lboard;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void rcheevos_lboard_submit(rcheevos_locals_t *locals,
|
|
rcheevos_ralboard_t* lboard, int value)
|
|
{
|
|
char buffer[256];
|
|
char formatted_value[16];
|
|
|
|
/* Show the OSD message (regardless of notifications setting). */
|
|
rc_format_value(formatted_value, sizeof(formatted_value), value, lboard->format);
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Submitting %s for leaderboard %u\n", formatted_value, lboard->id);
|
|
snprintf(buffer, sizeof(buffer), "Submitted %s for %s",
|
|
formatted_value, lboard->title);
|
|
runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
/* Hide the tracker */
|
|
if (gfx_widgets_ready())
|
|
gfx_widgets_set_leaderboard_display(lboard->id, NULL);
|
|
#endif
|
|
|
|
/* Start the submit task. */
|
|
{
|
|
rcheevos_async_io_request* request = (rcheevos_async_io_request*)calloc(1, sizeof(rcheevos_async_io_request));
|
|
request->type = CHEEVOS_ASYNC_SUBMIT_LBOARD;
|
|
request->id = lboard->id;
|
|
request->value = value;
|
|
request->success_message = "Submitted leaderboard";
|
|
request->failure_message = "Error submitting leaderboard";
|
|
rcheevos_get_user_agent(locals,
|
|
request->user_agent, sizeof(request->user_agent));
|
|
rcheevos_async_submit_lboard(locals, request);
|
|
}
|
|
}
|
|
|
|
static void rcheevos_lboard_canceled(rcheevos_ralboard_t * lboard)
|
|
{
|
|
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 (gfx_widgets_ready())
|
|
gfx_widgets_set_leaderboard_display(lboard->id, NULL);
|
|
#endif
|
|
|
|
if (rcheevos_locals.leaderboard_notifications)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "Leaderboard attempt failed: %s", lboard->title);
|
|
runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
}
|
|
|
|
static void rcheevos_lboard_started(rcheevos_ralboard_t * lboard, int value)
|
|
{
|
|
char buffer[256];
|
|
if (!lboard)
|
|
return;
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Leaderboard %u started: %s\n", lboard->id, lboard->title);
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
if (gfx_widgets_ready() && rcheevos_locals.leaderboard_trackers)
|
|
{
|
|
rc_format_value(buffer, sizeof(buffer), value, lboard->format);
|
|
gfx_widgets_set_leaderboard_display(lboard->id, buffer);
|
|
}
|
|
#endif
|
|
|
|
if (rcheevos_locals.leaderboard_notifications)
|
|
{
|
|
if (lboard->description && *lboard->description)
|
|
snprintf(buffer, sizeof(buffer), "Leaderboard attempt started: %s - %s", lboard->title, lboard->description);
|
|
else
|
|
snprintf(buffer, sizeof(buffer), "Leaderboard attempt started: %s", lboard->title);
|
|
|
|
runloop_msg_queue_push(buffer, 0, 2 * 60, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
}
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
static void rcheevos_lboard_updated(rcheevos_ralboard_t* lboard, int value)
|
|
{
|
|
if (!lboard)
|
|
return;
|
|
|
|
if (gfx_widgets_ready() && rcheevos_locals.leaderboard_trackers)
|
|
{
|
|
char buffer[32];
|
|
rc_format_value(buffer, sizeof(buffer), value, lboard->format);
|
|
gfx_widgets_set_leaderboard_display(lboard->id, buffer);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
const char* rcheevos_get_richpresence(void)
|
|
{
|
|
return rc_runtime_get_richpresence(&rcheevos_locals.runtime);
|
|
}
|
|
|
|
void rcheevos_reset_game(void)
|
|
{
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
/* Hide any visible trackers */
|
|
if (gfx_widgets_ready())
|
|
{
|
|
rcheevos_ralboard_t* lboard = rcheevos_locals.patchdata.lboards;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard)
|
|
gfx_widgets_set_leaderboard_display(lboard->id, NULL);
|
|
}
|
|
#endif
|
|
|
|
rc_runtime_reset(&rcheevos_locals.runtime);
|
|
|
|
/* some cores reallocate memory on reset, make sure we update our pointers */
|
|
if (rcheevos_locals.memory.total_size > 0)
|
|
rcheevos_memory_init(&rcheevos_locals.memory, rcheevos_locals.patchdata.console_id);
|
|
}
|
|
|
|
#ifdef HAVE_MENU
|
|
void rcheevos_get_achievement_state(unsigned index,
|
|
char *buffer, size_t len)
|
|
{
|
|
rcheevos_racheevo_t* cheevo;
|
|
enum msg_hash_enums enum_idx;
|
|
bool check_measured = false;
|
|
|
|
if (index < rcheevos_locals.patchdata.core_count)
|
|
{
|
|
enum_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_LOCKED_ENTRY;
|
|
cheevo = rcheevos_locals.patchdata.core ? &rcheevos_locals.patchdata.core[index] : NULL;
|
|
}
|
|
else
|
|
{
|
|
enum_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNOFFICIAL_ENTRY;
|
|
cheevo = rcheevos_locals.patchdata.unofficial ? &rcheevos_locals.patchdata.unofficial[index - rcheevos_locals.patchdata.core_count] : NULL;
|
|
}
|
|
|
|
if (!cheevo || !cheevo->memaddr)
|
|
enum_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNSUPPORTED_ENTRY;
|
|
else if (!(cheevo->active & RCHEEVOS_ACTIVE_HARDCORE))
|
|
enum_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY_HARDCORE;
|
|
else if (!(cheevo->active & RCHEEVOS_ACTIVE_SOFTCORE))
|
|
{
|
|
/* if in hardcore mode, track progress towards hardcore unlock */
|
|
check_measured = rcheevos_locals.hardcore_active;
|
|
|
|
enum_idx = MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY;
|
|
}
|
|
/* Use either "Locked" for core or "Unofficial" for unofficial as set above and track progress */
|
|
else
|
|
check_measured = true;
|
|
|
|
strlcpy(buffer, msg_hash_to_str(enum_idx), len);
|
|
|
|
if (check_measured)
|
|
{
|
|
const rc_trigger_t* trigger = rc_runtime_get_achievement(&rcheevos_locals.runtime, cheevo->id);
|
|
const unsigned int target = trigger->measured_target;
|
|
if (target > 0 && trigger->measured_value > 0)
|
|
{
|
|
char measured_buffer[12];
|
|
const unsigned int value = MIN(trigger->measured_value, target);
|
|
const int percent = (int)(((unsigned long)value) * 100 / target);
|
|
|
|
snprintf(measured_buffer, sizeof(measured_buffer), " - %d%%", percent);
|
|
strlcat(buffer, measured_buffer, len);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rcheevos_append_menu_achievement(
|
|
menu_displaylist_info_t* info, size_t idx, rcheevos_racheevo_t* cheevo)
|
|
{
|
|
bool badge_grayscale;
|
|
|
|
menu_entries_append_enum(info->list, cheevo->title,
|
|
cheevo->description, MENU_ENUM_LABEL_CHEEVOS_LOCKED_ENTRY,
|
|
MENU_SETTINGS_CHEEVOS_START + idx, 0, 0);
|
|
|
|
if (!cheevo->memaddr)
|
|
badge_grayscale = true; /* unsupported */
|
|
else if (!(cheevo->active & RCHEEVOS_ACTIVE_HARDCORE) ||
|
|
!(cheevo->active & RCHEEVOS_ACTIVE_SOFTCORE))
|
|
badge_grayscale = false; /* unlocked */
|
|
else
|
|
badge_grayscale = true; /* locked */
|
|
|
|
cheevos_set_menu_badge(idx, cheevo->badge, badge_grayscale);
|
|
}
|
|
#endif
|
|
|
|
void rcheevos_populate_hardcore_pause_menu(void* data)
|
|
{
|
|
#ifdef HAVE_MENU
|
|
menu_displaylist_info_t* info = (menu_displaylist_info_t*)data;
|
|
settings_t* settings = config_get_ptr();
|
|
bool cheevos_hardcore_mode_enable = settings->bools.cheevos_hardcore_mode_enable;
|
|
|
|
if (cheevos_hardcore_mode_enable && rcheevos_locals.loaded)
|
|
{
|
|
if (rcheevos_locals.hardcore_active)
|
|
{
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE_CANCEL),
|
|
msg_hash_to_str(MENU_ENUM_SUBLABEL_ACHIEVEMENT_PAUSE_CANCEL),
|
|
MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_CANCEL,
|
|
MENU_SETTING_ACTION_CLOSE, 0, 0);
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE),
|
|
MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE,
|
|
MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME_CANCEL),
|
|
msg_hash_to_str(MENU_ENUM_SUBLABEL_ACHIEVEMENT_RESUME_CANCEL),
|
|
MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_CANCEL,
|
|
MENU_SETTING_ACTION_CLOSE, 0, 0);
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_RESUME),
|
|
MENU_ENUM_LABEL_ACHIEVEMENT_RESUME,
|
|
MENU_SETTING_ACTION_RESUME_ACHIEVEMENTS, 0, 0);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void rcheevos_populate_menu(void* data)
|
|
{
|
|
#ifdef HAVE_MENU
|
|
int i = 0;
|
|
int count = 0;
|
|
rcheevos_racheevo_t* cheevo = NULL;
|
|
menu_displaylist_info_t* info = (menu_displaylist_info_t*)data;
|
|
settings_t* settings = config_get_ptr();
|
|
bool cheevos_enable = settings->bools.cheevos_enable;
|
|
bool cheevos_hardcore_mode_enable = settings->bools.cheevos_hardcore_mode_enable;
|
|
bool cheevos_test_unofficial = settings->bools.cheevos_test_unofficial;
|
|
|
|
if ( cheevos_enable
|
|
&& cheevos_hardcore_mode_enable
|
|
&& rcheevos_locals.loaded)
|
|
{
|
|
if (rcheevos_locals.hardcore_active)
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE_MENU),
|
|
MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_MENU,
|
|
MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS, 0, 0);
|
|
else
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE_MENU),
|
|
MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_MENU,
|
|
MENU_SETTING_ACTION_RESUME_ACHIEVEMENTS, 0, 0);
|
|
}
|
|
|
|
cheevo = rcheevos_locals.patchdata.core;
|
|
for (count = rcheevos_locals.patchdata.core_count; count > 0; count--)
|
|
rcheevos_append_menu_achievement(info, i++, cheevo++);
|
|
|
|
if (cheevos_test_unofficial)
|
|
{
|
|
cheevo = rcheevos_locals.patchdata.unofficial;
|
|
for (count = rcheevos_locals.patchdata.unofficial_count; count > 0; count--)
|
|
rcheevos_append_menu_achievement(info, i++, cheevo++);
|
|
}
|
|
|
|
if (i == 0)
|
|
{
|
|
if (!rcheevos_locals.core_supports)
|
|
{
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE),
|
|
MENU_ENUM_LABEL_CANNOT_ACTIVATE_ACHIEVEMENTS_WITH_THIS_CORE,
|
|
FILE_TYPE_NONE, 0, 0);
|
|
}
|
|
else if (!settings->arrays.cheevos_token[0])
|
|
{
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_LOGGED_IN),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_NOT_LOGGED_IN),
|
|
MENU_ENUM_LABEL_NOT_LOGGED_IN,
|
|
FILE_TYPE_NONE, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
menu_entries_append_enum(info->list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_ACHIEVEMENTS_TO_DISPLAY),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_NO_ACHIEVEMENTS_TO_DISPLAY),
|
|
MENU_ENUM_LABEL_NO_ACHIEVEMENTS_TO_DISPLAY,
|
|
FILE_TYPE_NONE, 0, 0);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool rcheevos_get_description(rcheevos_ctx_desc_t* desc)
|
|
{
|
|
unsigned idx;
|
|
const rcheevos_racheevo_t* cheevo;
|
|
|
|
if (!desc)
|
|
return false;
|
|
|
|
*desc->s = 0;
|
|
|
|
if (rcheevos_locals.loaded)
|
|
{
|
|
idx = desc->idx;
|
|
|
|
if (idx < rcheevos_locals.patchdata.core_count)
|
|
cheevo = rcheevos_locals.patchdata.core + idx;
|
|
else
|
|
{
|
|
idx -= rcheevos_locals.patchdata.core_count;
|
|
|
|
if (idx < rcheevos_locals.patchdata.unofficial_count)
|
|
cheevo = rcheevos_locals.patchdata.unofficial + idx;
|
|
else
|
|
return true;
|
|
}
|
|
|
|
strlcpy(desc->s, cheevo->description, desc->len);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool rcheevos_hardcore_active(void)
|
|
{
|
|
return rcheevos_locals.hardcore_active;
|
|
}
|
|
|
|
void rcheevos_pause_hardcore(void)
|
|
{
|
|
if (rcheevos_locals.hardcore_active)
|
|
rcheevos_toggle_hardcore_paused();
|
|
}
|
|
|
|
bool rcheevos_unload(void)
|
|
{
|
|
bool running = false;
|
|
settings_t* settings = config_get_ptr();
|
|
|
|
CHEEVOS_LOCK(rcheevos_locals.task_lock);
|
|
running = rcheevos_locals.task != NULL;
|
|
CHEEVOS_UNLOCK(rcheevos_locals.task_lock);
|
|
|
|
if (running)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Asked the load thread to terminate\n");
|
|
task_queue_cancel_task(rcheevos_locals.task);
|
|
|
|
#ifdef HAVE_THREADS
|
|
do
|
|
{
|
|
CHEEVOS_LOCK(rcheevos_locals.task_lock);
|
|
running = rcheevos_locals.task != NULL;
|
|
CHEEVOS_UNLOCK(rcheevos_locals.task_lock);
|
|
} while(running);
|
|
#endif
|
|
}
|
|
|
|
if (rcheevos_locals.loaded)
|
|
{
|
|
rcheevos_free_patchdata(&rcheevos_locals.patchdata);
|
|
rcheevos_memory_destroy(&rcheevos_locals.memory);
|
|
#ifdef HAVE_MENU
|
|
cheevos_reset_menu_badges();
|
|
#endif
|
|
|
|
rcheevos_locals.loaded = false;
|
|
rcheevos_locals.hardcore_active = false;
|
|
}
|
|
|
|
rc_runtime_destroy(&rcheevos_locals.runtime);
|
|
|
|
/* if the config-level token has been cleared, we need to re-login on loading the next game */
|
|
if (!settings->arrays.cheevos_token[0])
|
|
rcheevos_locals.token[0] = '\0';
|
|
|
|
return true;
|
|
}
|
|
|
|
static void rcheevos_toggle_hardcore_achievements(rcheevos_locals_t *locals,
|
|
rcheevos_racheevo_t* cheevo, unsigned count)
|
|
{
|
|
const unsigned active_mask = RCHEEVOS_ACTIVE_SOFTCORE | RCHEEVOS_ACTIVE_HARDCORE;
|
|
|
|
while (count--)
|
|
{
|
|
if (cheevo->memaddr && (cheevo->active & active_mask) == RCHEEVOS_ACTIVE_HARDCORE)
|
|
{
|
|
/* player has unlocked achievement in non-hardcore but has not unlocked in hardcore; toggle state */
|
|
if (locals->hardcore_active)
|
|
{
|
|
rc_runtime_activate_achievement(&locals->runtime, cheevo->id, cheevo->memaddr, NULL, 0);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u activated: %s\n", cheevo->id, cheevo->title);
|
|
}
|
|
else
|
|
{
|
|
rc_runtime_deactivate_achievement(&locals->runtime, cheevo->id);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u deactivated: %s\n", cheevo->id, cheevo->title);
|
|
}
|
|
}
|
|
|
|
++cheevo;
|
|
}
|
|
}
|
|
|
|
static void rcheevos_activate_leaderboards(rcheevos_locals_t* locals)
|
|
{
|
|
rcheevos_ralboard_t* lboard = locals->patchdata.lboards;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < locals->patchdata.lboard_count; ++i, ++lboard)
|
|
{
|
|
if (lboard->mem)
|
|
rc_runtime_activate_lboard(&locals->runtime, lboard->id, lboard->mem, NULL, 0);
|
|
}
|
|
}
|
|
|
|
static void rcheevos_deactivate_leaderboards(rcheevos_locals_t* locals)
|
|
{
|
|
rcheevos_ralboard_t* lboard = locals->patchdata.lboards;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < locals->patchdata.lboard_count; ++i, ++lboard)
|
|
{
|
|
if (lboard->mem)
|
|
{
|
|
rc_runtime_deactivate_lboard(&locals->runtime, lboard->id);
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
/* Hide any visible trackers */
|
|
gfx_widgets_set_leaderboard_display(lboard->id, NULL);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
void rcheevos_leaderboards_enabled_changed(void)
|
|
{
|
|
const settings_t* settings = config_get_ptr();
|
|
const bool leaderboards_enabled = rcheevos_locals.leaderboards_enabled;
|
|
const bool leaderboard_trackers = rcheevos_locals.leaderboard_trackers;
|
|
|
|
rcheevos_locals.leaderboards_enabled = rcheevos_hardcore_active();
|
|
|
|
if (string_is_equal(settings->arrays.cheevos_leaderboards_enable, "true"))
|
|
{
|
|
rcheevos_locals.leaderboard_notifications = true;
|
|
rcheevos_locals.leaderboard_trackers = true;
|
|
}
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
else if (string_is_equal(settings->arrays.cheevos_leaderboards_enable, "trackers"))
|
|
{
|
|
rcheevos_locals.leaderboard_notifications = false;
|
|
rcheevos_locals.leaderboard_trackers = true;
|
|
}
|
|
else if (string_is_equal(settings->arrays.cheevos_leaderboards_enable, "notifications"))
|
|
{
|
|
rcheevos_locals.leaderboard_notifications = true;
|
|
rcheevos_locals.leaderboard_trackers = false;
|
|
}
|
|
#endif
|
|
else
|
|
{
|
|
rcheevos_locals.leaderboards_enabled = false;
|
|
rcheevos_locals.leaderboard_notifications = false;
|
|
rcheevos_locals.leaderboard_trackers = false;
|
|
}
|
|
|
|
if (rcheevos_locals.loaded)
|
|
{
|
|
if (leaderboards_enabled != rcheevos_locals.leaderboards_enabled)
|
|
{
|
|
if (rcheevos_locals.leaderboards_enabled)
|
|
rcheevos_activate_leaderboards(&rcheevos_locals);
|
|
else
|
|
rcheevos_deactivate_leaderboards(&rcheevos_locals);
|
|
}
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
if (!rcheevos_locals.leaderboard_trackers && leaderboard_trackers)
|
|
{
|
|
/* Hide any visible trackers */
|
|
rcheevos_ralboard_t* lboard = rcheevos_locals.patchdata.lboards;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < rcheevos_locals.patchdata.lboard_count; ++i, ++lboard)
|
|
{
|
|
if (lboard->mem)
|
|
gfx_widgets_set_leaderboard_display(lboard->id, NULL);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static void rcheevos_toggle_hardcore_active(rcheevos_locals_t* locals)
|
|
{
|
|
settings_t* settings = config_get_ptr();
|
|
bool rewind_enable = settings->bools.rewind_enable;
|
|
|
|
if (!locals->hardcore_active)
|
|
{
|
|
/* activate hardcore */
|
|
locals->hardcore_active = true;
|
|
|
|
/* if one or more invalid settings is enabled, abort*/
|
|
rcheevos_validate_config_settings();
|
|
if (!locals->hardcore_active)
|
|
return;
|
|
|
|
#ifdef HAVE_CHEATS
|
|
/* if one or more emulator managed cheats is active, abort */
|
|
cheat_manager_apply_cheats();
|
|
if (!locals->hardcore_active)
|
|
return;
|
|
#endif
|
|
|
|
if (locals->loaded)
|
|
{
|
|
const char* msg = msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_ENABLE);
|
|
CHEEVOS_LOG("%s\n", msg);
|
|
runloop_msg_queue_push(msg, 0, 3 * 60, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
/* reactivate leaderboards */
|
|
if (locals->leaderboards_enabled)
|
|
rcheevos_activate_leaderboards(locals);
|
|
|
|
/* reset the game */
|
|
command_event(CMD_EVENT_RESET, NULL);
|
|
}
|
|
|
|
/* deinit rewind */
|
|
if (rewind_enable)
|
|
command_event(CMD_EVENT_REWIND_DEINIT, NULL);
|
|
}
|
|
else
|
|
{
|
|
/* pause hardcore */
|
|
locals->hardcore_active = false;
|
|
|
|
if (locals->loaded)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Hardcore paused\n");
|
|
|
|
/* deactivate leaderboards */
|
|
rcheevos_deactivate_leaderboards(locals);
|
|
}
|
|
|
|
/* re-init rewind */
|
|
if (rewind_enable)
|
|
command_event(CMD_EVENT_REWIND_INIT, NULL);
|
|
}
|
|
|
|
if (locals->loaded)
|
|
{
|
|
rcheevos_toggle_hardcore_achievements(locals, locals->patchdata.core, locals->patchdata.core_count);
|
|
if (settings->bools.cheevos_test_unofficial)
|
|
{
|
|
rcheevos_toggle_hardcore_achievements(locals,
|
|
locals->patchdata.unofficial, locals->patchdata.unofficial_count);
|
|
}
|
|
}
|
|
}
|
|
|
|
void rcheevos_toggle_hardcore_paused(void)
|
|
{
|
|
settings_t* settings = config_get_ptr();
|
|
|
|
/* if hardcore mode is not enabled, we can't toggle it */
|
|
if (settings->bools.cheevos_hardcore_mode_enable)
|
|
rcheevos_toggle_hardcore_active(&rcheevos_locals);
|
|
}
|
|
|
|
void rcheevos_hardcore_enabled_changed(void)
|
|
{
|
|
const settings_t* settings = config_get_ptr();
|
|
const bool enabled = settings && settings->bools.cheevos_enable && settings->bools.cheevos_hardcore_mode_enable;
|
|
|
|
if (enabled != rcheevos_locals.hardcore_active)
|
|
{
|
|
rcheevos_toggle_hardcore_active(&rcheevos_locals);
|
|
|
|
/* update leaderboard state flags */
|
|
rcheevos_leaderboards_enabled_changed();
|
|
}
|
|
}
|
|
|
|
typedef struct rc_disallowed_setting_t
|
|
{
|
|
const char* setting;
|
|
const char* value;
|
|
} rc_disallowed_setting_t;
|
|
|
|
typedef struct rc_disallowed_core_settings_t
|
|
{
|
|
const char* library_name;
|
|
const rc_disallowed_setting_t* disallowed_settings;
|
|
} rc_disallowed_core_settings_t;
|
|
|
|
static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = {
|
|
{ "dolphin_cheats_enabled", "enabled" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = {
|
|
{ "ecwolf-invulnerability", "enabled" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = {
|
|
{ "fbneo-allow-patched-romsets", "enabled" },
|
|
{ "fbneo-cheat-*", "!,Disabled,0 - Disabled" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = {
|
|
{ "genesis_plus_gx_lock_on", ",action replay (pro),game genie" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = {
|
|
{ "ppsspp_cheats", "enabled" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = {
|
|
{ "dolphin-emu", _rc_disallowed_dolphin_settings },
|
|
{ "ecwolf", _rc_disallowed_ecwolf_settings },
|
|
{ "FinalBurn Neo", _rc_disallowed_fbneo_settings },
|
|
{ "Genesis Plus GX", _rc_disallowed_gpgx_settings },
|
|
{ "PPSSPP", _rc_disallowed_ppsspp_settings },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static int rcheevos_match_value(const char* val, const char* match)
|
|
{
|
|
/* if value starts with a comma, it's a CSV list of potential matches */
|
|
if (*match == ',')
|
|
{
|
|
do
|
|
{
|
|
int size;
|
|
const char* ptr = ++match;
|
|
|
|
while (*match && *match != ',')
|
|
++match;
|
|
|
|
size = match - ptr;
|
|
if (val[size] == '\0')
|
|
{
|
|
char buffer[128];
|
|
if (string_is_equal_fast(ptr, val, size))
|
|
return true;
|
|
|
|
memcpy(buffer, ptr, size);
|
|
buffer[size] = '\0';
|
|
if (string_is_equal_case_insensitive(buffer, val))
|
|
return true;
|
|
}
|
|
} while(*match == ',');
|
|
|
|
return false;
|
|
}
|
|
|
|
/* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */
|
|
if (*match == '!')
|
|
return !rcheevos_match_value(val, &match[1]);
|
|
|
|
/* just a single value, attempt to match it */
|
|
return string_is_equal_case_insensitive(val, match);
|
|
}
|
|
|
|
void rcheevos_validate_config_settings(void)
|
|
{
|
|
const rc_disallowed_core_settings_t
|
|
*core_filter = rc_disallowed_core_settings;
|
|
struct retro_system_info* system = runloop_get_libretro_system_info();
|
|
if (!system->library_name || !rcheevos_hardcore_active())
|
|
return;
|
|
|
|
while (core_filter->library_name)
|
|
{
|
|
if (string_is_equal(core_filter->library_name, system->library_name))
|
|
{
|
|
core_option_manager_t* coreopts = NULL;
|
|
|
|
if (rarch_ctl(RARCH_CTL_CORE_OPTIONS_LIST_GET, &coreopts))
|
|
{
|
|
int i;
|
|
const char *key = NULL;
|
|
const char *val = NULL;
|
|
const rc_disallowed_setting_t
|
|
*disallowed_setting = core_filter->disallowed_settings;
|
|
int allowed = 1;
|
|
|
|
for (; disallowed_setting->setting; ++disallowed_setting)
|
|
{
|
|
size_t key_len;
|
|
key = disallowed_setting->setting;
|
|
key_len = strlen(key);
|
|
|
|
if (key[key_len - 1] == '*')
|
|
{
|
|
for (i = 0; i < coreopts->size; i++)
|
|
{
|
|
if (string_starts_with_size(
|
|
coreopts->opts[i].key, key, key_len - 1))
|
|
{
|
|
const char* val =core_option_manager_get_val(coreopts, i);
|
|
if (val)
|
|
{
|
|
if (rcheevos_match_value(
|
|
val, disallowed_setting->value))
|
|
{
|
|
key = coreopts->opts[i].key;
|
|
allowed = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (i = 0; i < coreopts->size; i++)
|
|
{
|
|
if (string_is_equal(coreopts->opts[i].key, key))
|
|
{
|
|
val = core_option_manager_get_val(coreopts, i);
|
|
if (rcheevos_match_value(val, disallowed_setting->value))
|
|
{
|
|
allowed = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!allowed)
|
|
{
|
|
char buffer[256];
|
|
snprintf(buffer, sizeof(buffer), "Hardcore paused. Setting not allowed: %s=%s", key, val);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", buffer);
|
|
rcheevos_pause_hardcore();
|
|
|
|
runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
++core_filter;
|
|
}
|
|
}
|
|
|
|
static void rcheevos_runtime_event_handler(const rc_runtime_event_t* runtime_event)
|
|
{
|
|
switch (runtime_event->type)
|
|
{
|
|
case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED:
|
|
rcheevos_award_achievement(&rcheevos_locals, rcheevos_find_cheevo(runtime_event->id));
|
|
break;
|
|
|
|
case RC_RUNTIME_EVENT_LBOARD_STARTED:
|
|
rcheevos_lboard_started(rcheevos_find_lboard(runtime_event->id), runtime_event->value);
|
|
break;
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
case RC_RUNTIME_EVENT_LBOARD_UPDATED:
|
|
rcheevos_lboard_updated(rcheevos_find_lboard(runtime_event->id), runtime_event->value);
|
|
break;
|
|
#endif
|
|
|
|
case RC_RUNTIME_EVENT_LBOARD_CANCELED:
|
|
rcheevos_lboard_canceled(rcheevos_find_lboard(runtime_event->id));
|
|
break;
|
|
|
|
case RC_RUNTIME_EVENT_LBOARD_TRIGGERED:
|
|
rcheevos_lboard_submit(&rcheevos_locals, rcheevos_find_lboard(runtime_event->id), runtime_event->value);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Test all the achievements (call once per frame).
|
|
*****************************************************************************/
|
|
void rcheevos_test(void)
|
|
{
|
|
settings_t* settings;
|
|
|
|
if (!rcheevos_locals.loaded)
|
|
return;
|
|
|
|
settings = config_get_ptr();
|
|
|
|
if (rcheevos_locals.memory.count == 0)
|
|
{
|
|
/* we were unable to initialize memory earlier, try now */
|
|
if (!rcheevos_memory_init(&rcheevos_locals.memory, rcheevos_locals.patchdata.console_id))
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "No memory exposed by core\n");
|
|
rcheevos_locals.core_supports = false;
|
|
|
|
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_do_frame(&rcheevos_locals.runtime, &rcheevos_runtime_event_handler, rcheevos_peek, NULL, 0);
|
|
}
|
|
|
|
void rcheevos_set_support_cheevos(bool state)
|
|
{
|
|
rcheevos_locals.core_supports = state;
|
|
}
|
|
|
|
bool rcheevos_get_support_cheevos(void)
|
|
{
|
|
return rcheevos_locals.core_supports;
|
|
}
|
|
|
|
const char* rcheevos_get_hash(void)
|
|
{
|
|
return rcheevos_locals.hash;
|
|
}
|
|
|
|
static void rcheevos_unlock_cb(unsigned id, void* userdata)
|
|
{
|
|
rcheevos_racheevo_t* cheevo = rcheevos_find_cheevo(id);
|
|
if (cheevo)
|
|
{
|
|
unsigned mode = *(unsigned*)userdata;
|
|
#ifndef CHEEVOS_DONT_DEACTIVATE
|
|
cheevo->active &= ~mode;
|
|
#endif
|
|
|
|
if ((rcheevos_locals.hardcore_active && mode == RCHEEVOS_ACTIVE_HARDCORE) ||
|
|
(!rcheevos_locals.hardcore_active && mode == RCHEEVOS_ACTIVE_SOFTCORE))
|
|
{
|
|
rc_runtime_deactivate_achievement(&rcheevos_locals.runtime, cheevo->id);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u deactivated: %s\n", id, cheevo->title);
|
|
}
|
|
}
|
|
}
|
|
|
|
#include "coro.h"
|
|
|
|
/* Uncomment the following two lines to debug rcheevos_iterate, this will
|
|
* disable the coroutine yielding.
|
|
*
|
|
* The code is very easy to understand. It's meant to be like BASIC:
|
|
* CORO_GOTO will jump execution to another label, CORO_GOSUB will
|
|
* call another label, and CORO_RET will return from a CORO_GOSUB.
|
|
*
|
|
* This coroutine code is inspired in a very old pure C implementation
|
|
* that runs everywhere:
|
|
*
|
|
* https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
|
|
*/
|
|
/*#undef CORO_YIELD
|
|
#define CORO_YIELD()*/
|
|
|
|
typedef struct
|
|
{
|
|
/* variables used in the co-routine */
|
|
char badge_name[16];
|
|
char url[256];
|
|
char badge_basepath[PATH_MAX_LENGTH];
|
|
char badge_fullpath[PATH_MAX_LENGTH];
|
|
char hash[33];
|
|
unsigned gameid;
|
|
unsigned i;
|
|
unsigned j;
|
|
unsigned k;
|
|
size_t len;
|
|
retro_time_t t0;
|
|
void *data;
|
|
char *json;
|
|
const char *path;
|
|
rcheevos_racheevo_t *cheevo;
|
|
const rcheevos_racheevo_t *cheevo_end;
|
|
settings_t *settings;
|
|
struct http_connection_t *conn;
|
|
struct http_t *http;
|
|
struct rc_hash_iterator iterator;
|
|
|
|
/* co-routine required fields */
|
|
CORO_FIELDS
|
|
} rcheevos_coro_t;
|
|
|
|
enum
|
|
{
|
|
/* Negative values because CORO_SUB generates positive values */
|
|
RCHEEVOS_GET_GAMEID = -1,
|
|
RCHEEVOS_GET_CHEEVOS = -2,
|
|
RCHEEVOS_GET_BADGES = -3,
|
|
RCHEEVOS_LOGIN = -4,
|
|
RCHEEVOS_HTTP_GET = -5,
|
|
RCHEEVOS_DEACTIVATE = -6,
|
|
RCHEEVOS_PLAYING = -7,
|
|
RCHEEVOS_DELAY = -8
|
|
};
|
|
|
|
static int rcheevos_iterate(rcheevos_coro_t* coro)
|
|
{
|
|
char buffer[2048];
|
|
#ifdef CHEEVOS_TIME_HASH
|
|
retro_time_t start;
|
|
#endif
|
|
|
|
CORO_ENTER();
|
|
|
|
coro->settings = config_get_ptr();
|
|
|
|
/* Bail out if cheevos are disabled.
|
|
* But set the above anyways,
|
|
* command_read_ram needs it. */
|
|
if (!coro->settings->bools.cheevos_enable)
|
|
CORO_STOP();
|
|
|
|
/* iterate over the possible hashes for the file being loaded */
|
|
rc_hash_initialize_iterator(&coro->iterator, coro->path, (uint8_t*)coro->data, coro->len);
|
|
#ifdef CHEEVOS_TIME_HASH
|
|
start = cpu_features_get_time_usec();
|
|
#endif
|
|
while (rc_hash_iterate(coro->hash, &coro->iterator))
|
|
{
|
|
#ifdef CHEEVOS_TIME_HASH
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "hash generated in %ums\n", (cpu_features_get_time_usec() - start) / 1000);
|
|
#endif
|
|
CORO_GOSUB(RCHEEVOS_GET_GAMEID);
|
|
if (coro->gameid != 0)
|
|
break;
|
|
|
|
#ifdef CHEEVOS_TIME_HASH
|
|
start = cpu_features_get_time_usec();
|
|
#endif
|
|
}
|
|
rc_hash_destroy_iterator(&coro->iterator);
|
|
|
|
/* if no match was found, bail */
|
|
if (coro->gameid == 0)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "this game doesn't feature achievements\n");
|
|
strlcpy(rcheevos_locals.hash, "N/A", sizeof(rcheevos_locals.hash));
|
|
rcheevos_pause_hardcore();
|
|
CORO_STOP();
|
|
}
|
|
|
|
#ifdef CHEEVOS_JSON_OVERRIDE
|
|
{
|
|
size_t size = 0;
|
|
FILE *file = fopen(CHEEVOS_JSON_OVERRIDE, "rb");
|
|
|
|
fseek(file, 0, SEEK_END);
|
|
size = ftell(file);
|
|
fseek(file, 0, SEEK_SET);
|
|
|
|
coro->json = (char*)malloc(size + 1);
|
|
fread((void*)coro->json, 1, size, file);
|
|
|
|
fclose(file);
|
|
coro->json[size] = 0;
|
|
}
|
|
#else
|
|
CORO_GOSUB(RCHEEVOS_GET_CHEEVOS);
|
|
|
|
if (!coro->json)
|
|
{
|
|
runloop_msg_queue_push("Error loading achievements.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "error loading achievements\n");
|
|
CORO_STOP();
|
|
}
|
|
#endif
|
|
|
|
#ifdef CHEEVOS_SAVE_JSON
|
|
{
|
|
FILE *file = fopen(CHEEVOS_SAVE_JSON, "w");
|
|
fwrite((void*)coro->json, 1, strlen(coro->json), file);
|
|
fclose(file);
|
|
}
|
|
#endif
|
|
if (rcheevos_parse(&rcheevos_locals, coro->json))
|
|
{
|
|
CHEEVOS_FREE(coro->json);
|
|
CORO_STOP();
|
|
}
|
|
|
|
CHEEVOS_FREE(coro->json);
|
|
|
|
if ( rcheevos_locals.patchdata.core_count == 0
|
|
&& rcheevos_locals.patchdata.unofficial_count == 0
|
|
&& rcheevos_locals.patchdata.lboard_count == 0)
|
|
{
|
|
runloop_msg_queue_push(
|
|
"This game has no achievements.",
|
|
0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
rcheevos_pause_hardcore();
|
|
CORO_STOP();
|
|
}
|
|
|
|
rcheevos_locals.loaded = true;
|
|
|
|
/*
|
|
* Inputs: CHEEVOS_VAR_GAMEID
|
|
* Outputs:
|
|
*/
|
|
if (!coro->settings->bools.cheevos_start_active)
|
|
CORO_GOSUB(RCHEEVOS_DEACTIVATE);
|
|
|
|
/*
|
|
* Inputs: CHEEVOS_VAR_GAMEID
|
|
* Outputs:
|
|
*/
|
|
CORO_GOSUB(RCHEEVOS_PLAYING);
|
|
|
|
if (coro->settings->bools.cheevos_verbose_enable && rcheevos_locals.patchdata.core_count > 0)
|
|
{
|
|
char msg[256];
|
|
int mode = RCHEEVOS_ACTIVE_SOFTCORE;
|
|
const rcheevos_racheevo_t* cheevo = rcheevos_locals.patchdata.core;
|
|
const rcheevos_racheevo_t* end = cheevo + rcheevos_locals.patchdata.core_count;
|
|
int number_of_unlocked = rcheevos_locals.patchdata.core_count;
|
|
int number_of_unsupported = 0;
|
|
|
|
if (rcheevos_locals.hardcore_active)
|
|
mode = RCHEEVOS_ACTIVE_HARDCORE;
|
|
|
|
for (; cheevo < end; cheevo++)
|
|
{
|
|
if (!cheevo->memaddr)
|
|
number_of_unsupported++;
|
|
else if (cheevo->active & mode)
|
|
number_of_unlocked--;
|
|
}
|
|
|
|
if (!number_of_unsupported)
|
|
{
|
|
if (coro->settings->bools.cheevos_start_active)
|
|
{
|
|
snprintf(msg, sizeof(msg),
|
|
"All %d achievements activated for this session.",
|
|
rcheevos_locals.patchdata.core_count);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg);
|
|
}
|
|
else
|
|
{
|
|
snprintf(msg, sizeof(msg),
|
|
"You have %d of %d achievements unlocked.",
|
|
number_of_unlocked, rcheevos_locals.patchdata.core_count);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", &msg[9]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (coro->settings->bools.cheevos_start_active)
|
|
{
|
|
snprintf(msg, sizeof(msg),
|
|
"All %d achievements activated for this session (%d unsupported).",
|
|
rcheevos_locals.patchdata.core_count,
|
|
number_of_unsupported);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", msg);
|
|
}
|
|
else
|
|
{
|
|
snprintf(msg, sizeof(msg),
|
|
"You have %d of %d achievements unlocked (%d unsupported).",
|
|
number_of_unlocked - number_of_unsupported,
|
|
rcheevos_locals.patchdata.core_count,
|
|
number_of_unsupported);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", &msg[9]);
|
|
}
|
|
}
|
|
|
|
msg[sizeof(msg) - 1] = 0;
|
|
runloop_msg_queue_push(msg, 0, 3 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
|
|
CORO_GOSUB(RCHEEVOS_GET_BADGES);
|
|
CORO_STOP();
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Gets the achievements from Retro Achievements
|
|
* Inputs coro->hash
|
|
* Outputs coro->gameid
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_GET_GAMEID)
|
|
|
|
{
|
|
int size;
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "checking %s\n", coro->hash);
|
|
memcpy(rcheevos_locals.hash, coro->hash, sizeof(coro->hash));
|
|
|
|
size = rc_url_get_gameid(coro->url, sizeof(coro->url), rcheevos_locals.hash);
|
|
if (size < 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n");
|
|
CORO_RET();
|
|
}
|
|
|
|
rcheevos_log_url("rc_url_get_gameid", coro->url);
|
|
CORO_GOSUB(RCHEEVOS_HTTP_GET);
|
|
|
|
if (!coro->json)
|
|
CORO_RET();
|
|
|
|
coro->gameid = chevos_get_gameid(coro->json);
|
|
|
|
CHEEVOS_FREE(coro->json);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "got game id %u\n", coro->gameid);
|
|
CORO_RET();
|
|
}
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Gets the achievements from Retro Achievements
|
|
* Inputs CHEEVOS_VAR_GAMEID
|
|
* Outputs CHEEVOS_VAR_JSON
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_GET_CHEEVOS)
|
|
{
|
|
int ret;
|
|
|
|
CORO_GOSUB(RCHEEVOS_LOGIN);
|
|
|
|
ret = rc_url_get_patch(coro->url, sizeof(coro->url), coro->settings->arrays.cheevos_username, rcheevos_locals.token, coro->gameid);
|
|
|
|
if (ret < 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n");
|
|
CORO_STOP();
|
|
}
|
|
|
|
rcheevos_log_url("rc_url_get_patch", coro->url);
|
|
CORO_GOSUB(RCHEEVOS_HTTP_GET);
|
|
|
|
if (!coro->json)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "error getting achievements for game id %u\n", coro->gameid);
|
|
CORO_STOP();
|
|
}
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "got achievements for game id %u\n", coro->gameid);
|
|
CORO_RET();
|
|
}
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Gets the achievements from Retro Achievements
|
|
* Inputs CHEEVOS_VAR_GAMEID
|
|
* Outputs CHEEVOS_VAR_JSON
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_GET_BADGES)
|
|
|
|
/* we always want badges if display widgets are enabled */
|
|
#if !defined(HAVE_GFX_WIDGETS)
|
|
{
|
|
settings_t *settings = config_get_ptr();
|
|
if (!(
|
|
string_is_equal(settings->arrays.menu_driver, "xmb") ||
|
|
string_is_equal(settings->arrays.menu_driver, "ozone")
|
|
) ||
|
|
!settings->bools.cheevos_badges_enable)
|
|
CORO_RET();
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_MENU
|
|
cheevos_reset_menu_badges();
|
|
#endif
|
|
|
|
for (coro->i = 0; coro->i < 2; coro->i++)
|
|
{
|
|
if (coro->i == 0)
|
|
{
|
|
coro->cheevo = rcheevos_locals.patchdata.core;
|
|
coro->cheevo_end = coro->cheevo + rcheevos_locals.patchdata.core_count;
|
|
}
|
|
else
|
|
{
|
|
coro->cheevo = rcheevos_locals.patchdata.unofficial;
|
|
coro->cheevo_end = coro->cheevo + rcheevos_locals.patchdata.unofficial_count;
|
|
}
|
|
|
|
for (; coro->cheevo < coro->cheevo_end; coro->cheevo++)
|
|
{
|
|
if (!coro->cheevo->badge[0])
|
|
continue;
|
|
|
|
for (coro->j = 0 ; coro->j < 2; coro->j++)
|
|
{
|
|
coro->badge_fullpath[0] = '\0';
|
|
fill_pathname_application_special(
|
|
coro->badge_fullpath,
|
|
sizeof(coro->badge_fullpath),
|
|
APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_CHEEVOS_BADGES);
|
|
|
|
if (!path_is_directory(coro->badge_fullpath))
|
|
path_mkdir(coro->badge_fullpath);
|
|
CORO_YIELD();
|
|
|
|
if (!coro->cheevo->badge || !coro->cheevo->badge[0])
|
|
continue;
|
|
|
|
if (coro->j == 0)
|
|
snprintf(coro->badge_name,
|
|
sizeof(coro->badge_name),
|
|
"%s" FILE_PATH_PNG_EXTENSION,
|
|
coro->cheevo->badge);
|
|
else
|
|
snprintf(coro->badge_name,
|
|
sizeof(coro->badge_name),
|
|
"%s_lock" FILE_PATH_PNG_EXTENSION,
|
|
coro->cheevo->badge);
|
|
|
|
fill_pathname_join(
|
|
coro->badge_fullpath,
|
|
coro->badge_fullpath,
|
|
coro->badge_name,
|
|
sizeof(coro->badge_fullpath));
|
|
|
|
if (!path_is_valid(coro->badge_fullpath))
|
|
{
|
|
#ifdef CHEEVOS_LOG_BADGES
|
|
CHEEVOS_LOG(
|
|
RCHEEVOS_TAG "downloading badge %s\n",
|
|
coro->badge_fullpath);
|
|
#endif
|
|
snprintf(coro->url,
|
|
sizeof(coro->url),
|
|
FILE_PATH_RETROACHIEVEMENTS_URL "/Badge/%s",
|
|
coro->badge_name);
|
|
|
|
CORO_GOSUB(RCHEEVOS_HTTP_GET);
|
|
|
|
if (coro->json)
|
|
{
|
|
if (!filestream_write_file(coro->badge_fullpath,
|
|
coro->json, coro->k))
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "Error writing badge %s\n", coro->badge_fullpath);
|
|
else
|
|
{
|
|
CHEEVOS_FREE(coro->json);
|
|
coro->json = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CORO_RET();
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Logs in the user at Retro Achievements
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_LOGIN)
|
|
{
|
|
int ret;
|
|
char tok[256];
|
|
|
|
if (rcheevos_locals.token[0])
|
|
CORO_RET();
|
|
|
|
if (string_is_empty(coro->settings->arrays.cheevos_username))
|
|
{
|
|
runloop_msg_queue_push(
|
|
"Missing RetroAchievements account information.",
|
|
0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
runloop_msg_queue_push(
|
|
"Please fill in your account information in Settings.",
|
|
0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "login info not informed\n");
|
|
CORO_STOP();
|
|
}
|
|
|
|
if (string_is_empty(coro->settings->arrays.cheevos_token))
|
|
{
|
|
ret = rc_url_login_with_password(coro->url, sizeof(coro->url),
|
|
coro->settings->arrays.cheevos_username,
|
|
coro->settings->arrays.cheevos_password);
|
|
|
|
if (ret == RC_OK)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "attempting to login %s (with password)\n", coro->settings->arrays.cheevos_username);
|
|
rcheevos_log_url("rc_url_login_with_password", coro->url);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ret = rc_url_login_with_token(coro->url, sizeof(coro->url),
|
|
coro->settings->arrays.cheevos_username,
|
|
coro->settings->arrays.cheevos_token);
|
|
|
|
if (ret == RC_OK)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "attempting to login %s (with token)\n", coro->settings->arrays.cheevos_username);
|
|
rcheevos_log_url("rc_url_login_with_token", coro->url);
|
|
}
|
|
}
|
|
|
|
if (ret < 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n");
|
|
CORO_STOP();
|
|
}
|
|
|
|
CORO_GOSUB(RCHEEVOS_HTTP_GET);
|
|
|
|
if (!coro->json)
|
|
{
|
|
runloop_msg_queue_push("RetroAchievements: Error contacting server.", 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "error getting user token\n");
|
|
|
|
CORO_STOP();
|
|
}
|
|
|
|
ret = rcheevos_get_token(coro->json, tok, sizeof(tok));
|
|
|
|
if (ret != 0)
|
|
{
|
|
char msg[512];
|
|
snprintf(msg, sizeof(msg),
|
|
"RetroAchievements: %s",
|
|
tok);
|
|
runloop_msg_queue_push(msg, 0, 5 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
*coro->settings->arrays.cheevos_token = 0;
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "login error: %s\n", tok);
|
|
|
|
CHEEVOS_FREE(coro->json);
|
|
CORO_STOP();
|
|
}
|
|
|
|
CHEEVOS_FREE(coro->json);
|
|
|
|
if (coro->settings->bools.cheevos_verbose_enable)
|
|
{
|
|
char msg[256];
|
|
snprintf(msg, sizeof(msg),
|
|
"RetroAchievements: Logged in as \"%s\".",
|
|
coro->settings->arrays.cheevos_username);
|
|
msg[sizeof(msg) - 1] = 0;
|
|
runloop_msg_queue_push(msg, 0, 2 * 60, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "logged in successfully\n");
|
|
strlcpy(rcheevos_locals.token, tok,
|
|
sizeof(rcheevos_locals.token));
|
|
|
|
/* Save token to config and clear pass on success */
|
|
strlcpy(coro->settings->arrays.cheevos_token, tok,
|
|
sizeof(coro->settings->arrays.cheevos_token));
|
|
|
|
*coro->settings->arrays.cheevos_password = 0;
|
|
CORO_RET();
|
|
}
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Pauses execution for five seconds
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_DELAY)
|
|
|
|
{
|
|
retro_time_t t1;
|
|
coro->t0 = cpu_features_get_time_usec();
|
|
|
|
do
|
|
{
|
|
CORO_YIELD();
|
|
t1 = cpu_features_get_time_usec();
|
|
} while((t1 - coro->t0) < 3000000);
|
|
}
|
|
|
|
CORO_RET();
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Makes a HTTP GET request
|
|
* Inputs CHEEVOS_VAR_URL
|
|
* Outputs CHEEVOS_VAR_JSON
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_HTTP_GET)
|
|
|
|
for (coro->k = 0; coro->k < 5; coro->k++)
|
|
{
|
|
if (coro->k != 0)
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Retrying HTTP request: %u of 5\n", coro->k + 1);
|
|
|
|
coro->json = NULL;
|
|
coro->conn = net_http_connection_new(
|
|
coro->url, "GET", NULL);
|
|
|
|
if (!coro->conn)
|
|
{
|
|
CORO_GOSUB(RCHEEVOS_DELAY);
|
|
continue;
|
|
}
|
|
|
|
/* Don't bother with timeouts here, it's just a string scan. */
|
|
while (!net_http_connection_iterate(coro->conn)) {}
|
|
|
|
/* Error finishing the connection descriptor. */
|
|
if (!net_http_connection_done(coro->conn))
|
|
{
|
|
net_http_connection_free(coro->conn);
|
|
continue;
|
|
}
|
|
|
|
rcheevos_get_user_agent(&rcheevos_locals,
|
|
buffer, sizeof(buffer));
|
|
net_http_connection_set_user_agent(coro->conn, buffer);
|
|
|
|
coro->http = net_http_new(coro->conn);
|
|
|
|
/* Error connecting to the endpoint. */
|
|
if (!coro->http)
|
|
{
|
|
net_http_connection_free(coro->conn);
|
|
CORO_GOSUB(RCHEEVOS_DELAY);
|
|
continue;
|
|
}
|
|
|
|
while (!net_http_update(coro->http, NULL, NULL))
|
|
CORO_YIELD();
|
|
|
|
{
|
|
size_t length;
|
|
uint8_t *data = net_http_data(coro->http,
|
|
&length, false);
|
|
|
|
if (data)
|
|
{
|
|
coro->json = (char*)malloc(length + 1);
|
|
|
|
if (coro->json)
|
|
{
|
|
memcpy((void*)coro->json, (void*)data, length);
|
|
CHEEVOS_FREE(data);
|
|
coro->json[length] = 0;
|
|
}
|
|
|
|
coro->k = (unsigned)length;
|
|
net_http_delete(coro->http);
|
|
net_http_connection_free(coro->conn);
|
|
CORO_RET();
|
|
}
|
|
}
|
|
|
|
net_http_delete(coro->http);
|
|
net_http_connection_free(coro->conn);
|
|
}
|
|
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Couldn't connect to server after 5 tries\n");
|
|
CORO_RET();
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Deactivates the achievements already awarded
|
|
* Inputs CHEEVOS_VAR_GAMEID
|
|
* Outputs
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_DEACTIVATE)
|
|
|
|
CORO_GOSUB(RCHEEVOS_LOGIN);
|
|
{
|
|
int ret;
|
|
unsigned mode;
|
|
|
|
/* Two calls - one for softcore and one for hardcore */
|
|
for (coro->i = 0; coro->i < 2; coro->i++)
|
|
{
|
|
ret = rc_url_get_unlock_list(coro->url, sizeof(coro->url),
|
|
coro->settings->arrays.cheevos_username,
|
|
rcheevos_locals.token, coro->gameid, coro->i);
|
|
|
|
if (ret < 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n");
|
|
CORO_STOP();
|
|
}
|
|
|
|
rcheevos_log_url("rc_url_get_unlock_list", coro->url);
|
|
CORO_GOSUB(RCHEEVOS_HTTP_GET);
|
|
|
|
if (coro->json)
|
|
{
|
|
mode = coro->i == 0 ? RCHEEVOS_ACTIVE_SOFTCORE : RCHEEVOS_ACTIVE_HARDCORE;
|
|
rcheevos_deactivate_unlocks(coro->json, rcheevos_unlock_cb, &mode);
|
|
CHEEVOS_FREE(coro->json);
|
|
}
|
|
else
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "error retrieving list of unlocked achievements in softcore mode\n");
|
|
}
|
|
}
|
|
|
|
CORO_RET();
|
|
|
|
|
|
/**************************************************************************
|
|
* Info Posts the "playing" activity to Retro Achievements
|
|
* Inputs CHEEVOS_VAR_GAMEID
|
|
* Outputs
|
|
*************************************************************************/
|
|
CORO_SUB(RCHEEVOS_PLAYING)
|
|
|
|
{
|
|
int ret = rc_url_post_playing(coro->url, sizeof(coro->url),
|
|
coro->settings->arrays.cheevos_username,
|
|
rcheevos_locals.token, coro->gameid);
|
|
|
|
if (ret < 0)
|
|
{
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "buffer too small to create URL\n");
|
|
CORO_STOP();
|
|
}
|
|
}
|
|
|
|
rcheevos_log_url("rc_url_post_playing", coro->url);
|
|
|
|
CORO_GOSUB(RCHEEVOS_HTTP_GET);
|
|
|
|
if (coro->json)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Posted playing activity\n");
|
|
CHEEVOS_FREE(coro->json);
|
|
}
|
|
else
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "error posting playing activity\n");
|
|
|
|
CORO_RET();
|
|
|
|
CORO_LEAVE();
|
|
}
|
|
|
|
static void rcheevos_task_handler(retro_task_t *task)
|
|
{
|
|
rcheevos_coro_t *coro = (rcheevos_coro_t*)task->state;
|
|
|
|
if (!coro)
|
|
return;
|
|
|
|
if (!rcheevos_iterate(coro) || task_get_cancelled(task))
|
|
{
|
|
task_set_finished(task, true);
|
|
|
|
CHEEVOS_LOCK(rcheevos_locals.task_lock);
|
|
rcheevos_locals.task = NULL;
|
|
CHEEVOS_UNLOCK(rcheevos_locals.task_lock);
|
|
|
|
if (task_get_cancelled(task))
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Load task cancelled\n");
|
|
}
|
|
else
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Load task finished\n");
|
|
}
|
|
|
|
CHEEVOS_FREE(coro->data);
|
|
CHEEVOS_FREE(coro->path);
|
|
CHEEVOS_FREE(coro);
|
|
}
|
|
}
|
|
|
|
/* hooks for rc_hash library */
|
|
|
|
static void* rc_hash_handle_file_open(const char* path)
|
|
{
|
|
return intfstream_open_file(path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
}
|
|
|
|
static void rc_hash_handle_file_seek(void* file_handle, size_t offset, int origin)
|
|
{
|
|
intfstream_seek((intfstream_t*)file_handle, offset, origin);
|
|
}
|
|
|
|
static size_t rc_hash_handle_file_tell(void* file_handle)
|
|
{
|
|
return intfstream_tell((intfstream_t*)file_handle);
|
|
}
|
|
|
|
static size_t rc_hash_handle_file_read(void* file_handle, void* buffer, size_t requested_bytes)
|
|
{
|
|
return intfstream_read((intfstream_t*)file_handle, buffer, requested_bytes);
|
|
}
|
|
|
|
static void rc_hash_handle_file_close(void* file_handle)
|
|
{
|
|
intfstream_close((intfstream_t*)file_handle);
|
|
CHEEVOS_FREE(file_handle);
|
|
}
|
|
|
|
static void* rc_hash_handle_cd_open_track(const char* path, uint32_t track)
|
|
{
|
|
cdfs_track_t* cdfs_track;
|
|
|
|
if (track == 0)
|
|
cdfs_track = cdfs_open_data_track(path);
|
|
else
|
|
cdfs_track = cdfs_open_track(path, track);
|
|
|
|
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;
|
|
|
|
CHEEVOS_FREE(file);
|
|
}
|
|
|
|
cdfs_close_track(cdfs_track); /* ASSERT: this free()s cdfs_track */
|
|
return NULL;
|
|
}
|
|
|
|
static size_t rc_hash_handle_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes)
|
|
{
|
|
cdfs_file_t* file = (cdfs_file_t*)track_handle;
|
|
|
|
cdfs_seek_sector(file, sector);
|
|
return cdfs_read_file(file, buffer, requested_bytes);
|
|
}
|
|
|
|
static void rc_hash_handle_cd_close_track(void* track_handle)
|
|
{
|
|
cdfs_file_t* file = (cdfs_file_t*)track_handle;
|
|
if (file)
|
|
{
|
|
cdfs_close_track(file->track);
|
|
cdfs_close_file(file); /* ASSERT: this does not free() file */
|
|
CHEEVOS_FREE(file);
|
|
}
|
|
}
|
|
|
|
static void rc_hash_handle_log_message(const char* message)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message);
|
|
}
|
|
|
|
/* end hooks */
|
|
|
|
bool rcheevos_load(const void *data)
|
|
{
|
|
retro_task_t *task = NULL;
|
|
const struct retro_game_info *info = NULL;
|
|
rcheevos_coro_t *coro = NULL;
|
|
settings_t *settings = config_get_ptr();
|
|
bool cheevos_enable = settings && settings->bools.cheevos_enable;
|
|
struct rc_hash_filereader filereader;
|
|
struct rc_hash_cdreader cdreader;
|
|
|
|
rcheevos_locals.loaded = false;
|
|
rc_runtime_init(&rcheevos_locals.runtime);
|
|
|
|
if (!cheevos_enable || !rcheevos_locals.core_supports || !data)
|
|
{
|
|
rcheevos_pause_hardcore();
|
|
return false;
|
|
}
|
|
|
|
/* reset hardcore mode and leaderboard settings based on configs */
|
|
rcheevos_hardcore_enabled_changed();
|
|
rcheevos_validate_config_settings();
|
|
rcheevos_leaderboards_enabled_changed();
|
|
|
|
coro = (rcheevos_coro_t*)calloc(1, sizeof(*coro));
|
|
|
|
if (!coro)
|
|
return false;
|
|
|
|
/* provide hooks for reading files */
|
|
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);
|
|
|
|
cdreader.open_track = rc_hash_handle_cd_open_track;
|
|
cdreader.read_sector = rc_hash_handle_cd_read_sector;
|
|
cdreader.close_track = rc_hash_handle_cd_close_track;
|
|
rc_hash_init_custom_cdreader(&cdreader);
|
|
|
|
rc_hash_init_error_message_callback(rc_hash_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(rc_hash_handle_log_message);
|
|
}
|
|
|
|
task = task_init();
|
|
if (!task)
|
|
{
|
|
CHEEVOS_FREE(coro);
|
|
return false;
|
|
}
|
|
|
|
CORO_SETUP();
|
|
|
|
info = (const struct retro_game_info*)data;
|
|
coro->path = strdup(info->path);
|
|
|
|
if (info->data)
|
|
{
|
|
coro->len = info->size;
|
|
|
|
/* size limit */
|
|
if (coro->len > CHEEVOS_MB(64))
|
|
coro->len = CHEEVOS_MB(64);
|
|
|
|
coro->data = malloc(coro->len);
|
|
|
|
if (!coro->data)
|
|
{
|
|
CHEEVOS_FREE(task);
|
|
CHEEVOS_FREE(coro);
|
|
return false;
|
|
}
|
|
|
|
memcpy(coro->data, info->data, coro->len);
|
|
}
|
|
else
|
|
{
|
|
coro->data = NULL;
|
|
}
|
|
|
|
task->handler = rcheevos_task_handler;
|
|
task->state = (void*)coro;
|
|
task->mute = true;
|
|
task->callback = NULL;
|
|
task->user_data = NULL;
|
|
task->progress = 0;
|
|
task->title = NULL;
|
|
|
|
#ifdef HAVE_THREADS
|
|
if (!rcheevos_locals.task_lock)
|
|
rcheevos_locals.task_lock = slock_new();
|
|
#endif
|
|
|
|
CHEEVOS_LOCK(rcheevos_locals.task_lock);
|
|
rcheevos_locals.task = task;
|
|
CHEEVOS_UNLOCK(rcheevos_locals.task_lock);
|
|
|
|
task_queue_push(task);
|
|
return true;
|
|
}
|