RetroArch/cheevos/cheevos.c

3688 lines
97 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 <formats/jsonsax.h>
#include <streams/interface_stream.h>
#include <streams/file_stream.h>
#include <features/features_cpu.h>
#include <compat/strl.h>
#include <rhash.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_THREADS
#include <rthreads/rthreads.h>
#endif
#include "badges.h"
#include "cheevos.h"
#include "var.h"
#include "cond.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 "../network/net_http_special.h"
#include "../tasks/tasks_internal.h"
#include "../verbosity.h"
/* Define this macro to prevent cheevos from being deactivated. */
#undef CHEEVOS_DONT_DEACTIVATE
/* Define this macro to log URLs (will log the user token). */
#undef CHEEVOS_LOG_URLS
/* Define this macro to dump all cheevos' addresses. */
#undef CHEEVOS_DUMP_ADDRS
/* Define this macro to remove HTTP timeouts. */
#undef CHEEVOS_NO_TIMEOUT
/* 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 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
/* C89 wants only int values in enums. */
#define CHEEVOS_JSON_KEY_GAMEID 0xb4960eecU
#define CHEEVOS_JSON_KEY_ACHIEVEMENTS 0x69749ae1U
#define CHEEVOS_JSON_KEY_ID 0x005973f2U
#define CHEEVOS_JSON_KEY_MEMADDR 0x1e76b53fU
#define CHEEVOS_JSON_KEY_TITLE 0x0e2a9a07U
#define CHEEVOS_JSON_KEY_DESCRIPTION 0xe61a1f69U
#define CHEEVOS_JSON_KEY_POINTS 0xca8fce22U
#define CHEEVOS_JSON_KEY_AUTHOR 0xa804edb8U
#define CHEEVOS_JSON_KEY_MODIFIED 0xdcea4fe6U
#define CHEEVOS_JSON_KEY_CREATED 0x3a84721dU
#define CHEEVOS_JSON_KEY_BADGENAME 0x887685d9U
#define CHEEVOS_JSON_KEY_CONSOLE_ID 0x071656e5U
#define CHEEVOS_JSON_KEY_TOKEN 0x0e2dbd26U
#define CHEEVOS_JSON_KEY_FLAGS 0x0d2e96b2U
#define CHEEVOS_JSON_KEY_LEADERBOARDS 0xf1247d2dU
#define CHEEVOS_JSON_KEY_MEM 0x0b8807e4U
#define CHEEVOS_JSON_KEY_FORMAT 0xb341208eU
#define CHEEVOS_JSON_KEY_SUCCESS 0x110461deU
#define CHEEVOS_JSON_KEY_ERROR 0x0d2011cfU
typedef struct
{
cheevos_cond_t *conds;
unsigned count;
} cheevos_condset_t;
typedef struct
{
cheevos_condset_t *condsets;
unsigned count;
} cheevos_condition_t;
typedef struct
{
unsigned id;
const char *title;
const char *description;
const char *author;
const char *badge;
unsigned points;
unsigned dirty;
int active;
int last;
int modified;
cheevos_condition_t condition;
} cheevo_t;
typedef struct
{
cheevo_t *cheevos;
unsigned count;
} cheevoset_t;
typedef struct
{
int is_element;
int mode;
} cheevos_deactivate_t;
typedef struct
{
unsigned key_hash;
int is_key;
const char *value;
size_t length;
} cheevos_getvalueud_t;
typedef struct
{
int in_cheevos;
int in_lboards;
uint32_t field_hash;
unsigned core_count;
unsigned unofficial_count;
unsigned lboard_count;
} cheevos_countud_t;
typedef struct
{
const char *string;
size_t length;
} cheevos_field_t;
typedef struct
{
int in_cheevos;
int in_lboards;
int is_console_id;
unsigned core_count;
unsigned unofficial_count;
unsigned lboard_count;
cheevos_field_t *field;
cheevos_field_t id, memaddr, title, desc, points, author;
cheevos_field_t modified, created, badge, flags, format;
} cheevos_readud_t;
typedef struct
{
int label;
const char *name;
const uint32_t *ext_hashes;
} cheevos_finder_t;
typedef struct
{
cheevos_var_t var;
double multiplier;
bool compare_next;
} cheevos_term_t;
typedef struct
{
cheevos_term_t *terms;
unsigned count;
unsigned compare_count;
} cheevos_expr_t;
typedef struct
{
unsigned id;
unsigned format;
const char *title;
const char *description;
int active;
int last_value;
cheevos_condition_t start;
cheevos_condition_t cancel;
cheevos_condition_t submit;
cheevos_expr_t value;
} cheevos_leaderboard_t;
typedef struct
{
retro_task_t* task;
#ifdef HAVE_THREADS
slock_t* task_lock;
#endif
cheevos_console_t console_id;
bool core_supports;
bool addrs_patched;
int add_buffer;
int add_hits;
cheevoset_t core;
cheevoset_t unofficial;
cheevos_leaderboard_t *leaderboards;
unsigned lboard_count;
char token[32];
retro_ctx_memory_info_t meminfo[4];
} cheevos_locals_t;
typedef struct
{
uint8_t id[4]; /* NES^Z */
uint8_t rom_size;
uint8_t vrom_size;
uint8_t rom_type;
uint8_t rom_type2;
uint8_t reserve[8];
} cheevos_nes_header_t;
static cheevos_locals_t cheevos_locals =
{
/* task */ NULL,
#ifdef HAVE_THREADS
/* task_lock */ NULL,
#endif
/* console_id */ CHEEVOS_CONSOLE_NONE,
/* core_supports */ true,
/* addrs_patched */ false,
/* add_buffer */ 0,
/* add_hits */ 0,
/* core */ {NULL, 0},
/* unofficial */ {NULL, 0},
/* leaderboards */ NULL,
/* lboard_count */ 0,
/* token */ {0},
{
/* meminfo[0] */ {NULL, 0, 0},
/* meminfo[1] */ {NULL, 0, 0},
/* meminfo[2] */ {NULL, 0, 0},
/* meminfo[3] */ {NULL, 0, 0}
}
};
bool cheevos_loaded = false;
bool cheevos_hardcore_active = false;
bool cheevos_hardcore_paused = false;
int cheats_are_enabled = 0;
int cheats_were_enabled = 0;
#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
/*****************************************************************************
Supporting functions.
*****************************************************************************/
#ifndef CHEEVOS_VERBOSE
void cheevos_log(const char *fmt, ...)
{
(void)fmt;
}
#endif
static unsigned size_in_megabytes(unsigned val)
{
return (val * 1024 * 1024);
}
#ifdef CHEEVOS_LOG_URLS
static void cheevos_log_url(const char* format, const char* url)
{
#ifdef CHEEVOS_LOG_PASSWORD
CHEEVOS_LOG(format, url);
#else
char copy[256];
char* aux = NULL;
char* next = NULL;
if (!string_is_empty(url))
strlcpy(copy, url, sizeof(copy));
aux = strstr(copy, "?p=");
if (!aux)
aux = strstr(copy, "&p=");
if (aux)
{
aux += 3;
next = strchr(aux, '&');
if (next)
{
do
{
*aux++ = *next++;
}while (next[-1] != 0);
}
else
*aux = 0;
}
aux = strstr(copy, "?t=");
if (!aux)
aux = strstr(copy, "&t=");
if (aux)
{
aux += 3;
next = strchr(aux, '&');
if (next)
{
do
{
*aux++ = *next++;
}while (next[-1] != 0);
}
else
*aux = 0;
}
CHEEVOS_LOG(format, copy);
#endif
}
#endif
static uint32_t cheevos_djb2(const char* str, size_t length)
{
const unsigned char *aux = (const unsigned char*)str;
const unsigned char *end = aux + length;
uint32_t hash = 5381;
while (aux < end)
hash = (hash << 5) + hash + *aux++;
return hash;
}
static int cheevos_getvalue__json_key(void *userdata,
const char *name, size_t length)
{
cheevos_getvalueud_t* ud = (cheevos_getvalueud_t*)userdata;
if (ud)
ud->is_key = cheevos_djb2(name, length) == ud->key_hash;
return 0;
}
static int cheevos_getvalue__json_string(void *userdata,
const char *string, size_t length)
{
cheevos_getvalueud_t* ud = (cheevos_getvalueud_t*)userdata;
if (ud && ud->is_key)
{
ud->value = string;
ud->length = length;
ud->is_key = 0;
}
return 0;
}
static int cheevos_getvalue__json_boolean(void *userdata, int istrue)
{
cheevos_getvalueud_t* ud = (cheevos_getvalueud_t*)userdata;
if (ud && ud->is_key)
{
if (istrue)
{
ud->value = "true";
ud->length = 4;
}
else
{
ud->value = "false";
ud->length = 5;
}
ud->is_key = 0;
}
return 0;
}
static int cheevos_getvalue__json_null(void *userdata)
{
cheevos_getvalueud_t* ud = (cheevos_getvalueud_t*)userdata;
if (ud && ud->is_key )
{
ud->value = "null";
ud->length = 4;
ud->is_key = 0;
}
return 0;
}
static int cheevos_get_value(const char *json, unsigned key_hash,
char *value, size_t length)
{
static const jsonsax_handlers_t handlers =
{
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
cheevos_getvalue__json_key,
NULL,
cheevos_getvalue__json_string,
cheevos_getvalue__json_string, /* number */
cheevos_getvalue__json_boolean,
cheevos_getvalue__json_null
};
cheevos_getvalueud_t ud;
ud.key_hash = key_hash;
ud.is_key = 0;
ud.value = NULL;
ud.length = 0;
*value = 0;
if ((jsonsax_parse(json, &handlers, (void*)&ud) == JSONSAX_OK)
&& ud.value && ud.length < length)
{
if (!string_is_empty(ud.value))
strlcpy(value, ud.value, ud.length + 1);
return 0;
}
return -1;
}
/*****************************************************************************
Count number of achievements in a JSON file.
*****************************************************************************/
static int cheevos_count__json_end_array(void *userdata)
{
cheevos_countud_t* ud = (cheevos_countud_t*)userdata;
if (ud)
{
ud->in_cheevos = 0;
ud->in_lboards = 0;
}
return 0;
}
static int cheevos_count__json_key(void *userdata,
const char *name, size_t length)
{
cheevos_countud_t* ud = (cheevos_countud_t*)userdata;
if (ud)
{
ud->field_hash = cheevos_djb2(name, length);
if (ud->field_hash == CHEEVOS_JSON_KEY_ACHIEVEMENTS)
ud->in_cheevos = 1;
else if (ud->field_hash == CHEEVOS_JSON_KEY_LEADERBOARDS)
ud->in_lboards = 1;
}
return 0;
}
static int cheevos_count__json_number(void *userdata,
const char *number, size_t length)
{
cheevos_countud_t* ud = (cheevos_countud_t*)userdata;
if (ud)
{
if (ud->in_cheevos && ud->field_hash == CHEEVOS_JSON_KEY_FLAGS)
{
long flags = strtol(number, NULL, 10);
if (flags == 3)
ud->core_count++; /* Core achievements */
else if (flags == 5)
ud->unofficial_count++; /* Unofficial achievements */
}
else if (ud->in_lboards && ud->field_hash == CHEEVOS_JSON_KEY_ID)
ud->lboard_count++;
}
return 0;
}
static int cheevos_count_cheevos(const char *json,
unsigned *core_count, unsigned *unofficial_count,
unsigned *lboard_count)
{
static const jsonsax_handlers_t handlers =
{
NULL,
NULL,
NULL,
NULL,
NULL,
cheevos_count__json_end_array,
cheevos_count__json_key,
NULL,
NULL,
cheevos_count__json_number,
NULL,
NULL
};
int res;
cheevos_countud_t ud;
ud.in_cheevos = 0;
ud.in_lboards = 0;
ud.core_count = 0;
ud.unofficial_count = 0;
ud.lboard_count = 0;
res = jsonsax_parse(json, &handlers, (void*)&ud);
*core_count = ud.core_count;
*unofficial_count = ud.unofficial_count;
*lboard_count = ud.lboard_count;
return res;
}
/*****************************************************************************
Parse the MemAddr field.
*****************************************************************************/
static unsigned cheevos_count_cond_sets(const char *memaddr)
{
cheevos_cond_t cond;
unsigned count = 0;
for (;;)
{
count++;
for (;;)
{
cheevos_cond_parse(&cond, &memaddr);
if (*memaddr != '_')
break;
memaddr++;
}
if (*memaddr != 'S')
break;
memaddr++;
}
return count;
}
static int cheevos_parse_condition(
cheevos_condition_t *condition,
const char* memaddr)
{
if (!condition)
return 0;
condition->count = cheevos_count_cond_sets(memaddr);
if (condition->count)
{
unsigned set = 0;
const cheevos_condset_t* end = NULL;
cheevos_condset_t *conds = NULL;
cheevos_condset_t *condset = NULL;
cheevos_condset_t *condsets = (cheevos_condset_t*)
calloc(condition->count, sizeof(cheevos_condset_t));
(void)conds;
if (!condsets)
return -1;
condition->condsets = condsets;
end = condition->condsets + condition->count;
for (condset = condition->condsets; condset < end; condset++, set++)
{
condset->count =
cheevos_cond_count_in_set(memaddr, set);
condset->conds = NULL;
CHEEVOS_LOG("[CHEEVOS]: set %p (index=%u)\n", condset, set);
CHEEVOS_LOG("[CHEEVOS]: conds: %u\n", condset->count);
if (condset->count)
{
cheevos_cond_t *conds = (cheevos_cond_t*)
calloc(condset->count, sizeof(cheevos_cond_t));
if (!conds)
{
while (--condset >= condition->condsets)
{
if ((void*)condset->conds)
free((void*)condset->conds);
}
return -1;
}
condset->conds = conds;
cheevos_cond_parse_in_set(condset->conds, memaddr, set);
}
}
}
return 0;
}
static void cheevos_free_condition(cheevos_condition_t* condition)
{
unsigned i;
if (!condition)
return;
if (condition->condsets)
{
for (i = 0; i < condition->count; i++)
{
if (condition->condsets[i].conds)
{
free(condition->condsets[i].conds);
condition->condsets[i].conds = NULL;
}
}
if (condition->condsets)
{
free(condition->condsets);
condition->condsets = NULL;
}
}
}
/*****************************************************************************
Parse the Mem field of leaderboards.
*****************************************************************************/
static int cheevos_parse_expression(cheevos_expr_t *expr, const char* mem)
{
unsigned i;
const char *aux;
cheevos_term_t *terms = NULL;
char *end = NULL;
if (!expr)
return -1;
expr->count = 1;
expr->compare_count = 1;
for (aux = mem;; aux++)
{
if (*aux == '"' || *aux == ':')
break;
expr->count += *aux == '_';
}
if (expr->count > 0)
terms = (cheevos_term_t*)
calloc(expr->count, sizeof(cheevos_term_t));
if (!terms)
return -1;
expr->terms = terms;
for (i = 0; i < expr->count; i++)
{
expr->terms[i].compare_next = false;
expr->terms[i].multiplier = 1;
}
for (i = 0, aux = mem; i < expr->count;)
{
cheevos_var_parse(&expr->terms[i].var, &aux);
if (*aux != '*')
{
/* expression has no multiplier */
if (*aux == '_')
{
aux++;
i++;
}
else if (*aux == '$')
{
expr->terms[i].compare_next = true;
expr->compare_count++;
aux++;
i++;
}
/* no multiplier at end of string */
else if (*aux == '\0' || *aux == '"' || *aux == ',')
return 0;
/* invalid character in expression */
else
{
if (expr->terms)
{
free(expr->terms);
expr->terms = NULL;
}
return -1;
}
}
else
{
if (aux[1] == 'h' || aux[1] == 'H')
expr->terms[i].multiplier = (double)strtol(aux + 2, &end, 16);
else
expr->terms[i].multiplier = strtod(aux + 1, &end);
aux = end;
if (*aux == '$')
{
aux++;
expr->terms[i].compare_next = true;
expr->compare_count++;
}
else
expr->terms[i].compare_next = false;
aux++;
i++;
}
}
return 0;
}
static int cheevos_parse_mem(cheevos_leaderboard_t *lb, const char* mem)
{
lb->start.condsets = NULL;
lb->cancel.condsets = NULL;
lb->submit.condsets = NULL;
lb->value.terms = NULL;
for (;;)
{
if (*mem == 'S' && mem[1] == 'T' && mem[2] == 'A' && mem[3] == ':')
{
if (cheevos_parse_condition(&lb->start, mem + 4))
goto error;
}
else if (*mem == 'C' && mem[1] == 'A' && mem[2] == 'N' && mem[3] == ':')
{
if (cheevos_parse_condition(&lb->cancel, mem + 4))
goto error;
}
else if (*mem == 'S' && mem[1] == 'U' && mem[2] == 'B' && mem[3] == ':')
{
if (cheevos_parse_condition(&lb->submit, mem + 4))
goto error;
}
else if (*mem == 'V' && mem[1] == 'A' && mem[2] == 'L' && mem[3] == ':')
{
if (cheevos_parse_expression(&lb->value, mem + 4))
goto error;
}
for (mem += 4;; mem++)
{
if (*mem == ':' && mem[1] == ':')
{
mem += 2;
break;
}
else if (*mem == '"')
return 0;
}
}
error:
cheevos_free_condition(&lb->start);
cheevos_free_condition(&lb->cancel);
cheevos_free_condition(&lb->submit);
if (lb->value.terms)
{
free((void*)lb->value.terms);
lb->value.terms = NULL;
}
return -1;
}
/*****************************************************************************
Load achievements from a JSON string.
*****************************************************************************/
static INLINE const char *cheevos_dupstr(const cheevos_field_t *field)
{
char *string = (char*)malloc(field->length + 1);
if (!string)
return NULL;
memcpy ((void*)string, (void*)field->string, field->length);
string[field->length] = 0;
return string;
}
static int cheevos_new_cheevo(cheevos_readud_t *ud)
{
cheevo_t *cheevo = NULL;
long flags = strtol(ud->flags.string, NULL, 10);
if (flags == 3)
cheevo = cheevos_locals.core.cheevos + ud->core_count++;
else if (flags == 5)
cheevo = cheevos_locals.unofficial.cheevos + ud->unofficial_count++;
else
return 0;
cheevo->id = (unsigned)strtol(ud->id.string, NULL, 10);
cheevo->title = cheevos_dupstr(&ud->title);
cheevo->description = cheevos_dupstr(&ud->desc);
cheevo->author = cheevos_dupstr(&ud->author);
cheevo->badge = cheevos_dupstr(&ud->badge);
cheevo->points = (unsigned)strtol(ud->points.string, NULL, 10);
cheevo->dirty = 0;
cheevo->active = CHEEVOS_ACTIVE_SOFTCORE | CHEEVOS_ACTIVE_HARDCORE;
cheevo->last = 1;
cheevo->modified = 0;
if ( !cheevo->title ||
!cheevo->description ||
!cheevo->author ||
!cheevo->badge)
goto error;
if (cheevos_parse_condition(&cheevo->condition, ud->memaddr.string))
goto error;
return 0;
error:
if (cheevo->title)
{
free((void*)cheevo->title);
cheevo->title = NULL;
}
if (cheevo->description)
{
free((void*)cheevo->description);
cheevo->description = NULL;
}
if (cheevo->author)
{
free((void*)cheevo->author);
cheevo->author = NULL;
}
if (cheevo->badge)
{
free((void*)cheevo->badge);
cheevo->badge = NULL;
}
return -1;
}
/*****************************************************************************
Helper functions for displaying leaderboard values.
*****************************************************************************/
static void cheevos_format_value(const unsigned value, const unsigned type,
char* formatted_value, size_t formatted_size)
{
unsigned mins, secs, millis;
switch(type)
{
case CHEEVOS_FORMAT_VALUE:
snprintf(formatted_value, formatted_size, "%u", value);
break;
case CHEEVOS_FORMAT_SCORE:
snprintf(formatted_value, formatted_size,
"%06upts", value);
break;
case CHEEVOS_FORMAT_FRAMES:
mins = value / 3600;
secs = (value % 3600) / 60;
millis = (int) (value % 60) * (10.00 / 6.00);
snprintf(formatted_value, formatted_size,
"%02u:%02u.%02u", mins, secs, millis);
break;
case CHEEVOS_FORMAT_MILLIS:
mins = value / 6000;
secs = (value % 6000) / 100;
millis = (int) (value % 100);
snprintf(formatted_value, formatted_size,
"%02u:%02u.%02u", mins, secs, millis);
break;
case CHEEVOS_FORMAT_SECS:
mins = value / 60;
secs = value % 60;
snprintf(formatted_value, formatted_size,
"%02u:%02u", mins, secs);
break;
default:
snprintf(formatted_value, formatted_size,
"%u (?)", value);
}
}
unsigned cheevos_parse_format(cheevos_field_t* format)
{
/* Most likely */
if (strncmp(format->string, "VALUE", format->length) == 0)
return CHEEVOS_FORMAT_VALUE;
else if (strncmp(format->string, "TIME", format->length) == 0)
return CHEEVOS_FORMAT_FRAMES;
else if (strncmp(format->string, "SCORE", format->length) == 0)
return CHEEVOS_FORMAT_SCORE;
/* Less likely */
else if (strncmp(format->string, "MILLISECS", format->length) == 0)
return CHEEVOS_FORMAT_MILLIS;
else if (strncmp(format->string, "TIMESECS", format->length) == 0)
return CHEEVOS_FORMAT_SECS;
/* Rare (RPS only) */
else if (strncmp(format->string, "POINTS", format->length) == 0)
return CHEEVOS_FORMAT_SCORE;
else if (strncmp(format->string, "FRAMES", format->length) == 0)
return CHEEVOS_FORMAT_FRAMES;
else
return CHEEVOS_FORMAT_OTHER;
}
static int cheevos_new_lboard(cheevos_readud_t *ud)
{
cheevos_leaderboard_t *lboard = NULL;
cheevos_leaderboard_t *ldb = cheevos_locals.leaderboards;
if (!ldb || !ud)
return -1;
lboard = ldb + ud->lboard_count++;
lboard->id = (unsigned)strtol(ud->id.string, NULL, 10);
lboard->format = cheevos_parse_format(&ud->format);
lboard->title = cheevos_dupstr(&ud->title);
lboard->description = cheevos_dupstr(&ud->desc);
if (!lboard->title || !lboard->description)
goto error;
if (cheevos_parse_mem(lboard, ud->memaddr.string))
goto error;
return 0;
error:
if ((void*)lboard->title)
free((void*)lboard->title);
if ((void*)lboard->description)
free((void*)lboard->description);
return -1;
}
static int cheevos_read__json_key( void *userdata,
const char *name, size_t length)
{
cheevos_readud_t *ud = (cheevos_readud_t*)userdata;
if (ud)
{
int common = ud->in_cheevos || ud->in_lboards;
uint32_t hash = cheevos_djb2(name, length);
ud->field = NULL;
switch (hash)
{
case CHEEVOS_JSON_KEY_ACHIEVEMENTS:
ud->in_cheevos = 1;
break;
case CHEEVOS_JSON_KEY_LEADERBOARDS:
ud->in_lboards = 1;
break;
case CHEEVOS_JSON_KEY_CONSOLE_ID:
ud->is_console_id = 1;
break;
case CHEEVOS_JSON_KEY_ID:
if (common)
ud->field = &ud->id;
break;
case CHEEVOS_JSON_KEY_MEMADDR:
if (ud->in_cheevos)
ud->field = &ud->memaddr;
break;
case CHEEVOS_JSON_KEY_MEM:
if (ud->in_lboards)
ud->field = &ud->memaddr;
break;
case CHEEVOS_JSON_KEY_TITLE:
if (common)
ud->field = &ud->title;
break;
case CHEEVOS_JSON_KEY_DESCRIPTION:
if (common)
ud->field = &ud->desc;
break;
case CHEEVOS_JSON_KEY_POINTS:
if (ud->in_cheevos)
ud->field = &ud->points;
break;
case CHEEVOS_JSON_KEY_AUTHOR:
if (ud->in_cheevos)
ud->field = &ud->author;
break;
case CHEEVOS_JSON_KEY_MODIFIED:
if (ud->in_cheevos)
ud->field = &ud->modified;
break;
case CHEEVOS_JSON_KEY_CREATED:
if (ud->in_cheevos)
ud->field = &ud->created;
break;
case CHEEVOS_JSON_KEY_BADGENAME:
if (ud->in_cheevos)
ud->field = &ud->badge;
break;
case CHEEVOS_JSON_KEY_FLAGS:
if (ud->in_cheevos)
ud->field = &ud->flags;
break;
case CHEEVOS_JSON_KEY_FORMAT:
if (ud->in_lboards)
ud->field = &ud->format;
break;
default:
break;
}
}
return 0;
}
static int cheevos_read__json_string(void *userdata,
const char *string, size_t length)
{
cheevos_readud_t *ud = (cheevos_readud_t*)userdata;
if (ud && ud->field)
{
ud->field->string = string;
ud->field->length = length;
}
return 0;
}
static int cheevos_read__json_number(void *userdata,
const char *number, size_t length)
{
cheevos_readud_t *ud = (cheevos_readud_t*)userdata;
if (ud)
{
if (ud->field)
{
ud->field->string = number;
ud->field->length = length;
}
else if (ud->is_console_id)
{
cheevos_locals.console_id = (cheevos_console_t)
strtol(number, NULL, 10);
ud->is_console_id = 0;
}
}
return 0;
}
static int cheevos_read__json_end_object(void *userdata)
{
cheevos_readud_t *ud = (cheevos_readud_t*)userdata;
if (ud)
{
if (ud->in_cheevos)
return cheevos_new_cheevo(ud);
if (ud->in_lboards)
return cheevos_new_lboard(ud);
}
return 0;
}
static int cheevos_read__json_end_array(void *userdata)
{
cheevos_readud_t *ud = (cheevos_readud_t*)userdata;
if (ud)
{
ud->in_cheevos = 0;
ud->in_lboards = 0;
}
return 0;
}
static int cheevos_parse(const char *json)
{
static const jsonsax_handlers_t handlers =
{
NULL,
NULL,
NULL,
cheevos_read__json_end_object,
NULL,
cheevos_read__json_end_array,
cheevos_read__json_key,
NULL,
cheevos_read__json_string,
cheevos_read__json_number,
NULL,
NULL
};
cheevos_readud_t ud;
unsigned core_count, unofficial_count, lboard_count;
/* Count the number of achievements in the JSON file. */
int res = cheevos_count_cheevos(json, &core_count, &unofficial_count,
&lboard_count);
if (res != JSONSAX_OK)
return -1;
/* Allocate the achievements. */
cheevos_locals.core.cheevos = (cheevo_t*)
calloc(core_count, sizeof(cheevo_t));
cheevos_locals.core.count = core_count;
cheevos_locals.unofficial.cheevos = (cheevo_t*)
calloc(unofficial_count, sizeof(cheevo_t));
cheevos_locals.unofficial.count = unofficial_count;
cheevos_locals.leaderboards = (cheevos_leaderboard_t*)
calloc(lboard_count, sizeof(cheevos_leaderboard_t));
cheevos_locals.lboard_count = lboard_count;
if ( !cheevos_locals.core.cheevos ||
!cheevos_locals.unofficial.cheevos ||
!cheevos_locals.leaderboards)
{
if ((void*)cheevos_locals.core.cheevos)
free((void*)cheevos_locals.core.cheevos);
if ((void*)cheevos_locals.unofficial.cheevos)
free((void*)cheevos_locals.unofficial.cheevos);
if ((void*)cheevos_locals.leaderboards)
free((void*)cheevos_locals.leaderboards);
cheevos_locals.core.count = cheevos_locals.unofficial.count =
cheevos_locals.lboard_count = 0;
return -1;
}
/* Load the achievements. */
ud.in_cheevos = 0;
ud.in_lboards = 0;
ud.is_console_id = 0;
ud.field = NULL;
ud.core_count = 0;
ud.unofficial_count = 0;
ud.lboard_count = 0;
if (jsonsax_parse(json, &handlers, (void*)&ud) != JSONSAX_OK)
goto error;
return 0;
error:
cheevos_unload();
return -1;
}
/*****************************************************************************
Test all the achievements (call once per frame).
*****************************************************************************/
static int cheevos_test_condition(cheevos_cond_t *cond)
{
unsigned sval = 0;
unsigned tval = 0;
if (!cond)
return 0;
sval = cheevos_var_get_value(&cond->source) +
cheevos_locals.add_buffer;
tval = cheevos_var_get_value(&cond->target);
switch (cond->op)
{
case CHEEVOS_COND_OP_EQUALS:
return (sval == tval);
case CHEEVOS_COND_OP_LESS_THAN:
return (sval < tval);
case CHEEVOS_COND_OP_LESS_THAN_OR_EQUAL:
return (sval <= tval);
case CHEEVOS_COND_OP_GREATER_THAN:
return (sval > tval);
case CHEEVOS_COND_OP_GREATER_THAN_OR_EQUAL:
return (sval >= tval);
case CHEEVOS_COND_OP_NOT_EQUAL_TO:
return (sval != tval);
default:
break;
}
return 1;
}
static int cheevos_test_cond_set(const cheevos_condset_t *condset,
int *dirty_conds, int *reset_conds, int match_any)
{
int cond_valid = 0;
int set_valid = 1;
const cheevos_cond_t *end = NULL;
cheevos_cond_t *cond = NULL;
if (!condset)
return 0;
end = condset->conds + condset->count;
cheevos_locals.add_buffer = 0;
cheevos_locals.add_hits = 0;
/* Now, read all Pause conditions, and if any are true,
* do not process further (retain old state). */
for (cond = condset->conds; cond < end; cond++)
{
if (cond->type != CHEEVOS_COND_TYPE_PAUSE_IF)
continue;
/* Reset by default, set to 1 if hit! */
cond->curr_hits = 0;
if (cheevos_test_condition(cond))
{
cond->curr_hits = 1;
*dirty_conds = 1;
/* Early out: this achievement is paused,
* do not process any further! */
return 0;
}
}
/* Read all standard conditions, and process as normal: */
for (cond = condset->conds; cond < end; cond++)
{
if ( cond->type == CHEEVOS_COND_TYPE_PAUSE_IF ||
cond->type == CHEEVOS_COND_TYPE_RESET_IF)
continue;
if (cond->type == CHEEVOS_COND_TYPE_ADD_SOURCE)
{
cheevos_locals.add_buffer += cheevos_var_get_value(&cond->source);
set_valid &= 1;
continue;
}
if (cond->type == CHEEVOS_COND_TYPE_SUB_SOURCE)
{
cheevos_locals.add_buffer -= cheevos_var_get_value(&cond->source);
set_valid &= 1;
continue;
}
if (cond->type == CHEEVOS_COND_TYPE_ADD_HITS)
{
if (cheevos_test_condition(cond))
{
cond->curr_hits++;
*dirty_conds = 1;
}
cheevos_locals.add_hits += cond->curr_hits;
continue;
}
if ( (cond->req_hits != 0) &&
(cond->curr_hits + cheevos_locals.add_hits) >= cond->req_hits)
{
cheevos_locals.add_buffer = 0;
cheevos_locals.add_hits = 0;
continue;
}
cond_valid = cheevos_test_condition(cond);
if (cond_valid)
{
cond->curr_hits++;
*dirty_conds = 1;
/* Process this logic, if this condition is true: */
if (cond->req_hits == 0)
; /* Not a hit-based requirement: ignore any additional logic! */
else if ((cond->curr_hits + cheevos_locals.add_hits) < cond->req_hits)
cond_valid = 0; /* Not entirely valid yet! */
if (match_any)
break;
}
cheevos_locals.add_buffer = 0;
cheevos_locals.add_hits = 0;
/* Sequential or non-sequential? */
set_valid &= cond_valid;
}
/* Now, ONLY read reset conditions! */
for (cond = condset->conds; cond < end; cond++)
{
if (cond->type != CHEEVOS_COND_TYPE_RESET_IF)
continue;
cond_valid = cheevos_test_condition(cond);
if (cond_valid)
{
*reset_conds = 1; /* Resets all hits found so far */
set_valid = 0; /* Cannot be valid if we've hit a reset condition. */
break; /* No point processing any further reset conditions. */
}
}
return set_valid;
}
static int cheevos_reset_cond_set(cheevos_condset_t *condset, int deltas)
{
int dirty = 0;
const cheevos_cond_t *end = NULL;
if (!condset)
return 0;
end = condset->conds + condset->count;
if (deltas)
{
cheevos_cond_t *cond = NULL;
for (cond = condset->conds; cond < end; cond++)
{
dirty |= cond->curr_hits != 0;
cond->curr_hits = 0;
cond->source.previous = cond->source.value;
cond->target.previous = cond->target.value;
}
}
else
{
cheevos_cond_t *cond = NULL;
for (cond = condset->conds; cond < end; cond++)
{
dirty |= cond->curr_hits != 0;
cond->curr_hits = 0;
}
}
return dirty;
}
static int cheevos_test_cheevo(cheevo_t *cheevo)
{
int dirty_conds = 0;
int reset_conds = 0;
int ret_val = 0;
int ret_val_sub_cond = 0;
cheevos_condset_t *condset = NULL;
cheevos_condset_t *end = NULL;
if (!cheevo)
return 0;
ret_val_sub_cond = cheevo->condition.count == 1;
condset = cheevo->condition.condsets;
if (!condset)
return 0;
end = condset + cheevo->condition.count;
if (condset < end)
{
ret_val = cheevos_test_cond_set(condset, &dirty_conds, &reset_conds, 0);
condset++;
}
while (condset < end)
{
ret_val_sub_cond |= cheevos_test_cond_set(
condset, &dirty_conds, &reset_conds, 0);
condset++;
}
if (dirty_conds)
cheevo->dirty |= CHEEVOS_DIRTY_CONDITIONS;
if (reset_conds)
{
int dirty = 0;
for (condset = cheevo->condition.condsets; condset < end; condset++)
dirty |= cheevos_reset_cond_set(condset, 0);
if (dirty)
cheevo->dirty |= CHEEVOS_DIRTY_CONDITIONS;
}
return (ret_val && ret_val_sub_cond);
}
static void cheevos_url_encode(const char *str, char *encoded, size_t len)
{
if (!str)
return;
while (*str)
{
if ( isalnum((unsigned char)*str) || *str == '-'
|| *str == '_' || *str == '.'
|| *str == '~')
{
if (len >= 2)
{
*encoded++ = *str++;
len--;
}
else
break;
}
else
{
if (len >= 4)
{
snprintf(encoded, len, "%%%02x", (uint8_t)*str);
encoded += 3;
str++;
len -= 3;
}
else
break;
}
}
*encoded = 0;
}
static void cheevos_make_unlock_url(const cheevo_t *cheevo,
char* url, size_t url_size)
{
settings_t *settings = config_get_ptr();
if (!settings)
return;
snprintf(
url, url_size,
"http://retroachievements.org/dorequest.php?r=awardachievement&u=%s&t=%s&a=%u&h=%d",
settings->arrays.cheevos_username,
cheevos_locals.token,
cheevo->id,
settings->bools.cheevos_hardcore_mode_enable && !cheevos_hardcore_paused ? 1 : 0
);
url[url_size - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to award the cheevo: %s\n", url);
#endif
}
static void cheevos_unlocked(void *task_data, void *user_data,
const char *error)
{
cheevo_t *cheevo = (cheevo_t *)user_data;
if (!error)
{
CHEEVOS_LOG("[CHEEVOS]: awarded achievement %u.\n", cheevo->id);
}
else
{
char url[256];
url[0] = '\0';
CHEEVOS_ERR("[CHEEVOS]: error awarding achievement %u, retrying...\n", cheevo->id);
cheevos_make_unlock_url(cheevo, url, sizeof(url));
task_push_http_transfer(url, true, NULL, cheevos_unlocked, cheevo);
}
}
static void cheevos_test_cheevo_set(const cheevoset_t *set)
{
settings_t *settings = config_get_ptr();
int mode = CHEEVOS_ACTIVE_SOFTCORE;
cheevo_t *cheevo = NULL;
const cheevo_t *end = NULL;
if (!set)
return;
end = set->cheevos + set->count;
if (settings && settings->bools.cheevos_hardcore_mode_enable && !cheevos_hardcore_paused)
mode = CHEEVOS_ACTIVE_HARDCORE;
for (cheevo = set->cheevos; cheevo < end; cheevo++)
{
if (cheevo->active & mode)
{
int valid = cheevos_test_cheevo(cheevo);
if (cheevo->last)
{
cheevos_condset_t* condset = cheevo->condition.condsets;
const cheevos_condset_t* end = cheevo->condition.condsets
+ cheevo->condition.count;
for (; condset < end; condset++)
cheevos_reset_cond_set(condset, 0);
}
else if (valid)
{
char msg[256];
char url[256];
msg[0] = url[0] = '\0';
cheevo->active &= ~mode;
if (mode == CHEEVOS_ACTIVE_HARDCORE)
cheevo->active &= ~CHEEVOS_ACTIVE_SOFTCORE;
CHEEVOS_LOG("[CHEEVOS]: awarding cheevo %u: %s (%s).\n",
cheevo->id, cheevo->title, cheevo->description);
snprintf(msg, sizeof(msg), "Achievement Unlocked: %s",
cheevo->title);
msg[sizeof(msg) - 1] = 0;
runloop_msg_queue_push(msg, 0, 2 * 60, false);
runloop_msg_queue_push(cheevo->description, 0, 3 * 60, false);
cheevos_make_unlock_url(cheevo, url, sizeof(url));
task_push_http_transfer(url, true, NULL,
cheevos_unlocked, cheevo);
if (settings && settings->bools.cheevos_auto_screenshot)
{
char shotname[256];
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(shotname, true,
video_driver_cached_frame_has_valid_framebuffer()))
CHEEVOS_LOG("[CHEEVOS]: got a screenshot for cheevo %u\n", cheevo->id);
else
CHEEVOS_LOG("[CHEEVOS]: failed to get screenshot for cheevo %u\n", cheevo->id);
}
}
cheevo->last = valid;
}
}
}
static int cheevos_test_lboard_condition(const cheevos_condition_t* condition)
{
int dirty_conds = 0;
int reset_conds = 0;
int ret_val = 0;
int ret_val_sub_cond = 0;
cheevos_condset_t *condset = NULL;
const cheevos_condset_t *end = NULL;
if (!condition)
return 0;
ret_val_sub_cond = condition->count == 1;
condset = condition->condsets;
end = condset + condition->count;
if (condset < end)
{
ret_val = cheevos_test_cond_set(
condset, &dirty_conds, &reset_conds, 0);
condset++;
}
while (condset < end)
{
ret_val_sub_cond |= cheevos_test_cond_set(
condset, &dirty_conds, &reset_conds, 0);
condset++;
}
if (reset_conds)
{
for (condset = condition->condsets; condset < end; condset++)
cheevos_reset_cond_set(condset, 0);
}
return (ret_val && ret_val_sub_cond);
}
static int cheevos_expr_value(cheevos_expr_t* expr)
{
unsigned i;
int values[16];
/* Separate possible values with '$' operator, submit the largest */
unsigned current_value = 0;
cheevos_term_t* term = NULL;
if (!expr)
return 0;
term = expr->terms;
if (!term)
return 0;
if (expr->compare_count >= ARRAY_SIZE(values))
{
CHEEVOS_ERR("[CHEEVOS]: too many values in the leaderboard expression: %u\n", expr->compare_count);
return 0;
}
memset(values, 0, sizeof values);
for (i = expr->count; i != 0; i--, term++)
{
if (current_value >= ARRAY_SIZE(values))
{
CHEEVOS_ERR("[CHEEVOS]: too many values in the leaderboard expression: %u\n", current_value);
return 0;
}
values[current_value] +=
cheevos_var_get_value(&term->var) * term->multiplier;
if (term->compare_next)
current_value++;
}
if (expr->compare_count > 1)
{
unsigned j;
int maximum = values[0];
for (j = 1; j < expr->compare_count; j++)
maximum = values[j] > maximum ? values[j] : maximum;
return maximum;
}
else
return values[0];
}
static void cheevos_make_lboard_url(const cheevos_leaderboard_t *lboard,
char* url, size_t url_size)
{
MD5_CTX ctx;
uint8_t hash[16];
char signature[64];
settings_t *settings = config_get_ptr();
hash[0] = '\0';
snprintf(signature, sizeof(signature), "%u%s%u", lboard->id,
settings->arrays.cheevos_username,
lboard->id);
MD5_Init(&ctx);
MD5_Update(&ctx, (void*)signature, strlen(signature));
MD5_Final(hash, &ctx);
snprintf(
url, url_size,
"http://retroachievements.org/dorequest.php?r=submitlbentry&u=%s&t=%s&i=%u&s=%d"
"&v=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
settings->arrays.cheevos_username,
cheevos_locals.token,
lboard->id,
lboard->last_value,
hash[ 0], hash[ 1], hash[ 2], hash[ 3],
hash[ 4], hash[ 5], hash[ 6], hash[ 7],
hash[ 8], hash[ 9], hash[10], hash[11],
hash[12], hash[13], hash[14], hash[15]
);
url[url_size - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to submit the leaderboard: %s\n", url);
#endif
}
static void cheevos_lboard_submit(void *task_data, void *user_data,
const char *error)
{
cheevos_leaderboard_t *lboard = (cheevos_leaderboard_t *)user_data;
if (!lboard)
return;
if (!error)
{
CHEEVOS_ERR("[CHEEVOS]: error submitting leaderboard %u\n", lboard->id);
return;
}
CHEEVOS_LOG("[CHEEVOS]: submitted leaderboard %u.\n", lboard->id);
}
static void cheevos_test_leaderboards(void)
{
unsigned i;
cheevos_leaderboard_t* lboard = cheevos_locals.leaderboards;
if (!lboard)
return;
for (i = cheevos_locals.lboard_count; i != 0; i--, lboard++)
{
if (lboard->active)
{
int value = cheevos_expr_value(&lboard->value);
if (value != lboard->last_value)
{
CHEEVOS_LOG("[CHEEVOS]: value lboard %s %u\n",
lboard->title, value);
lboard->last_value = value;
}
if (cheevos_test_lboard_condition(&lboard->submit))
{
lboard->active = 0;
/* failsafe for improper LBs */
if (value == 0)
{
CHEEVOS_LOG("[CHEEVOS]: error: lboard %s tried to submit 0\n",
lboard->title);
runloop_msg_queue_push("Leaderboard attempt cancelled!",
0, 2 * 60, false);
}
else
{
char url[256];
char msg[256];
char formatted_value[16];
cheevos_make_lboard_url(lboard, url, sizeof(url));
task_push_http_transfer(url, true, NULL,
cheevos_lboard_submit, lboard);
CHEEVOS_LOG("[CHEEVOS]: submit lboard %s\n", lboard->title);
cheevos_format_value(value, lboard->format,
formatted_value, sizeof(formatted_value));
snprintf(msg, sizeof(msg), "Submitted %s for %s",
formatted_value, lboard->title);
msg[sizeof(msg) - 1] = 0;
runloop_msg_queue_push(msg, 0, 2 * 60, false);
}
}
if (cheevos_test_lboard_condition(&lboard->cancel))
{
CHEEVOS_LOG("[CHEEVOS]: cancel lboard %s\n", lboard->title);
lboard->active = 0;
runloop_msg_queue_push("Leaderboard attempt cancelled!",
0, 2 * 60, false);
}
}
else
{
if (cheevos_test_lboard_condition(&lboard->start))
{
char msg[256];
CHEEVOS_LOG("[CHEEVOS]: start lboard %s\n", lboard->title);
lboard->active = 1;
lboard->last_value = -1;
snprintf(msg, sizeof(msg),
"Leaderboard Active: %s", lboard->title);
msg[sizeof(msg) - 1] = 0;
runloop_msg_queue_push(msg, 0, 2 * 60, false);
runloop_msg_queue_push(lboard->description, 0, 3*60, false);
}
}
}
}
/*****************************************************************************
Free the loaded achievements.
*****************************************************************************/
static void cheevos_free_condset(const cheevos_condset_t *set)
{
if (set && set->conds)
free((void*)set->conds);
}
static void cheevos_free_cheevo(const cheevo_t *cheevo)
{
if (!cheevo)
return;
if (cheevo->title)
free((void*)cheevo->title);
if (cheevo->description)
free((void*)cheevo->description);
if (cheevo->author)
free((void*)cheevo->author);
if (cheevo->badge)
free((void*)cheevo->badge);
cheevos_free_condset(cheevo->condition.condsets);
}
static void cheevos_free_cheevo_set(const cheevoset_t *set)
{
const cheevo_t *cheevo = NULL;
const cheevo_t *end = NULL;
if (!set)
return;
cheevo = set->cheevos;
end = cheevo + set->count;
while (cheevo < end)
cheevos_free_cheevo(cheevo++);
if (set->cheevos)
free((void*)set->cheevos);
}
#ifndef CHEEVOS_DONT_DEACTIVATE
static int cheevos_deactivate__json_index(void *userdata, unsigned int index)
{
cheevos_deactivate_t *ud = (cheevos_deactivate_t*)userdata;
if (ud)
ud->is_element = 1;
return 0;
}
static int cheevos_deactivate__json_number(void *userdata,
const char *number, size_t length)
{
long id;
int found;
cheevo_t* cheevo = NULL;
const cheevo_t* end = NULL;
cheevos_deactivate_t *ud = (cheevos_deactivate_t*)userdata;
if (ud && ud->is_element)
{
ud->is_element = 0;
id = strtol(number, NULL, 10);
found = 0;
cheevo = cheevos_locals.core.cheevos;
end = cheevo + cheevos_locals.core.count;
for (; cheevo < end; cheevo++)
{
if (cheevo->id == (unsigned)id)
{
cheevo->active &= ~ud->mode;
found = 1;
break;
}
}
if (!found)
{
cheevo = cheevos_locals.unofficial.cheevos;
end = cheevo + cheevos_locals.unofficial.count;
for (; cheevo < end; cheevo++)
{
if (cheevo->id == (unsigned)id)
{
cheevo->active &= ~ud->mode;
found = 1;
break;
}
}
}
if (found)
CHEEVOS_LOG("[CHEEVOS]: deactivated unlocked cheevo %u (%s).\n",
cheevo->id, cheevo->title);
else
CHEEVOS_ERR("[CHEEVOS]: unknown cheevo to deactivate: %u.\n", id);
}
return 0;
}
static int cheevos_deactivate_unlocks(const char* json, unsigned mode)
{
static const jsonsax_handlers_t handlers =
{
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
cheevos_deactivate__json_index,
NULL,
cheevos_deactivate__json_number,
NULL,
NULL
};
cheevos_deactivate_t ud;
ud.is_element = 0;
ud.mode = mode;
return jsonsax_parse(json, &handlers, (void*)&ud) != JSONSAX_OK;
}
#endif
void cheevos_reset_game(void)
{
cheevo_t *end = NULL;
cheevo_t *cheevo = cheevos_locals.core.cheevos;
if (!cheevo)
return;
end = cheevo + cheevos_locals.core.count;
for (; cheevo < end; cheevo++)
cheevo->last = 1;
cheevo = cheevos_locals.unofficial.cheevos;
end = cheevo + cheevos_locals.unofficial.count;
for (; cheevo < end; cheevo++)
cheevo->last = 1;
}
void cheevos_populate_menu(void *data)
{
#ifdef HAVE_MENU
unsigned i;
unsigned items_found = 0;
settings_t *settings = config_get_ptr();
menu_displaylist_info_t *info = (menu_displaylist_info_t*)data;
cheevo_t *end = NULL;
cheevo_t *cheevo = cheevos_locals.core.cheevos;
end = cheevo + cheevos_locals.core.count;
if (cheevo)
{
for (i = 0; cheevo < end; i++, cheevo++)
{
if (!(cheevo->active & CHEEVOS_ACTIVE_HARDCORE))
{
menu_entries_append_enum(info->list, cheevo->title,
cheevo->description,
MENU_ENUM_LABEL_CHEEVOS_UNLOCKED_ENTRY_HARDCORE,
MENU_SETTINGS_CHEEVOS_START + i, 0, 0);
items_found++;
set_badge_info(&badges_ctx, i, cheevo->badge,
(cheevo->active & CHEEVOS_ACTIVE_HARDCORE));
}
else if (!(cheevo->active & CHEEVOS_ACTIVE_SOFTCORE))
{
menu_entries_append_enum(info->list, cheevo->title,
cheevo->description,
MENU_ENUM_LABEL_CHEEVOS_UNLOCKED_ENTRY,
MENU_SETTINGS_CHEEVOS_START + i, 0, 0);
items_found++;
set_badge_info(&badges_ctx, i, cheevo->badge,
(cheevo->active & CHEEVOS_ACTIVE_SOFTCORE));
}
else
{
menu_entries_append_enum(info->list, cheevo->title,
cheevo->description,
MENU_ENUM_LABEL_CHEEVOS_LOCKED_ENTRY,
MENU_SETTINGS_CHEEVOS_START + i, 0, 0);
items_found++;
set_badge_info(&badges_ctx, i, cheevo->badge,
(cheevo->active & CHEEVOS_ACTIVE_SOFTCORE));
}
}
}
cheevo = cheevos_locals.unofficial.cheevos;
if (cheevo && settings->bools.cheevos_test_unofficial)
{
end = cheevo + cheevos_locals.unofficial.count;
for (i = cheevos_locals.core.count; cheevo < end; i++, cheevo++)
{
if (!(cheevo->active & CHEEVOS_ACTIVE_HARDCORE))
{
menu_entries_append_enum(info->list, cheevo->title,
cheevo->description,
MENU_ENUM_LABEL_CHEEVOS_UNLOCKED_ENTRY_HARDCORE,
MENU_SETTINGS_CHEEVOS_START + i, 0, 0);
items_found++;
set_badge_info(&badges_ctx, i, cheevo->badge,
(cheevo->active & CHEEVOS_ACTIVE_HARDCORE));
}
else if (!(cheevo->active & CHEEVOS_ACTIVE_SOFTCORE))
{
menu_entries_append_enum(info->list, cheevo->title,
cheevo->description,
MENU_ENUM_LABEL_CHEEVOS_UNLOCKED_ENTRY,
MENU_SETTINGS_CHEEVOS_START + i, 0, 0);
items_found++;
set_badge_info(&badges_ctx, i, cheevo->badge,
(cheevo->active & CHEEVOS_ACTIVE_SOFTCORE));
}
else
{
menu_entries_append_enum(info->list, cheevo->title,
cheevo->description,
MENU_ENUM_LABEL_CHEEVOS_LOCKED_ENTRY,
MENU_SETTINGS_CHEEVOS_START + i, 0, 0);
items_found++;
set_badge_info(&badges_ctx, i, cheevo->badge,
(cheevo->active & CHEEVOS_ACTIVE_SOFTCORE));
}
}
}
if (items_found == 0)
{
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 cheevos_get_description(cheevos_ctx_desc_t *desc)
{
if (!desc)
return false;
if (cheevos_loaded)
{
cheevo_t *cheevos = cheevos_locals.core.cheevos;
if (!cheevos)
return false;
if (desc->idx >= cheevos_locals.core.count)
{
cheevos = cheevos_locals.unofficial.cheevos;
desc->idx -= cheevos_locals.unofficial.count;
}
if (!string_is_empty(cheevos[desc->idx].description))
strlcpy(desc->s, cheevos[desc->idx].description, desc->len);
}
else
*desc->s = 0;
return true;
}
bool cheevos_apply_cheats(bool *data_bool)
{
cheats_are_enabled = *data_bool;
cheats_were_enabled |= cheats_are_enabled;
return true;
}
bool cheevos_unload(void)
{
bool running;
CHEEVOS_LOCK(cheevos_locals.task_lock);
running = cheevos_locals.task != NULL;
CHEEVOS_UNLOCK(cheevos_locals.task_lock);
if (running)
{
CHEEVOS_LOG("[CHEEVOS]: Asked the load thread to terminate\n");
task_queue_cancel_task(cheevos_locals.task);
#ifdef HAVE_THREADS
do
{
CHEEVOS_LOCK(cheevos_locals.task_lock);
running = cheevos_locals.task != NULL;
CHEEVOS_UNLOCK(cheevos_locals.task_lock);
}
while (running);
#endif
}
if (cheevos_loaded)
{
cheevos_free_cheevo_set(&cheevos_locals.core);
cheevos_free_cheevo_set(&cheevos_locals.unofficial);
}
cheevos_locals.core.cheevos = NULL;
cheevos_locals.unofficial.cheevos = NULL;
cheevos_locals.core.count = 0;
cheevos_locals.unofficial.count = 0;
cheevos_loaded = false;
return true;
}
bool cheevos_toggle_hardcore_mode(void)
{
settings_t *settings = config_get_ptr();
if (!settings)
return false;
/* reset and deinit rewind to avoid cheat the score */
if (settings->bools.cheevos_hardcore_mode_enable && !cheevos_hardcore_paused)
{
const char *msg = msg_hash_to_str(
MSG_CHEEVOS_HARDCORE_MODE_ENABLE);
/* send reset core cmd to avoid any user
* savestate previusly loaded. */
command_event(CMD_EVENT_RESET, NULL);
if (settings->bools.rewind_enable)
command_event(CMD_EVENT_REWIND_DEINIT, NULL);
CHEEVOS_LOG("%s\n", msg);
runloop_msg_queue_push(msg, 0, 3 * 60, true);
}
else
{
if (settings->bools.rewind_enable)
command_event(CMD_EVENT_REWIND_INIT, NULL);
}
return true;
}
static void cheevos_patch_addresses(cheevoset_t* set)
{
unsigned i;
cheevo_t* cheevo = NULL;
if (!set)
return;
cheevo = set->cheevos;
if (!cheevo)
return;
for (i = set->count; i != 0; i--, cheevo++)
{
unsigned j;
cheevos_condset_t* condset = cheevo->condition.condsets;
for (j = cheevo->condition.count; j != 0; j--, condset++)
{
unsigned k;
cheevos_cond_t* cond = condset->conds;
for (k = condset->count; k != 0; k--, cond++)
{
switch (cond->source.type)
{
case CHEEVOS_VAR_TYPE_ADDRESS:
case CHEEVOS_VAR_TYPE_DELTA_MEM:
cheevos_var_patch_addr(&cond->source,
cheevos_locals.console_id);
#ifdef CHEEVOS_DUMP_ADDRS
CHEEVOS_LOG("[CHEEVOS]: s-var %03d:%08X\n",
cond->source.bank_id + 1, cond->source.value);
#endif
break;
default:
break;
}
switch (cond->target.type)
{
case CHEEVOS_VAR_TYPE_ADDRESS:
case CHEEVOS_VAR_TYPE_DELTA_MEM:
cheevos_var_patch_addr(&cond->target,
cheevos_locals.console_id);
#ifdef CHEEVOS_DUMP_ADDRS
CHEEVOS_LOG("[CHEEVOS]: t-var %03d:%08X\n",
cond->target.bank_id + 1, cond->target.value);
#endif
break;
default:
break;
}
}
}
}
}
static void cheevos_patch_lb_conditions(cheevos_condition_t* condition)
{
unsigned i;
cheevos_condset_t* condset = NULL;
if (!condition)
return;
condset = condition->condsets;
for (i = condition->count; i != 0; i--, condset++)
{
unsigned j;
cheevos_cond_t* cond = condset->conds;
for (j = condset->count; j != 0; j--, cond++)
{
switch (cond->source.type)
{
case CHEEVOS_VAR_TYPE_ADDRESS:
case CHEEVOS_VAR_TYPE_DELTA_MEM:
cheevos_var_patch_addr(&cond->source,
cheevos_locals.console_id);
#ifdef CHEEVOS_DUMP_ADDRS
CHEEVOS_LOG("[CHEEVOS]: s-var %03d:%08X\n",
cond->source.bank_id + 1, cond->source.value);
#endif
break;
default:
break;
}
switch (cond->target.type)
{
case CHEEVOS_VAR_TYPE_ADDRESS:
case CHEEVOS_VAR_TYPE_DELTA_MEM:
cheevos_var_patch_addr(&cond->target,
cheevos_locals.console_id);
#ifdef CHEEVOS_DUMP_ADDRS
CHEEVOS_LOG("[CHEEVOS]: t-var %03d:%08X\n",
cond->target.bank_id + 1, cond->target.value);
#endif
break;
default:
break;
}
}
}
}
static void cheevos_patch_lb_expressions(cheevos_expr_t* expression)
{
unsigned i;
cheevos_term_t* term = NULL;
if (!expression)
return;
term = expression->terms;
for (i = expression->count; i != 0; i--, term++)
{
switch (term->var.type)
{
case CHEEVOS_VAR_TYPE_ADDRESS:
case CHEEVOS_VAR_TYPE_DELTA_MEM:
cheevos_var_patch_addr(&term->var, cheevos_locals.console_id);
#ifdef CHEEVOS_DUMP_ADDRS
CHEEVOS_LOG("[CHEEVOS]: s-var %03d:%08X\n",
term->var.bank_id + 1, term->var.value);
#endif
break;
default:
break;
}
}
}
static void cheevos_patch_lbs(cheevos_leaderboard_t *leaderboard)
{
unsigned i;
for (i = 0; i < cheevos_locals.lboard_count; i++)
{
cheevos_condition_t *start = &leaderboard[i].start;
cheevos_condition_t *cancel = &leaderboard[i].cancel;
cheevos_condition_t *submit = &leaderboard[i].submit;
cheevos_expr_t *value = &leaderboard[i].value;
cheevos_patch_lb_conditions(start);
cheevos_patch_lb_conditions(cancel);
cheevos_patch_lb_conditions(submit);
cheevos_patch_lb_expressions(value);
}
}
void cheevos_test(void)
{
settings_t *settings = config_get_ptr();
if (!cheevos_locals.addrs_patched)
{
cheevos_patch_addresses(&cheevos_locals.core);
cheevos_patch_addresses(&cheevos_locals.unofficial);
cheevos_patch_lbs(cheevos_locals.leaderboards);
cheevos_locals.addrs_patched = true;
}
cheevos_test_cheevo_set(&cheevos_locals.core);
if (settings)
{
if (settings->bools.cheevos_test_unofficial)
cheevos_test_cheevo_set(&cheevos_locals.unofficial);
if (settings->bools.cheevos_hardcore_mode_enable &&
settings->bools.cheevos_leaderboards_enable &&
!cheevos_hardcore_paused)
cheevos_test_leaderboards();
}
}
bool cheevos_set_cheats(void)
{
cheats_were_enabled = cheats_are_enabled;
return true;
}
void cheevos_set_support_cheevos(bool state)
{
cheevos_locals.core_supports = state;
}
bool cheevos_get_support_cheevos(void)
{
return cheevos_locals.core_supports;
}
cheevos_console_t cheevos_get_console(void)
{
return cheevos_locals.console_id;
}
#include "coro.h"
/* Uncomment the following two lines to debug cheevos_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];
unsigned char hash[16];
bool round;
unsigned gameid;
unsigned i;
unsigned j;
unsigned k;
size_t bytes;
size_t count;
size_t offset;
size_t len;
size_t size;
MD5_CTX md5;
cheevos_nes_header_t header;
retro_time_t t0;
struct retro_system_info sysinfo;
void *data;
char *json;
const char *path;
const char *ext;
intfstream_t *stream;
cheevo_t *cheevo;
settings_t *settings;
struct http_connection_t *conn;
struct http_t *http;
const cheevo_t *cheevo_end;
/* co-routine required fields */
CORO_FIELDS
} coro_t;
enum
{
/* Negative values because CORO_SUB generates positive values */
SNES_MD5 = -1,
GENESIS_MD5 = -2,
LYNX_MD5 = -3,
NES_MD5 = -4,
GENERIC_MD5 = -5,
FILENAME_MD5 = -6,
EVAL_MD5 = -7,
FILL_MD5 = -8,
GET_GAMEID = -9,
GET_CHEEVOS = -10,
GET_BADGES = -11,
LOGIN = -12,
HTTP_GET = -13,
DEACTIVATE = -14,
PLAYING = -15,
DELAY = -16
};
static int cheevos_iterate(coro_t *coro)
{
ssize_t num_read = 0;
size_t to_read = 4096;
uint8_t *buffer = NULL;
const char *end = NULL;
static const uint32_t genesis_exts[] =
{
0x0b888feeU, /* mdx */
0x005978b6U, /* md */
0x0b88aa89U, /* smd */
0x0b88767fU, /* gen */
0x0b8861beU, /* bin */
0x0b886782U, /* cue */
0x0b8880d0U, /* iso */
0x0b88aa98U, /* sms */
0x005977f3U, /* gg */
0x0059797fU, /* sg */
0
};
static const uint32_t snes_exts[] =
{
0x0b88aa88U, /* smc */
0x0b8872bbU, /* fig */
0x0b88a9a1U, /* sfc */
0x0b887623U, /* gd3 */
0x0b887627U, /* gd7 */
0x0b886bf3U, /* dx2 */
0x0b886312U, /* bsx */
0x0b88abd2U, /* swc */
0
};
static const uint32_t lynx_exts[] =
{
0x0b888cf7U, /* lnx */
0
};
static cheevos_finder_t finders[] =
{
{SNES_MD5, "SNES (8Mb padding)", snes_exts},
{GENESIS_MD5, "Genesis (6Mb padding)", genesis_exts},
{LYNX_MD5, "Atari Lynx (only first 512 bytes)", lynx_exts},
{NES_MD5, "NES (discards VROM)", NULL},
{GENERIC_MD5, "Generic (plain content)", NULL},
{FILENAME_MD5, "Generic (filename)", NULL}
};
CORO_ENTER();
cheevos_locals.addrs_patched = false;
coro->settings = config_get_ptr();
cheevos_locals.meminfo[0].id = RETRO_MEMORY_SYSTEM_RAM;
core_get_memory(&cheevos_locals.meminfo[0]);
cheevos_locals.meminfo[1].id = RETRO_MEMORY_SAVE_RAM;
core_get_memory(&cheevos_locals.meminfo[1]);
cheevos_locals.meminfo[2].id = RETRO_MEMORY_VIDEO_RAM;
core_get_memory(&cheevos_locals.meminfo[2]);
cheevos_locals.meminfo[3].id = RETRO_MEMORY_RTC;
core_get_memory(&cheevos_locals.meminfo[3]);
CHEEVOS_LOG("[CHEEVOS]: system RAM: %p %u\n",
cheevos_locals.meminfo[0].data,
cheevos_locals.meminfo[0].size);
CHEEVOS_LOG("[CHEEVOS]: save RAM: %p %u\n",
cheevos_locals.meminfo[1].data,
cheevos_locals.meminfo[1].size);
CHEEVOS_LOG("[CHEEVOS]: video RAM: %p %u\n",
cheevos_locals.meminfo[2].data,
cheevos_locals.meminfo[2].size);
CHEEVOS_LOG("[CHEEVOS]: RTC: %p %u\n",
cheevos_locals.meminfo[3].data,
cheevos_locals.meminfo[3].size);
/* Bail out if cheevos are disabled.
* But set the above anyways,
* command_read_ram needs it. */
if (!coro->settings->bools.cheevos_enable)
CORO_STOP();
/* Load the content into memory, or copy it
* over to our own buffer */
if (!coro->data)
{
coro->stream = intfstream_open_file(
coro->path,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!coro->stream)
CORO_STOP();
CORO_YIELD();
coro->len = 0;
coro->count = intfstream_get_size(coro->stream);
/* size limit */
if (coro->count > size_in_megabytes(64))
coro->count = size_in_megabytes(64);
coro->data = malloc(coro->count);
if (!coro->data)
{
intfstream_close(coro->stream);
free(coro->stream);
CORO_STOP();
}
for (;;)
{
buffer = (uint8_t*)coro->data + coro->len;
to_read = 4096;
if (to_read > coro->count)
to_read = coro->count;
num_read = intfstream_read(coro->stream,
(void*)buffer, to_read);
if (num_read <= 0)
break;
coro->len += num_read;
coro->count -= num_read;
if (coro->count == 0)
break;
CORO_YIELD();
}
intfstream_close(coro->stream);
free(coro->stream);
}
/* Use the supported extensions as a hint
* to what method we should use. */
core_get_system_info(&coro->sysinfo);
for (coro->i = 0; coro->i < ARRAY_SIZE(finders); coro->i++)
{
if (finders[coro->i].ext_hashes)
{
coro->ext = coro->sysinfo.valid_extensions;
while (coro->ext)
{
unsigned hash;
end = strchr(coro->ext, '|');
if (end)
{
hash = cheevos_djb2(
coro->ext, end - coro->ext);
coro->ext = end + 1;
}
else
{
hash = cheevos_djb2(
coro->ext, strlen(coro->ext));
coro->ext = NULL;
}
for (coro->j = 0; finders[coro->i].ext_hashes[coro->j]; coro->j++)
{
if (finders[coro->i].ext_hashes[coro->j] == hash)
{
CHEEVOS_LOG("[CHEEVOS]: testing %s.\n",
finders[coro->i].name);
/*
* Inputs: CHEEVOS_VAR_INFO
* Outputs: CHEEVOS_VAR_GAMEID, the game was found if it's different from 0
*/
CORO_GOSUB(finders[coro->i].label);
if (coro->gameid != 0)
goto found;
coro->ext = NULL; /* force next finder */
break;
}
}
}
}
}
for (coro->i = 0; coro->i < ARRAY_SIZE(finders); coro->i++)
{
if (finders[coro->i].ext_hashes)
continue;
CHEEVOS_LOG("[CHEEVOS]: testing %s.\n",
finders[coro->i].name);
/*
* Inputs: CHEEVOS_VAR_INFO
* Outputs: CHEEVOS_VAR_GAMEID
*/
CORO_GOSUB(finders[coro->i].label);
if (coro->gameid != 0)
goto found;
}
CHEEVOS_LOG("[CHEEVOS]: this game doesn't feature achievements.\n");
CORO_STOP();
found:
#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(GET_CHEEVOS);
if (!coro->json)
{
runloop_msg_queue_push("Error loading achievements.", 0, 5 * 60, false);
CHEEVOS_ERR("[CHEEVOS]: 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 (cheevos_parse(coro->json))
{
if ((void*)coro->json)
free((void*)coro->json);
CORO_STOP();
}
if ((void*)coro->json)
free((void*)coro->json);
if ( cheevos_locals.core.count == 0
&& cheevos_locals.unofficial.count == 0
&& cheevos_locals.lboard_count == 0)
{
runloop_msg_queue_push(
"This game has no achievements.",
0, 5 * 60, false);
cheevos_free_cheevo_set(&cheevos_locals.core);
cheevos_free_cheevo_set(&cheevos_locals.unofficial);
cheevos_locals.core.cheevos = NULL;
cheevos_locals.unofficial.cheevos = NULL;
cheevos_locals.core.count = 0;
cheevos_locals.unofficial.count = 0;
cheevos_loaded = false;
CORO_STOP();
}
cheevos_loaded = true;
/*
* Inputs: CHEEVOS_VAR_GAMEID
* Outputs:
*/
CORO_GOSUB(DEACTIVATE);
/*
* Inputs: CHEEVOS_VAR_GAMEID
* Outputs:
*/
CORO_GOSUB(PLAYING);
if (coro->settings->bools.cheevos_verbose_enable && cheevos_locals.core.count > 0)
{
char msg[256];
int mode = CHEEVOS_ACTIVE_SOFTCORE;
const cheevo_t* cheevo = cheevos_locals.core.cheevos;
const cheevo_t* end = cheevo + cheevos_locals.core.count;
int number_of_unlocked = cheevos_locals.core.count;
if (coro->settings->bools.cheevos_hardcore_mode_enable && !cheevos_hardcore_paused)
mode = CHEEVOS_ACTIVE_HARDCORE;
for (; cheevo < end; cheevo++)
if (cheevo->active & mode)
number_of_unlocked--;
snprintf(msg, sizeof(msg),
"You have %d of %d achievements unlocked.",
number_of_unlocked, cheevos_locals.core.count);
msg[sizeof(msg) - 1] = 0;
runloop_msg_queue_push(msg, 0, 6 * 60, false);
}
CORO_GOSUB(GET_BADGES);
CORO_STOP();
/**************************************************************************
* Info Tries to identify a SNES game
* Input CHEEVOS_VAR_INFO the content info
* Output CHEEVOS_VAR_GAMEID the Retro Achievements game ID, or 0 if not found
*************************************************************************/
CORO_SUB(SNES_MD5)
MD5_Init(&coro->md5);
coro->offset = 0;
coro->count = 0;
CORO_GOSUB(EVAL_MD5);
if (coro->count == 0)
{
MD5_Final(coro->hash, &coro->md5);
coro->gameid = 0;
CORO_RET();
}
if (coro->count < size_in_megabytes(8))
{
/*
* Inputs: CHEEVOS_VAR_MD5, CHEEVOS_VAR_OFFSET, CHEEVOS_VAR_COUNT
* Outputs: CHEEVOS_VAR_MD5
*/
coro->offset = 0;
coro->count = size_in_megabytes(8) - coro->count;
CORO_GOSUB(FILL_MD5);
}
MD5_Final(coro->hash, &coro->md5);
CORO_GOTO(GET_GAMEID);
/**************************************************************************
* Info Tries to identify a Genesis game
* Input CHEEVOS_VAR_INFO the content info
* Output CHEEVOS_VAR_GAMEID the Retro Achievements game ID, or 0 if not found
*************************************************************************/
CORO_SUB(GENESIS_MD5)
MD5_Init(&coro->md5);
coro->offset = 0;
coro->count = 0;
CORO_GOSUB(EVAL_MD5);
if (coro->count == 0)
{
MD5_Final(coro->hash, &coro->md5);
coro->gameid = 0;
CORO_RET();
}
if (coro->count < size_in_megabytes(6))
{
coro->offset = 0;
coro->count = size_in_megabytes(6) - coro->count;
CORO_GOSUB(FILL_MD5);
}
MD5_Final(coro->hash, &coro->md5);
CORO_GOTO(GET_GAMEID);
/**************************************************************************
* Info Tries to identify an Atari Lynx game
* Input CHEEVOS_VAR_INFO the content info
* Output CHEEVOS_VAR_GAMEID the Retro Achievements game ID, or 0 if not found
*************************************************************************/
CORO_SUB(LYNX_MD5)
if (coro->len < 0x0240)
{
coro->gameid = 0;
CORO_RET();
}
MD5_Init(&coro->md5);
coro->offset = 0x0040;
coro->count = 0x0200;
CORO_GOSUB(EVAL_MD5);
MD5_Final(coro->hash, &coro->md5);
CORO_GOTO(GET_GAMEID);
/**************************************************************************
* Info Tries to identify a NES game
* Input CHEEVOS_VAR_INFO the content info
* Output CHEEVOS_VAR_GAMEID the Retro Achievements game ID, or 0 if not found
*************************************************************************/
CORO_SUB(NES_MD5)
/* Note about the references to the FCEU emulator below. There is no
* core-specific code in this function, it's rather Retro Achievements
* specific code that must be followed to the letter so we compute
* the correct ROM hash. Retro Achievements does indeed use some
* FCEU related method to compute the hash, since its NES emulator
* is based on it. */
if (coro->len < sizeof(coro->header))
{
coro->gameid = 0;
CORO_RET();
}
memcpy((void*)&coro->header, coro->data,
sizeof(coro->header));
if ( coro->header.id[0] != 'N'
|| coro->header.id[1] != 'E'
|| coro->header.id[2] != 'S'
|| coro->header.id[3] != 0x1a)
{
coro->gameid = 0;
CORO_RET();
}
{
size_t romsize = 256;
/* from FCEU core - compute size using the cart mapper */
int mapper = (coro->header.rom_type >> 4) | (coro->header.rom_type2 & 0xF0);
if (coro->header.rom_size)
romsize = next_pow2(coro->header.rom_size);
/* for games not to the power of 2, so we just read enough
* PRG rom from it, but we have to keep ROM_size to the power of 2
* since PRGCartMapping wants ROM_size to be to the power of 2
* so instead if not to power of 2, we just use head.ROM_size when
* we use FCEU_read. */
coro->round = mapper != 53 && mapper != 198 && mapper != 228;
coro->bytes = coro->round ? romsize : coro->header.rom_size;
}
/* from FCEU core - check if Trainer included in ROM data */
MD5_Init(&coro->md5);
coro->offset = sizeof(coro->header) + (coro->header.rom_type & 4
? sizeof(coro->header) : 0);
coro->count = 0x4000 * coro->bytes;
CORO_GOSUB(EVAL_MD5);
if (coro->count < 0x4000 * coro->bytes)
{
coro->offset = 0xff;
coro->count = 0x4000 * coro->bytes - coro->count;
CORO_GOSUB(FILL_MD5);
}
MD5_Final(coro->hash, &coro->md5);
CORO_GOTO(GET_GAMEID);
/**************************************************************************
* Info Tries to identify a "generic" game
* Input CHEEVOS_VAR_INFO the content info
* Output CHEEVOS_VAR_GAMEID the Retro Achievements game ID, or 0 if not found
*************************************************************************/
CORO_SUB(GENERIC_MD5)
MD5_Init(&coro->md5);
coro->offset = 0;
coro->count = 0;
CORO_GOSUB(EVAL_MD5);
MD5_Final(coro->hash, &coro->md5);
if (coro->count == 0)
CORO_RET();
CORO_GOTO(GET_GAMEID);
/**************************************************************************
* Info Tries to identify a game based on its filename (with no extension)
* Input CHEEVOS_VAR_INFO the content info
* Output CHEEVOS_VAR_GAMEID the Retro Achievements game ID, or 0 if not found
*************************************************************************/
CORO_SUB(FILENAME_MD5)
if (!string_is_empty(coro->path))
{
char base_noext[PATH_MAX_LENGTH];
fill_pathname_base_noext(base_noext, coro->path, sizeof(base_noext));
MD5_Init(&coro->md5);
MD5_Update(&coro->md5, (void*)base_noext, strlen(base_noext));
MD5_Final(coro->hash, &coro->md5);
CORO_GOTO(GET_GAMEID);
}
CORO_RET();
/**************************************************************************
* Info Evaluates the CHEEVOS_VAR_MD5 hash
* Inputs CHEEVOS_VAR_INFO, CHEEVOS_VAR_OFFSET, CHEEVOS_VAR_COUNT
* Outputs CHEEVOS_VAR_MD5, CHEEVOS_VAR_COUNT
*************************************************************************/
CORO_SUB(EVAL_MD5)
if (coro->count == 0)
coro->count = coro->len;
if (coro->len - coro->offset < coro->count)
coro->count = coro->len - coro->offset;
/* size limit */
if (coro->count > size_in_megabytes(64))
coro->count = size_in_megabytes(64);
MD5_Update(&coro->md5,
(void*)((uint8_t*)coro->data + coro->offset),
coro->count);
CORO_RET();
/**************************************************************************
* Info Updates the CHEEVOS_VAR_MD5 hash with a repeated value
* Inputs CHEEVOS_VAR_OFFSET, CHEEVOS_VAR_COUNT
* Outputs CHEEVOS_VAR_MD5
*************************************************************************/
CORO_SUB(FILL_MD5)
{
char buffer[4096];
while (coro->count > 0)
{
size_t len = sizeof(buffer);
if (len > coro->count)
len = coro->count;
memset((void*)buffer, coro->offset, len);
MD5_Update(&coro->md5, (void*)buffer, len);
coro->count -= len;
}
}
CORO_RET();
/**************************************************************************
* Info Gets the achievements from Retro Achievements
* Inputs coro->hash
* Outputs CHEEVOS_VAR_GAMEID
*************************************************************************/
CORO_SUB(GET_GAMEID)
{
char gameid[16];
CHEEVOS_LOG(
"[CHEEVOS]: getting game id for hash %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x\n",
coro->hash[ 0], coro->hash[ 1], coro->hash[ 2], coro->hash[ 3],
coro->hash[ 4], coro->hash[ 5], coro->hash[ 6], coro->hash[ 7],
coro->hash[ 8], coro->hash[ 9], coro->hash[10], coro->hash[11],
coro->hash[12], coro->hash[13], coro->hash[14], coro->hash[15]
);
snprintf(
coro->url, sizeof(coro->url),
"http://retroachievements.org/dorequest.php?r=gameid&m=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
coro->hash[ 0], coro->hash[ 1], coro->hash[ 2], coro->hash[ 3],
coro->hash[ 4], coro->hash[ 5], coro->hash[ 6], coro->hash[ 7],
coro->hash[ 8], coro->hash[ 9], coro->hash[10], coro->hash[11],
coro->hash[12], coro->hash[13], coro->hash[14], coro->hash[15]
);
coro->url[sizeof(coro->url) - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to get the game's id: %s\n", coro->url);
#endif
CORO_GOSUB(HTTP_GET);
if (!coro->json)
CORO_RET();
if (cheevos_get_value(coro->json,
CHEEVOS_JSON_KEY_GAMEID, gameid, sizeof(gameid)))
{
if ((void*)coro->json)
free((void*)coro->json);
CHEEVOS_ERR("[CHEEVOS]: error getting game_id.\n");
CORO_RET();
}
if ((void*)coro->json)
free((void*)coro->json);
CHEEVOS_LOG("[CHEEVOS]: got game id %s.\n", gameid);
coro->gameid = (unsigned)strtol(gameid, NULL, 10);
CORO_RET();
}
/**************************************************************************
* Info Gets the achievements from Retro Achievements
* Inputs CHEEVOS_VAR_GAMEID
* Outputs CHEEVOS_VAR_JSON
*************************************************************************/
CORO_SUB(GET_CHEEVOS)
CORO_GOSUB(LOGIN);
snprintf(coro->url, sizeof(coro->url),
"http://retroachievements.org/dorequest.php?r=patch&g=%u&u=%s&t=%s",
coro->gameid,
coro->settings->arrays.cheevos_username,
cheevos_locals.token);
coro->url[sizeof(coro->url) - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to get the list of cheevos: %s\n", coro->url);
#endif
CORO_GOSUB(HTTP_GET);
if (!coro->json)
{
CHEEVOS_ERR("[CHEEVOS]: error getting achievements for game id %u.\n", coro->gameid);
CORO_STOP();
}
CHEEVOS_LOG("[CHEEVOS]: 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(GET_BADGES)
badges_ctx = new_badges_ctx;
{
settings_t *settings = config_get_ptr();
if (!string_is_equal(settings->arrays.menu_driver, "xmb") ||
!settings->bools.cheevos_badges_enable)
CORO_RET();
}
coro->cheevo = cheevos_locals.core.cheevos;
coro->cheevo_end = cheevos_locals.core.cheevos + cheevos_locals.core.count;
for (; coro->cheevo < coro->cheevo_end; coro->cheevo++)
{
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->j == 0)
snprintf(coro->badge_name,
sizeof(coro->badge_name),
"%s.png", coro->cheevo->badge);
else
snprintf(coro->badge_name,
sizeof(coro->badge_name),
"%s_lock.png", coro->cheevo->badge);
fill_pathname_join(
coro->badge_fullpath,
coro->badge_fullpath,
coro->badge_name,
sizeof(coro->badge_fullpath));
if (!badge_exists(coro->badge_fullpath))
{
#ifdef CHEEVOS_LOG_BADGES
CHEEVOS_LOG(
"[CHEEVOS]: downloading badge %s\n",
coro->badge_fullpath);
#endif
snprintf(coro->url,
sizeof(coro->url),
"http://i.retroachievements.org/Badge/%s",
coro->badge_name);
CORO_GOSUB(HTTP_GET);
if (coro->json)
{
if (!filestream_write_file(coro->badge_fullpath,
coro->json, coro->k))
CHEEVOS_ERR("[CHEEVOS]: error writing badge %s\n", coro->badge_fullpath);
else
free(coro->json);
}
}
}
}
CORO_RET();
/**************************************************************************
* Info Logs in the user at Retro Achievements
*************************************************************************/
CORO_SUB(LOGIN)
if (cheevos_locals.token[0])
CORO_RET();
{
char urle_user[64];
char urle_login[64];
const char *username = coro ? coro->settings->arrays.cheevos_username : NULL;
const char *login = NULL;
bool via_token = false;
if (coro)
{
if (string_is_empty(coro->settings->arrays.cheevos_password))
{
via_token = true;
login = coro->settings->arrays.cheevos_token;
}
else
login = coro->settings->arrays.cheevos_password;
}
else
login = NULL;
if (string_is_empty(username) || string_is_empty(login))
{
runloop_msg_queue_push(
"Missing RetroAchievements account information.",
0, 5 * 60, false);
runloop_msg_queue_push(
"Please fill in your account information in Settings.",
0, 5 * 60, false);
CHEEVOS_ERR("[CHEEVOS]: login info not informed.\n");
CORO_STOP();
}
cheevos_url_encode(username, urle_user, sizeof(urle_user));
cheevos_url_encode(login, urle_login, sizeof(urle_login));
snprintf(
coro->url, sizeof(coro->url),
"http://retroachievements.org/dorequest.php?r=login&u=%s&%c=%s",
urle_user, via_token ? 't' : 'p', urle_login
);
coro->url[sizeof(coro->url) - 1] = 0;
}
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to login: %s\n",
coro->url);
#endif
CORO_GOSUB(HTTP_GET);
if (coro->json)
{
char error_response[64];
char error_message[256];
cheevos_get_value(
coro->json,
CHEEVOS_JSON_KEY_ERROR,
error_response,
sizeof(error_response)
);
/* No error, continue with login */
if (string_is_empty(error_response))
{
int res = cheevos_get_value(
coro->json,
CHEEVOS_JSON_KEY_TOKEN,
cheevos_locals.token,
sizeof(cheevos_locals.token));
if ((void*)coro->json)
free((void*)coro->json);
if (!res)
{
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, 3 * 60, false);
}
/* Save token to config and clear pass on success */
*coro->settings->arrays.cheevos_password = '\0';
strncpy(
coro->settings->arrays.cheevos_token,
cheevos_locals.token, sizeof(cheevos_locals.token)
);
CORO_RET();
}
}
if ((void*)coro->json)
free((void*)coro->json);
/* Site returned error, display it */
snprintf(error_message, sizeof(error_message),
"RetroAchievements: %s",
error_response);
error_message[sizeof(error_message) - 1] = 0;
runloop_msg_queue_push(error_message, 0, 5 * 60, false);
*coro->settings->arrays.cheevos_token = '\0';
CORO_STOP();
}
runloop_msg_queue_push("RetroAchievements: Error contacting server.", 0, 5 * 60, false);
CHEEVOS_ERR("[CHEEVOS]: error getting user token.\n");
CORO_STOP();
/**************************************************************************
* Info Pauses execution for five seconds
*************************************************************************/
CORO_SUB(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(HTTP_GET)
for (coro->k = 0; coro->k < 5; coro->k++)
{
if (coro->k != 0)
CHEEVOS_LOG("[CHEEVOS]: 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(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;
}
coro->http = net_http_new(coro->conn);
/* Error connecting to the endpoint. */
if (!coro->http)
{
net_http_connection_free(coro->conn);
CORO_GOSUB(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);
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("[CHEEVOS]: Couldn't connect to server after 5 tries\n");
CORO_RET();
/**************************************************************************
* Info Deactivates the achievements already awarded
* Inputs CHEEVOS_VAR_GAMEID
* Outputs
*************************************************************************/
CORO_SUB(DEACTIVATE)
#ifndef CHEEVOS_DONT_DEACTIVATE
CORO_GOSUB(LOGIN);
/* Deactivate achievements in softcore mode. */
snprintf(
coro->url, sizeof(coro->url),
"http://retroachievements.org/dorequest.php?r=unlocks&u=%s&t=%s&g=%u&h=0",
coro->settings->arrays.cheevos_username,
cheevos_locals.token, coro->gameid
);
coro->url[sizeof(coro->url) - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to get the list of unlocked cheevos in softcore: %s\n", coro->url);
#endif
CORO_GOSUB(HTTP_GET);
if (coro->json)
{
if (!cheevos_deactivate_unlocks(coro->json, CHEEVOS_ACTIVE_SOFTCORE))
CHEEVOS_LOG("[CHEEVOS]: deactivated unlocked achievements in softcore mode.\n");
else
CHEEVOS_ERR("[CHEEVOS]: error deactivating unlocked achievements in softcore mode.\n");
if ((void*)coro->json)
free((void*)coro->json);
}
else
CHEEVOS_ERR("[CHEEVOS]: error retrieving list of unlocked achievements in softcore mode.\n");
/* Deactivate achievements in hardcore mode. */
snprintf(
coro->url, sizeof(coro->url),
"http://retroachievements.org/dorequest.php?r=unlocks&u=%s&t=%s&g=%u&h=1",
coro->settings->arrays.cheevos_username,
cheevos_locals.token, coro->gameid
);
coro->url[sizeof(coro->url) - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to get the list of unlocked cheevos in hardcore: %s\n", coro->url);
#endif
CORO_GOSUB(HTTP_GET);
if (coro->json)
{
if (!cheevos_deactivate_unlocks(coro->json, CHEEVOS_ACTIVE_HARDCORE))
CHEEVOS_LOG("[CHEEVOS]: deactivated unlocked achievements in hardcore mode.\n");
else
CHEEVOS_ERR("[CHEEVOS]: error deactivating unlocked achievements in hardcore mode.\n");
if ((void*)coro->json)
free((void*)coro->json);
}
else
CHEEVOS_ERR("[CHEEVOS]: error retrieving list of unlocked achievements in hardcore mode.\n");
#endif
CORO_RET();
/**************************************************************************
* Info Posts the "playing" activity to Retro Achievements
* Inputs CHEEVOS_VAR_GAMEID
* Outputs
*************************************************************************/
CORO_SUB(PLAYING)
snprintf(
coro->url, sizeof(coro->url),
"http://retroachievements.org/dorequest.php?r=postactivity&u=%s&t=%s&a=3&m=%u",
coro->settings->arrays.cheevos_username,
cheevos_locals.token, coro->gameid
);
coro->url[sizeof(coro->url) - 1] = 0;
#ifdef CHEEVOS_LOG_URLS
cheevos_log_url("[CHEEVOS]: url to post the 'playing' activity: %s\n", coro->url);
#endif
CORO_GOSUB(HTTP_GET);
if (coro->json)
{
CHEEVOS_LOG("[CHEEVOS]: posted playing activity.\n");
if ((void*)coro->json)
free((void*)coro->json);
}
else
CHEEVOS_ERR("[CHEEVOS]: error posting playing activity.\n");
CHEEVOS_LOG("[CHEEVOS]: posted playing activity.\n");
CORO_RET();
CORO_LEAVE();
}
static void cheevos_task_handler(retro_task_t *task)
{
coro_t *coro = (coro_t*)task->state;
if (!coro)
return;
if (!cheevos_iterate(coro) || task_get_cancelled(task))
{
task_set_finished(task, true);
CHEEVOS_LOCK(cheevos_locals.task_lock);
cheevos_locals.task = NULL;
CHEEVOS_UNLOCK(cheevos_locals.task_lock);
if (task_get_cancelled(task))
{
CHEEVOS_LOG("[CHEEVOS]: Load task cancelled\n");
}
else
{
CHEEVOS_LOG("[CHEEVOS]: Load task finished\n");
}
if (coro->data)
free(coro->data);
if ((void*)coro->path)
free((void*)coro->path);
free((void*)coro);
}
}
bool cheevos_load(const void *data)
{
retro_task_t *task;
const struct retro_game_info *info = NULL;
coro_t *coro = NULL;
cheevos_loaded = false;
if (!cheevos_locals.core_supports || !data)
return false;
coro = (coro_t*)calloc(1, sizeof(*coro));
if (!coro)
return false;
task = (retro_task_t*)calloc(1, sizeof(*task));
if (!task)
{
if ((void*)coro)
free((void*)coro);
return false;
}
CORO_SETUP();
info = (const struct retro_game_info*)data;
if (info->data)
{
coro->len = info->size;
/* size limit */
if (coro->len > size_in_megabytes(64))
coro->len = size_in_megabytes(64);
coro->data = malloc(coro->len);
if (!coro->data)
{
if ((void*)task)
free((void*)task);
if ((void*)coro)
free((void*)coro);
return false;
}
memcpy(coro->data, info->data, coro->len);
coro->path = NULL;
}
else
{
coro->data = NULL;
coro->path = strdup(info->path);
}
task->handler = cheevos_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 (cheevos_locals.task_lock == NULL)
{
cheevos_locals.task_lock = slock_new();
}
#endif
CHEEVOS_LOCK(cheevos_locals.task_lock);
cheevos_locals.task = task;
CHEEVOS_UNLOCK(cheevos_locals.task_lock);
task_queue_push(task);
return true;
}