mirror of
https://github.com/CTCaer/RetroArch.git
synced 2024-12-05 01:16:38 +00:00
1384 lines
31 KiB
C
1384 lines
31 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2015 - 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 <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
#include <configuration.h>
|
|
#include <formats/jsonsax.h>
|
|
#include <net/net_http.h>
|
|
#include <rhash.h>
|
|
#include <performance.h>
|
|
#include <runloop.h>
|
|
#include <retro_log.h>
|
|
|
|
#include "cheevos.h"
|
|
#include "dynamic.h"
|
|
#include "net_http_special.h"
|
|
|
|
enum
|
|
{
|
|
CHEEVOS_VAR_SIZE_BIT_0,
|
|
CHEEVOS_VAR_SIZE_BIT_1,
|
|
CHEEVOS_VAR_SIZE_BIT_2,
|
|
CHEEVOS_VAR_SIZE_BIT_3,
|
|
CHEEVOS_VAR_SIZE_BIT_4,
|
|
CHEEVOS_VAR_SIZE_BIT_5,
|
|
CHEEVOS_VAR_SIZE_BIT_6,
|
|
CHEEVOS_VAR_SIZE_BIT_7,
|
|
CHEEVOS_VAR_SIZE_NIBBLE_LOWER,
|
|
CHEEVOS_VAR_SIZE_NIBBLE_UPPER,
|
|
/* Byte, */
|
|
CHEEVOS_VAR_SIZE_EIGHT_BITS, /* =Byte, */
|
|
CHEEVOS_VAR_SIZE_SIXTEEN_BITS,
|
|
CHEEVOS_VAR_SIZE_THIRTYTWO_BITS,
|
|
|
|
CHEEVOS_VAR_SIZE_LAST
|
|
}; /* cheevos_var_t.size */
|
|
|
|
enum
|
|
{
|
|
CHEEVOS_VAR_TYPE_ADDRESS, /* compare to the value of a live address in RAM */
|
|
CHEEVOS_VAR_TYPE_VALUE_COMP, /* a number. assume 32 bit */
|
|
CHEEVOS_VAR_TYPE_DELTA_MEM, /* the value last known at this address. */
|
|
CHEEVOS_VAR_TYPE_DYNAMIC_VAR, /* a custom user-set variable */
|
|
|
|
CHEEVOS_VAR_TYPE_LAST
|
|
}; /* cheevos_var_t.type */
|
|
|
|
enum
|
|
{
|
|
CHEEVOS_COND_OP_EQUALS,
|
|
CHEEVOS_COND_OP_LESS_THAN,
|
|
CHEEVOS_COND_OP_LESS_THAN_OR_EQUAL,
|
|
CHEEVOS_COND_OP_GREATER_THAN,
|
|
CHEEVOS_COND_OP_GREATER_THAN_OR_EQUAL,
|
|
CHEEVOS_COND_OP_NOT_EQUAL_TO,
|
|
|
|
CHEEVOS_COND_OP_LAST
|
|
}; /* cheevos_cond_t.op */
|
|
|
|
enum
|
|
{
|
|
CHEEVOS_COND_TYPE_STANDARD,
|
|
CHEEVOS_COND_TYPE_PAUSE_IF,
|
|
CHEEVOS_COND_TYPE_RESET_IF,
|
|
|
|
CHEEVOS_COND_TYPE_LAST
|
|
}; /* cheevos_cond_t.type */
|
|
|
|
enum
|
|
{
|
|
CHEEVOS_DIRTY_TITLE = 1 << 0,
|
|
CHEEVOS_DIRTY_DESC = 1 << 1,
|
|
CHEEVOS_DIRTY_POINTS = 1 << 2,
|
|
CHEEVOS_DIRTY_AUTHOR = 1 << 3,
|
|
CHEEVOS_DIRTY_ID = 1 << 4,
|
|
CHEEVOS_DIRTY_BADGE = 1 << 5,
|
|
CHEEVOS_DIRTY_CONDITIONS = 1 << 6,
|
|
CHEEVOS_DIRTY_VOTES = 1 << 7,
|
|
CHEEVOS_DIRTY_DESCRIPTION = 1 << 8,
|
|
|
|
CHEEVOS_DIRTY_ALL = ( 1 << 9 ) - 1
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
unsigned size;
|
|
unsigned type;
|
|
unsigned bank_id;
|
|
unsigned value;
|
|
unsigned previous;
|
|
}
|
|
cheevos_var_t;
|
|
|
|
typedef struct
|
|
{
|
|
unsigned type;
|
|
unsigned req_hits;
|
|
unsigned curr_hits;
|
|
|
|
cheevos_var_t source;
|
|
unsigned op;
|
|
cheevos_var_t target;
|
|
}
|
|
cheevos_cond_t;
|
|
|
|
typedef struct
|
|
{
|
|
cheevos_cond_t* conds;
|
|
unsigned count;
|
|
|
|
const char* expression;
|
|
}
|
|
cheevos_condset_t;
|
|
|
|
typedef struct
|
|
{
|
|
unsigned id;
|
|
const char* title;
|
|
const char* description;
|
|
const char* author;
|
|
const char* badge;
|
|
unsigned points;
|
|
unsigned dirty;
|
|
int active;
|
|
int modified;
|
|
|
|
cheevos_condset_t* condsets;
|
|
unsigned count;
|
|
}
|
|
cheevo_t;
|
|
|
|
typedef struct
|
|
{
|
|
cheevo_t* cheevos;
|
|
unsigned count;
|
|
}
|
|
cheevoset_t;
|
|
|
|
static cheevoset_t core_cheevos = { NULL, 0 };
|
|
static cheevoset_t unofficial_cheevos = { NULL, 0 };
|
|
|
|
static char token[ 32 ] = { 0 };
|
|
|
|
/*****************************************************************************
|
|
Supporting functions.
|
|
*****************************************************************************/
|
|
|
|
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;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Count number of achievements in a JSON file.
|
|
*****************************************************************************/
|
|
|
|
typedef struct
|
|
{
|
|
int in_cheevos;
|
|
uint32_t field_hash;
|
|
unsigned core_count;
|
|
unsigned unofficial_count;
|
|
}
|
|
cheevos_countud_t;
|
|
|
|
static int count__json_end_array( void* userdata )
|
|
{
|
|
cheevos_countud_t* ud = (cheevos_countud_t*)userdata;
|
|
ud->in_cheevos = 0;
|
|
return 0;
|
|
}
|
|
|
|
static int count__json_key( void* userdata, const char* name, size_t length )
|
|
{
|
|
cheevos_countud_t* ud = (cheevos_countud_t*)userdata;
|
|
ud->field_hash = cheevos_djb2( name, length );
|
|
|
|
if ( ud->field_hash == 0x69749ae1U /* Achievements */ )
|
|
{
|
|
ud->in_cheevos = 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int count__json_number( void* userdata, const char* number, size_t length )
|
|
{
|
|
cheevos_countud_t* ud = (cheevos_countud_t*)userdata;
|
|
|
|
if ( ud->in_cheevos && ud->field_hash == 0x0d2e96b2U /* Flags */ )
|
|
{
|
|
long flags = strtol( number, NULL, 10 );
|
|
|
|
if ( flags == 3 ) /* core achievements */
|
|
{
|
|
ud->core_count++;
|
|
}
|
|
else if ( flags == 5 ) /* unofficial achievements */
|
|
{
|
|
ud->unofficial_count++;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int count_cheevos( const char* json, unsigned* core_count, unsigned* unofficial_count )
|
|
{
|
|
static const jsonsax_handlers_t handlers =
|
|
{
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
count__json_end_array,
|
|
count__json_key,
|
|
NULL,
|
|
NULL,
|
|
count__json_number,
|
|
NULL,
|
|
NULL
|
|
};
|
|
|
|
int res;
|
|
cheevos_countud_t ud;
|
|
ud.in_cheevos = 0;
|
|
ud.core_count = 0;
|
|
ud.unofficial_count = 0;
|
|
|
|
res = jsonsax_parse( json, &handlers, (void*)&ud );
|
|
|
|
*core_count = ud.core_count;
|
|
*unofficial_count = ud.unofficial_count;
|
|
|
|
return res;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Parse the MemAddr field.
|
|
*****************************************************************************/
|
|
|
|
static unsigned prefix_to_comp_size( char prefix )
|
|
{
|
|
/* Careful not to use ABCDEF here, this denotes part of an actual variable! */
|
|
|
|
switch( toupper( prefix ) )
|
|
{
|
|
case 'M': return CHEEVOS_VAR_SIZE_BIT_0;
|
|
case 'N': return CHEEVOS_VAR_SIZE_BIT_1;
|
|
case 'O': return CHEEVOS_VAR_SIZE_BIT_2;
|
|
case 'P': return CHEEVOS_VAR_SIZE_BIT_3;
|
|
case 'Q': return CHEEVOS_VAR_SIZE_BIT_4;
|
|
case 'R': return CHEEVOS_VAR_SIZE_BIT_5;
|
|
case 'S': return CHEEVOS_VAR_SIZE_BIT_6;
|
|
case 'T': return CHEEVOS_VAR_SIZE_BIT_7;
|
|
case 'L': return CHEEVOS_VAR_SIZE_NIBBLE_LOWER;
|
|
case 'U': return CHEEVOS_VAR_SIZE_NIBBLE_UPPER;
|
|
case 'H': return CHEEVOS_VAR_SIZE_EIGHT_BITS;
|
|
case 'X': return CHEEVOS_VAR_SIZE_THIRTYTWO_BITS;
|
|
default:
|
|
case ' ': return CHEEVOS_VAR_SIZE_SIXTEEN_BITS;
|
|
}
|
|
}
|
|
|
|
static unsigned read_hits( const char** memaddr )
|
|
{
|
|
const char* str = *memaddr;
|
|
char* end;
|
|
unsigned num_hits = 0;
|
|
|
|
if ( *str == '(' || *str == '.' )
|
|
{
|
|
num_hits = strtol( str + 1, &end, 10 );
|
|
str = end + 1;
|
|
}
|
|
|
|
*memaddr = str;
|
|
return num_hits;
|
|
}
|
|
|
|
static unsigned parse_operator( const char** memaddr )
|
|
{
|
|
const char* str = *memaddr;
|
|
unsigned char op;
|
|
|
|
if ( *str == '=' && str[ 1 ] == '=' )
|
|
{
|
|
op = CHEEVOS_COND_OP_EQUALS;
|
|
str += 2;
|
|
}
|
|
else if ( *str == '=' )
|
|
{
|
|
op = CHEEVOS_COND_OP_EQUALS;
|
|
str++;
|
|
}
|
|
else if ( *str == '!' && str[ 1 ] == '=' )
|
|
{
|
|
op = CHEEVOS_COND_OP_NOT_EQUAL_TO;
|
|
str += 2;
|
|
}
|
|
else if ( *str == '<' && str[ 1 ] == '=' )
|
|
{
|
|
op = CHEEVOS_COND_OP_LESS_THAN_OR_EQUAL;
|
|
str += 2;
|
|
}
|
|
else if ( *str == '<' )
|
|
{
|
|
op = CHEEVOS_COND_OP_LESS_THAN;
|
|
str++;
|
|
}
|
|
else if ( *str == '>' && str[ 1 ] == '=' )
|
|
{
|
|
op = CHEEVOS_COND_OP_GREATER_THAN_OR_EQUAL;
|
|
str += 2;
|
|
}
|
|
else if ( *str == '>' )
|
|
{
|
|
op = CHEEVOS_COND_OP_GREATER_THAN;
|
|
str++;
|
|
}
|
|
else
|
|
{
|
|
/* TODO log the exception */
|
|
op = CHEEVOS_COND_OP_EQUALS;
|
|
}
|
|
|
|
*memaddr = str;
|
|
return op;
|
|
}
|
|
|
|
static void parse_var( cheevos_var_t* var, const char** memaddr )
|
|
{
|
|
const char* str = *memaddr;
|
|
char* end;
|
|
unsigned base = 16;
|
|
|
|
if ( toupper( *str ) == 'D' && str[ 1 ] == '0' && toupper( str[ 2 ] ) == 'X' )
|
|
{
|
|
/* d0x + 4 hex digits */
|
|
str += 3;
|
|
var->type = CHEEVOS_VAR_TYPE_DELTA_MEM;
|
|
}
|
|
else if ( *str == '0' && toupper( str[ 1 ] ) == 'X' )
|
|
{
|
|
/* 0x + 4 hex digits */
|
|
str += 2;
|
|
var->type = CHEEVOS_VAR_TYPE_ADDRESS;
|
|
}
|
|
else
|
|
{
|
|
var->type = CHEEVOS_VAR_TYPE_VALUE_COMP;
|
|
|
|
if ( toupper( *str ) == 'H' )
|
|
{
|
|
str++;
|
|
}
|
|
else
|
|
{
|
|
base = 10;
|
|
}
|
|
}
|
|
|
|
if ( var->type != CHEEVOS_VAR_TYPE_VALUE_COMP )
|
|
{
|
|
var->size = prefix_to_comp_size( *str );
|
|
|
|
if ( var->size != CHEEVOS_VAR_SIZE_SIXTEEN_BITS )
|
|
{
|
|
str++;
|
|
}
|
|
}
|
|
|
|
var->value = strtol( str, &end, base );
|
|
*memaddr = end;
|
|
}
|
|
|
|
static void parse_cond( cheevos_cond_t* cond, const char** memaddr )
|
|
{
|
|
const char* str = *memaddr;
|
|
|
|
if ( *str == 'R' && str[ 1 ] == ':' )
|
|
{
|
|
cond->type = CHEEVOS_COND_TYPE_RESET_IF;
|
|
str += 2;
|
|
}
|
|
else if ( *str == 'P' && str[ 1 ] == ':' )
|
|
{
|
|
cond->type = CHEEVOS_COND_TYPE_PAUSE_IF;
|
|
str += 2;
|
|
}
|
|
else
|
|
{
|
|
cond->type = CHEEVOS_COND_TYPE_STANDARD;
|
|
}
|
|
|
|
parse_var( &cond->source, &str );
|
|
cond->op = parse_operator( &str );
|
|
parse_var( &cond->target, &str );
|
|
cond->curr_hits = 0;
|
|
cond->req_hits = read_hits( &str );
|
|
|
|
*memaddr = str;
|
|
}
|
|
|
|
static unsigned count_cond_sets( const char* memaddr )
|
|
{
|
|
unsigned count = 0;
|
|
cheevos_cond_t cond;
|
|
|
|
do
|
|
{
|
|
do
|
|
{
|
|
while( *memaddr == ' ' || *memaddr == '_' || *memaddr == '|' || *memaddr == 'S' )
|
|
{
|
|
memaddr++; /* Skip any chars up til the start of the achievement condition */
|
|
}
|
|
|
|
parse_cond( &cond, &memaddr );
|
|
}
|
|
while( *memaddr == '_' || *memaddr == 'R' || *memaddr == 'P' ); /* AND, ResetIf, PauseIf */
|
|
|
|
count++;
|
|
}
|
|
while( *memaddr == 'S' ); /* Repeat for all subconditions if they exist */
|
|
|
|
return count;
|
|
}
|
|
|
|
static unsigned count_conds_in_set( const char* memaddr, unsigned set )
|
|
{
|
|
unsigned index = 0;
|
|
unsigned count = 0;
|
|
cheevos_cond_t cond;
|
|
|
|
do
|
|
{
|
|
do
|
|
{
|
|
while ( *memaddr == ' ' || *memaddr == '_' || *memaddr == '|' || *memaddr == 'S' )
|
|
{
|
|
memaddr++; /* Skip any chars up til the start of the achievement condition */
|
|
}
|
|
|
|
parse_cond( &cond, &memaddr );
|
|
|
|
if ( index == set )
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
while ( *memaddr == '_' || *memaddr == 'R' || *memaddr == 'P' ); /* AND, ResetIf, PauseIf */
|
|
}
|
|
while( *memaddr == 'S' ); /* Repeat for all subconditions if they exist */
|
|
|
|
return count;
|
|
}
|
|
|
|
static void parse_memaddr( cheevos_cond_t* cond, const char* memaddr )
|
|
{
|
|
do
|
|
{
|
|
do
|
|
{
|
|
while( *memaddr == ' ' || *memaddr == '_' || *memaddr == '|' || *memaddr == 'S' )
|
|
{
|
|
memaddr++; /* Skip any chars up til the start of the achievement condition */
|
|
}
|
|
|
|
parse_cond( cond++, &memaddr );
|
|
}
|
|
while( *memaddr == '_' || *memaddr == 'R' || *memaddr == 'P' ); /* AND, ResetIf, PauseIf */
|
|
}
|
|
while( *memaddr == 'S' ); /* Repeat for all subconditions if they exist */
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Load achievements from a JSON string.
|
|
*****************************************************************************/
|
|
|
|
typedef struct
|
|
{
|
|
const char* string;
|
|
size_t length;
|
|
}
|
|
cheevos_field_t;
|
|
|
|
typedef struct
|
|
{
|
|
int in_cheevos;
|
|
unsigned core_count;
|
|
unsigned unofficial_count;
|
|
|
|
cheevos_field_t* field;
|
|
cheevos_field_t id, memaddr, title, desc, points, author;
|
|
cheevos_field_t modified, created, badge, flags;
|
|
}
|
|
cheevos_readud_t;
|
|
|
|
static INLINE const char* dupstr( const cheevos_field_t* field )
|
|
{
|
|
char* string = (char*)malloc( field->length + 1 );
|
|
|
|
if ( string )
|
|
{
|
|
memcpy( (void*)string, (void*)field->string, field->length );
|
|
string[ field->length ] = 0;
|
|
}
|
|
|
|
return string;
|
|
}
|
|
|
|
static int new_cheevo( cheevos_readud_t* ud )
|
|
{
|
|
const cheevos_condset_t* end;
|
|
unsigned set;
|
|
cheevos_condset_t* condset;
|
|
cheevo_t* cheevo;
|
|
int flags = strtol( ud->flags.string, NULL, 10 );
|
|
|
|
if ( flags == 3 )
|
|
{
|
|
cheevo = core_cheevos.cheevos + ud->core_count++;
|
|
}
|
|
else
|
|
{
|
|
cheevo = unofficial_cheevos.cheevos + ud->unofficial_count++;
|
|
}
|
|
|
|
cheevo->id = strtol( ud->id.string, NULL, 10 );
|
|
cheevo->title = dupstr( &ud->title );
|
|
cheevo->description = dupstr( &ud->desc );
|
|
cheevo->author = dupstr( &ud->author );
|
|
cheevo->badge = dupstr( &ud->badge );
|
|
cheevo->points = strtol( ud->points.string, NULL, 10 );
|
|
cheevo->dirty = 0;
|
|
cheevo->active = 1; /* flags == 3; */
|
|
cheevo->modified = 0;
|
|
|
|
if ( !cheevo->title || !cheevo->description || !cheevo->author || !cheevo->badge )
|
|
{
|
|
free( (void*)cheevo->title );
|
|
free( (void*)cheevo->description );
|
|
free( (void*)cheevo->author );
|
|
free( (void*)cheevo->badge );
|
|
return -1;
|
|
}
|
|
|
|
cheevo->count = count_cond_sets( ud->memaddr.string );
|
|
|
|
if ( cheevo->count )
|
|
{
|
|
cheevo->condsets = (cheevos_condset_t*)malloc( cheevo->count * sizeof( cheevos_condset_t ) );
|
|
|
|
if ( !cheevo->condsets )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
memset( (void*)cheevo->condsets, 0, cheevo->count * sizeof( cheevos_condset_t ) );
|
|
end = cheevo->condsets + cheevo->count;
|
|
set = 0;
|
|
|
|
for ( condset = cheevo->condsets; condset < end; condset++ )
|
|
{
|
|
condset->count = count_conds_in_set( ud->memaddr.string, set++ );
|
|
|
|
if ( condset->count )
|
|
{
|
|
condset->conds = (cheevos_cond_t*)malloc( condset->count * sizeof( cheevos_cond_t ) );
|
|
|
|
if ( !condset->conds )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
memset( (void*)condset->conds, 0, condset->count * sizeof( cheevos_cond_t ) );
|
|
condset->expression = dupstr( &ud->memaddr );
|
|
parse_memaddr( condset->conds, ud->memaddr.string );
|
|
}
|
|
else
|
|
{
|
|
condset->conds = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read__json_key( void* userdata, const char* name, size_t length )
|
|
{
|
|
cheevos_readud_t* ud = (cheevos_readud_t*)userdata;
|
|
uint32_t hash = cheevos_djb2( name, length );
|
|
|
|
ud->field = NULL;
|
|
|
|
if ( hash == 0x69749ae1U /* Achievements */ )
|
|
{
|
|
ud->in_cheevos = 1;
|
|
}
|
|
else if ( ud->in_cheevos )
|
|
{
|
|
switch ( hash )
|
|
{
|
|
case 0x005973f2U: /* ID */ ud->field = &ud->id; break;
|
|
case 0x1e76b53fU: /* MemAddr */ ud->field = &ud->memaddr; break;
|
|
case 0x0e2a9a07U: /* Title */ ud->field = &ud->title; break;
|
|
case 0xe61a1f69U: /* Description */ ud->field = &ud->desc; break;
|
|
case 0xca8fce22U: /* Points */ ud->field = &ud->points; break;
|
|
case 0xa804edb8U: /* Author */ ud->field = &ud->author; break;
|
|
case 0xdcea4fe6U: /* Modified */ ud->field = &ud->modified; break;
|
|
case 0x3a84721dU: /* Created */ ud->field = &ud->created; break;
|
|
case 0x887685d9U: /* BadgeName */ ud->field = &ud->badge; break;
|
|
case 0x0d2e96b2U: /* Flags */ ud->field = &ud->flags; break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read__json_string( void* userdata, const char* string, size_t length )
|
|
{
|
|
cheevos_readud_t* ud = (cheevos_readud_t*)userdata;
|
|
|
|
if ( ud->field )
|
|
{
|
|
ud->field->string = string;
|
|
ud->field->length = length;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read__json_number( void* userdata, const char* number, size_t length )
|
|
{
|
|
cheevos_readud_t* ud = (cheevos_readud_t*)userdata;
|
|
|
|
if ( ud->field )
|
|
{
|
|
ud->field->string = number;
|
|
ud->field->length = length;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read__json_end_object( void* userdata )
|
|
{
|
|
cheevos_readud_t* ud = (cheevos_readud_t*)userdata;
|
|
|
|
if ( ud->in_cheevos )
|
|
{
|
|
return new_cheevo( ud );
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read__json_end_array( void* userdata )
|
|
{
|
|
cheevos_readud_t* ud = (cheevos_readud_t*)userdata;
|
|
ud->in_cheevos = 0;
|
|
return 0;
|
|
}
|
|
|
|
int cheevos_load( const char* json )
|
|
{
|
|
static const jsonsax_handlers_t handlers =
|
|
{
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
read__json_end_object,
|
|
NULL,
|
|
read__json_end_array,
|
|
read__json_key,
|
|
NULL,
|
|
read__json_string,
|
|
read__json_number,
|
|
NULL,
|
|
NULL
|
|
};
|
|
|
|
if ( !config_get_ptr()->cheevos.enable )
|
|
{
|
|
/* Just return OK if cheevos are disabled. */
|
|
return 0;
|
|
}
|
|
|
|
/* Count the number of achievements in the JSON file. */
|
|
|
|
unsigned core_count, unofficial_count;
|
|
cheevos_readud_t ud;
|
|
|
|
if ( count_cheevos( json, &core_count, &unofficial_count ) != JSONSAX_OK )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
/* Allocate the achievements. */
|
|
|
|
core_cheevos.cheevos = (cheevo_t*)malloc( core_count * sizeof( cheevo_t ) );
|
|
core_cheevos.count = core_count;
|
|
|
|
unofficial_cheevos.cheevos = (cheevo_t*)malloc( unofficial_count * sizeof( cheevo_t ) );
|
|
unofficial_cheevos.count = unofficial_count;
|
|
|
|
if ( !core_cheevos.cheevos || !unofficial_cheevos.cheevos )
|
|
{
|
|
free( (void*)core_cheevos.cheevos );
|
|
free( (void*)unofficial_cheevos.cheevos );
|
|
core_cheevos.count = unofficial_cheevos.count = 0;
|
|
|
|
return -1;
|
|
}
|
|
|
|
memset( (void*)core_cheevos.cheevos, 0, core_count * sizeof( cheevo_t ) );
|
|
memset( (void*)unofficial_cheevos.cheevos, 0, unofficial_count * sizeof( cheevo_t ) );
|
|
|
|
/* Load the achievements. */
|
|
|
|
ud.in_cheevos = 0;
|
|
ud.field = NULL;
|
|
ud.core_count = 0;
|
|
ud.unofficial_count = 0;
|
|
|
|
if ( !jsonsax_parse( json, &handlers, (void*)&ud ) == JSONSAX_OK )
|
|
{
|
|
cheevos_unload();
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Test all the achievements (call once per frame).
|
|
*****************************************************************************/
|
|
|
|
static const uint8_t* get_memory( unsigned offset )
|
|
{
|
|
uint8_t* memory;
|
|
size_t size = core.retro_get_memory_size( RETRO_MEMORY_SYSTEM_RAM );
|
|
|
|
if ( offset < size )
|
|
{
|
|
memory = (uint8_t*)core.retro_get_memory_data( RETRO_MEMORY_SYSTEM_RAM );
|
|
return memory + offset;
|
|
}
|
|
|
|
offset -= size;
|
|
size = core.retro_get_memory_size( RETRO_MEMORY_SAVE_RAM );
|
|
|
|
if ( offset < size )
|
|
{
|
|
memory = (uint8_t*)core.retro_get_memory_data( RETRO_MEMORY_SAVE_RAM );
|
|
return memory + offset;
|
|
}
|
|
|
|
offset -= size;
|
|
size = core.retro_get_memory_size( RETRO_MEMORY_VIDEO_RAM );
|
|
|
|
if ( offset < size )
|
|
{
|
|
memory = (uint8_t*)core.retro_get_memory_data( RETRO_MEMORY_VIDEO_RAM );
|
|
return memory + offset;
|
|
}
|
|
|
|
offset -= size;
|
|
size = core.retro_get_memory_size( RETRO_MEMORY_RTC );
|
|
|
|
if ( offset < size )
|
|
{
|
|
memory = (uint8_t*)core.retro_get_memory_data( RETRO_MEMORY_RTC );
|
|
return memory + offset;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static unsigned get_var_value( cheevos_var_t* var )
|
|
{
|
|
unsigned previous = var->previous;
|
|
unsigned live_val = 0;
|
|
const uint8_t* memory;
|
|
|
|
if ( var->type == CHEEVOS_VAR_TYPE_VALUE_COMP )
|
|
{
|
|
return var->value;
|
|
}
|
|
|
|
if ( var->type == CHEEVOS_VAR_TYPE_ADDRESS || var->type == CHEEVOS_VAR_TYPE_DELTA_MEM )
|
|
{
|
|
/* TODO Check with Scott if the bank id is needed */
|
|
memory = get_memory( var->value );
|
|
live_val = memory[ 0 ];
|
|
|
|
if ( var->size >= CHEEVOS_VAR_SIZE_BIT_0 && var->size <= CHEEVOS_VAR_SIZE_BIT_7 )
|
|
{
|
|
live_val = ( live_val & ( 1 << ( var->size - CHEEVOS_VAR_SIZE_BIT_0 ) ) ) != 0;
|
|
}
|
|
else if ( var->size == CHEEVOS_VAR_SIZE_NIBBLE_LOWER )
|
|
{
|
|
live_val &= 0x0f;
|
|
}
|
|
else if ( var->size == CHEEVOS_VAR_SIZE_NIBBLE_UPPER )
|
|
{
|
|
live_val = ( live_val >> 4 ) & 0x0f;
|
|
}
|
|
else if ( var->size == CHEEVOS_VAR_SIZE_EIGHT_BITS )
|
|
{
|
|
/* nothing */
|
|
}
|
|
else if ( var->size == CHEEVOS_VAR_SIZE_SIXTEEN_BITS )
|
|
{
|
|
live_val |= memory[ 1 ] << 8;
|
|
}
|
|
else if ( var->size == CHEEVOS_VAR_SIZE_THIRTYTWO_BITS )
|
|
{
|
|
live_val |= memory[ 1 ] << 8;
|
|
live_val |= memory[ 2 ] << 16;
|
|
live_val |= memory[ 3 ] << 24;
|
|
}
|
|
|
|
if ( var->type == CHEEVOS_VAR_TYPE_DELTA_MEM )
|
|
{
|
|
var->previous = live_val;
|
|
return previous;
|
|
}
|
|
|
|
return live_val;
|
|
}
|
|
|
|
/* We shouldn't get here... */
|
|
return 0;
|
|
}
|
|
|
|
static int test_condition( cheevos_cond_t* cond )
|
|
{
|
|
unsigned sval = get_var_value( &cond->source );
|
|
unsigned tval = get_var_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:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static int 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;
|
|
int pause_active = 0;
|
|
const cheevos_cond_t* end = condset->conds + condset->count;
|
|
cheevos_cond_t* cond;
|
|
|
|
(void)pause_active;
|
|
|
|
/* 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 )
|
|
{
|
|
/* Reset by default, set to 1 if hit! */
|
|
cond->curr_hits = 0;
|
|
|
|
if ( 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->req_hits != 0 && cond->curr_hits >= cond->req_hits )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
cond_valid = 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 < cond->req_hits )
|
|
{
|
|
/* Not entirely valid yet! */
|
|
cond_valid = 0;
|
|
}
|
|
|
|
if ( match_any )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* 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 )
|
|
{
|
|
cond_valid = 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 reset_cond_set( cheevos_condset_t* condset, int deltas )
|
|
{
|
|
int dirty = 0;
|
|
const cheevos_cond_t* end = condset->conds + condset->count;
|
|
cheevos_cond_t* cond;
|
|
|
|
if ( deltas )
|
|
{
|
|
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
|
|
{
|
|
for ( cond = condset->conds; cond < end; cond++ )
|
|
{
|
|
dirty |= cond->curr_hits != 0;
|
|
cond->curr_hits = 0;
|
|
}
|
|
}
|
|
|
|
return dirty;
|
|
}
|
|
|
|
static int test_cheevo( cheevo_t* cheevo )
|
|
{
|
|
int dirty_conds = 0;
|
|
int reset_conds = 0;
|
|
int ret_val = 0;
|
|
int dirty;
|
|
int ret_val_sub_cond = cheevo->count == 1;
|
|
cheevos_condset_t* condset = cheevo->condsets;
|
|
const cheevos_condset_t* end = condset + cheevo->count;
|
|
|
|
if ( condset < end )
|
|
{
|
|
ret_val = test_cond_set( condset, &dirty_conds, &reset_conds, 0 );
|
|
if ( ret_val ) RARCH_LOG( "CHEEVOS %s\n", condset->expression );
|
|
condset++;
|
|
}
|
|
|
|
while ( condset < end )
|
|
{
|
|
int res = test_cond_set( condset, &dirty_conds, &reset_conds, 0 );
|
|
ret_val_sub_cond |= res;
|
|
if ( res ) RARCH_LOG( "CHEEVOS %s\n", condset->expression );
|
|
condset++;
|
|
}
|
|
|
|
if ( dirty_conds )
|
|
{
|
|
cheevo->dirty |= CHEEVOS_DIRTY_CONDITIONS;
|
|
}
|
|
|
|
if ( reset_conds )
|
|
{
|
|
dirty = 0;
|
|
|
|
for ( condset = cheevo->condsets; condset < end; condset++ )
|
|
{
|
|
dirty |= reset_cond_set( condset, 0 );
|
|
}
|
|
|
|
if ( dirty )
|
|
{
|
|
cheevo->dirty |= CHEEVOS_DIRTY_CONDITIONS;
|
|
}
|
|
}
|
|
|
|
return ret_val && ret_val_sub_cond;
|
|
}
|
|
|
|
static void test_cheevo_set( const cheevoset_t* set )
|
|
{
|
|
const cheevo_t* end = set->cheevos + set->count;
|
|
cheevo_t* cheevo;
|
|
|
|
for ( cheevo = set->cheevos; cheevo < end; cheevo++ )
|
|
{
|
|
if ( cheevo->active && test_cheevo( cheevo ) )
|
|
{
|
|
RARCH_LOG( "CHEEVOS %s\n", cheevo->title );
|
|
RARCH_LOG( "CHEEVOS %s\n", cheevo->description );
|
|
|
|
rarch_main_msg_queue_push( cheevo->title, 0, 3 * 60, false );
|
|
rarch_main_msg_queue_push( cheevo->description, 0, 5 * 60, false );
|
|
|
|
cheevo->active = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void cheevos_test( void )
|
|
{
|
|
if ( config_get_ptr()->cheevos.enable )
|
|
{
|
|
test_cheevo_set( &core_cheevos );
|
|
|
|
if ( config_get_ptr()->cheevos.test_unofficial )
|
|
{
|
|
test_cheevo_set( &unofficial_cheevos );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Free the loaded achievements.
|
|
*****************************************************************************/
|
|
|
|
static void free_condset( const cheevos_condset_t* set )
|
|
{
|
|
free( (void*)set->conds );
|
|
}
|
|
|
|
static void free_cheevo( const cheevo_t* cheevo )
|
|
{
|
|
free( (void*)cheevo->title );
|
|
free( (void*)cheevo->description );
|
|
free( (void*)cheevo->author );
|
|
free( (void*)cheevo->badge );
|
|
free_condset( cheevo->condsets );
|
|
}
|
|
|
|
static void free_cheevo_set( const cheevoset_t* set )
|
|
{
|
|
const cheevo_t* cheevo = set->cheevos;
|
|
const cheevo_t* end = cheevo + set->count;
|
|
|
|
while ( cheevo < end )
|
|
{
|
|
free_cheevo( cheevo++ );
|
|
}
|
|
|
|
free( (void*)set->cheevos );
|
|
}
|
|
|
|
void cheevos_unload( void )
|
|
{
|
|
free_cheevo_set( &core_cheevos );
|
|
free_cheevo_set( &unofficial_cheevos );
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Load achievements from retroachievements.org.
|
|
*****************************************************************************/
|
|
|
|
typedef struct
|
|
{
|
|
unsigned key_hash;
|
|
int is_key;
|
|
const char* value;
|
|
size_t length;
|
|
}
|
|
cheevo_getvalueud_t;
|
|
|
|
static int getvalue__json_key( void* userdata, const char* name, size_t length )
|
|
{
|
|
cheevo_getvalueud_t* ud = (cheevo_getvalueud_t*)userdata;
|
|
|
|
ud->is_key = cheevos_djb2( name, length ) == ud->key_hash;
|
|
return 0;
|
|
}
|
|
|
|
static int getvalue__json_string( void* userdata, const char* string, size_t length )
|
|
{
|
|
cheevo_getvalueud_t* ud = (cheevo_getvalueud_t*)userdata;
|
|
|
|
if ( ud->is_key )
|
|
{
|
|
ud->value = string;
|
|
ud->length = length;
|
|
ud->is_key = 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int getvalue__json_boolean( void* userdata, int istrue )
|
|
{
|
|
cheevo_getvalueud_t* ud = (cheevo_getvalueud_t*)userdata;
|
|
|
|
if ( ud->is_key )
|
|
{
|
|
ud->value = istrue ? "true" : "false";
|
|
ud->length = istrue ? 4 : 5;
|
|
ud->is_key = 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int getvalue__json_null( void* userdata )
|
|
{
|
|
cheevo_getvalueud_t* ud = (cheevo_getvalueud_t*)userdata;
|
|
|
|
if ( 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,
|
|
getvalue__json_key,
|
|
NULL,
|
|
getvalue__json_string,
|
|
getvalue__json_string, /* number */
|
|
getvalue__json_boolean,
|
|
getvalue__json_null
|
|
};
|
|
|
|
cheevo_getvalueud_t ud;
|
|
|
|
ud.key_hash = key_hash;
|
|
ud.is_key = 0;
|
|
ud.length = 0;
|
|
*value = 0;
|
|
|
|
if ( jsonsax_parse( json, &handlers, (void*)&ud ) == JSONSAX_OK && ud.length < length )
|
|
{
|
|
strncpy( value, ud.value, length );
|
|
value[ ud.length ] = 0;
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int cheevos_login( retro_time_t* timeout )
|
|
{
|
|
char request[ 256 ];
|
|
const char* json;
|
|
int res;
|
|
|
|
if ( !token[ 0 ] )
|
|
{
|
|
snprintf(
|
|
request, sizeof( request ),
|
|
"http://retroachievements.org/dorequest.php?r=login&u=%s&p=%s",
|
|
config_get_ptr()->cheevos.username, config_get_ptr()->cheevos.password
|
|
);
|
|
|
|
request[ sizeof( request ) - 1 ] = 0;
|
|
|
|
if ( !net_http_get( &json, NULL, request, timeout ) )
|
|
{
|
|
res = cheevos_get_value( json, 0x0e2dbd26U /* Token */, token, sizeof( token ) );
|
|
free( (void*)json );
|
|
|
|
if ( !res )
|
|
{
|
|
RARCH_LOG( "CHEEVOS user token is '%s'\n", token );
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
RARCH_LOG( "CHEEVOS error getting user token\n" );
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int cheevos_get_by_game_id( const char** json, unsigned game_id, retro_time_t* timeout )
|
|
{
|
|
char request[ 256 ];
|
|
|
|
if ( !config_get_ptr()->cheevos.enable )
|
|
{
|
|
/* Just return OK if cheevos are disabled. */
|
|
return 0;
|
|
}
|
|
|
|
if ( !cheevos_login( timeout ) )
|
|
{
|
|
snprintf(
|
|
request, sizeof( request ),
|
|
"http://retroachievements.org/dorequest.php?r=patch&u=%s&g=%u&f=3&l=1&t=%s",
|
|
config_get_ptr()->cheevos.username, game_id, token
|
|
);
|
|
|
|
request[ sizeof( request ) - 1 ] = 0;
|
|
|
|
if ( !net_http_get( json, NULL, request, timeout ) )
|
|
{
|
|
RARCH_LOG( "CHEEVOS got achievements for game id %u\n", game_id );
|
|
return 0;
|
|
}
|
|
|
|
RARCH_LOG( "CHEEVOS error getting achievements for game id %u\n", game_id );
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static unsigned cheevos_get_game_id( unsigned char* hash, retro_time_t* timeout )
|
|
{
|
|
char request[ 256 ];
|
|
const char* json;
|
|
char game_id[ 16 ];
|
|
int res;
|
|
|
|
snprintf(
|
|
request, sizeof( request ),
|
|
"http://retroachievements.org/dorequest.php?r=gameid&u=%s&m=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
|
config_get_ptr()->cheevos.username,
|
|
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 ]
|
|
);
|
|
|
|
request[ sizeof( request ) - 1 ] = 0;
|
|
|
|
if ( !net_http_get( &json, NULL, request, timeout ) )
|
|
{
|
|
res = cheevos_get_value( json, 0xb4960eecU /* GameID */, game_id, sizeof( game_id ) );
|
|
free( (void*)json );
|
|
|
|
if ( !res )
|
|
{
|
|
RARCH_LOG( "CHEEVOS got game id %s\n", game_id );
|
|
return strtoul( game_id, NULL, 10 );
|
|
}
|
|
}
|
|
|
|
RARCH_LOG( "CHEEVOS error getting game_id\n" );
|
|
return 0;
|
|
}
|
|
|
|
#define CHEEVOS_EIGHT_MB ( 8 * 1024 * 1024 )
|
|
|
|
int cheevos_get_by_content( const char** json, const void* data, size_t size )
|
|
{
|
|
MD5_CTX ctx, saved_ctx;
|
|
retro_time_t timeout;
|
|
char buffer[ 4096 ];
|
|
size_t len;
|
|
unsigned char hash[ 16 ];
|
|
char request[ 256 ];
|
|
unsigned game_id;
|
|
int res;
|
|
|
|
(void)request;
|
|
(void)res;
|
|
|
|
if ( !config_get_ptr()->cheevos.enable )
|
|
{
|
|
/* Just return OK if cheevos are disabled. */
|
|
return 0;
|
|
}
|
|
|
|
MD5_Init( &ctx );
|
|
MD5_Update( &ctx, data, size );
|
|
saved_ctx = ctx;
|
|
MD5_Final( hash, &ctx );
|
|
|
|
timeout = 15000000;
|
|
game_id = cheevos_get_game_id( hash, &timeout );
|
|
|
|
if ( !game_id && size < CHEEVOS_EIGHT_MB )
|
|
{
|
|
/* Maybe the content is a SNES game, continue MD5 with zeroes up to 8 MB. */
|
|
size = CHEEVOS_EIGHT_MB - size;
|
|
memset( (void*)buffer, 0, sizeof( buffer ) );
|
|
|
|
do
|
|
{
|
|
len = sizeof( buffer );
|
|
|
|
if ( len > size )
|
|
{
|
|
len = size;
|
|
}
|
|
|
|
MD5_Update( &saved_ctx, (void*)buffer, len );
|
|
size -= len;
|
|
}
|
|
while ( size );
|
|
|
|
MD5_Final( hash, &saved_ctx );
|
|
|
|
game_id = cheevos_get_game_id( hash, &timeout );
|
|
}
|
|
|
|
return game_id ? cheevos_get_by_game_id( json, game_id, &timeout ) : -1;
|
|
}
|