/* 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 . */ #include #include #include #include #include #include #include #include #include #include #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 }; unsigned core_count, unofficial_count; cheevos_readud_t ud; if ( !config_get_ptr()->cheevos.enable ) { /* Just return OK if cheevos are disabled. */ return 0; } /* Count the number of achievements in the JSON file. */ 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; }