diff --git a/Makefile.common b/Makefile.common index 8b339f3218e7..2165cd4ed996 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2101,12 +2101,24 @@ ifeq ($(HAVE_NETWORKING), 1) DEFINES += -DHAVE_CHEEVOS INCLUDE_DIRS += -Ideps/rcheevos/include + ifneq ($(HAVE_THREADS), 1) + DEFINES += -DRC_NO_THREADS + else ifneq (,$(filter GEKKO,$(CFLAGS))) + # Gekko (Wii) and 3DS use custom pthread wrappers (see rthreads.c) + DEFINES += -DRC_NO_THREADS + else ifneq (,$(filter _3DS,$(CFLAGS))) + DEFINES += -DRC_NO_THREADS + endif + OBJ += cheevos/cheevos.o \ cheevos/cheevos_client.o \ cheevos/cheevos_menu.o \ $(LIBRETRO_COMM_DIR)/formats/cdfs/cdfs.o \ + deps/rcheevos/src/rc_client.o \ + deps/rcheevos/src/rc_compat.o \ + deps/rcheevos/src/rc_libretro.o \ + deps/rcheevos/src/rc_util.o \ deps/rcheevos/src/rcheevos/alloc.o \ - deps/rcheevos/src/rcheevos/compat.o \ deps/rcheevos/src/rcheevos/condition.o \ deps/rcheevos/src/rcheevos/condset.o \ deps/rcheevos/src/rcheevos/consoleinfo.o \ @@ -2114,7 +2126,6 @@ ifeq ($(HAVE_NETWORKING), 1) deps/rcheevos/src/rcheevos/lboard.o \ deps/rcheevos/src/rcheevos/memref.o \ deps/rcheevos/src/rcheevos/operand.o \ - deps/rcheevos/src/rcheevos/rc_libretro.o \ deps/rcheevos/src/rcheevos/richpresence.o \ deps/rcheevos/src/rcheevos/runtime.o \ deps/rcheevos/src/rcheevos/runtime_progress.o \ @@ -2123,6 +2134,7 @@ ifeq ($(HAVE_NETWORKING), 1) deps/rcheevos/src/rhash/cdreader.o \ deps/rcheevos/src/rhash/hash.o \ deps/rcheevos/src/rapi/rc_api_common.o \ + deps/rcheevos/src/rapi/rc_api_info.o \ deps/rcheevos/src/rapi/rc_api_runtime.o \ deps/rcheevos/src/rapi/rc_api_user.o \ diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index db08fb874672..4ab541553d76 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -75,7 +75,7 @@ #include "../deps/rcheevos/include/rc_runtime.h" #include "../deps/rcheevos/include/rc_runtime_types.h" #include "../deps/rcheevos/include/rc_hash.h" -#include "../deps/rcheevos/src/rcheevos/rc_libretro.h" +#include "../deps/rcheevos/src/rc_libretro.h" /* Define this macro to prevent cheevos from being deactivated when they trigger. */ #undef CHEEVOS_DONT_DEACTIVATE @@ -163,7 +163,7 @@ static void rcheevos_handle_log_message(const char* message) CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message); } -static void rcheevos_get_core_memory_info(unsigned id, +static void rcheevos_get_core_memory_info(uint32_t id, rc_libretro_core_memory_info_t* info) { retro_ctx_memory_info_t ctx_info; @@ -220,10 +220,10 @@ uint8_t* rcheevos_patch_address(unsigned address) return rc_libretro_memory_find(&rcheevos_locals.memory, address); } -static unsigned rcheevos_peek(unsigned address, - unsigned num_bytes, void* ud) +static uint32_t rcheevos_peek(uint32_t address, + uint32_t num_bytes, void* ud) { - unsigned avail; + uint32_t avail; uint8_t* data = rc_libretro_memory_find_avail( &rcheevos_locals.memory, address, &avail); @@ -1321,7 +1321,7 @@ static void rcheevos_runtime_event_handler( } } -static int rcheevos_runtime_address_validator(unsigned address) +static int rcheevos_runtime_address_validator(uint32_t address) { return rc_libretro_memory_find( &rcheevos_locals.memory, address) != NULL; @@ -2066,7 +2066,7 @@ static void rcheevos_identify_game_callback(void* userdata) rcheevos_fetch_game_data(); } -static int rcheevos_get_image_path(unsigned index, char* buffer, size_t buffer_size) +static int rcheevos_get_image_path(uint32_t index, char* buffer, size_t buffer_size) { rarch_system_info_t *sys_info = &runloop_state_get_ptr()->system; if (!sys_info->disk_control.cb.get_image_path) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 576979403b7e..d02d0f1dca7f 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -1756,7 +1756,7 @@ static void rcheevos_async_award_achievement_callback( { if ((int)api_response.awarded_achievement_id != request->id) snprintf(buffer, buffer_size, "Achievement %u awarded instead", - api_response.awarded_achievement_id); + (unsigned)api_response.awarded_achievement_id); else if (api_response.response.error_message) { /* previously unlocked achievements are returned as a "successful" error */ diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 3ca72ef073f6..97cefb7f75c4 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -18,7 +18,7 @@ #define __RARCH_CHEEVOS_LOCALS_H #include "../deps/rcheevos/include/rc_runtime.h" -#include "../deps/rcheevos/src/rcheevos/rc_libretro.h" +#include "../deps/rcheevos/src/rc_libretro.h" #include #include diff --git a/deps/rcheevos/CHANGELOG.md b/deps/rcheevos/CHANGELOG.md index 150f90e12b14..9cced4c5d231 100644 --- a/deps/rcheevos/CHANGELOG.md +++ b/deps/rcheevos/CHANGELOG.md @@ -1,3 +1,25 @@ +# v11.0.0 +* add rc_client_t and related functions +* add RC_MEMSIZE_FLOAT_BE +* add Game Pak SRAM to GBA memory map +* add hash method for Super Cassettevision +* add PSP to potential consoles for chd iterator +* add content_type to rc_api_request_t for client to pass to server +* add rc_api_process_X_server_response methods to pass status_code and body_length to response processing functions +* add additional error codes to rc_api_process_login_response: RC_INVALID_CREDENTIALS, RC_EXPIRED_TOKEN, RC_ACCESS_DENIED +* rc_api_start_session now also returns unlocks without having to explicitly call rc_api_fetch_user_unlocks separately +* add validation warning for using hit target of 1 on ResetIf condition +* move compat.c up a directory and rename to rc_compat.c as it's shared by all subfolders +* move rc_libretro.c up a directory as it uses files from all subfolders +* convert loosely sized types to strongly sized types (unsigned -> uint32t, unsigned char -> uint8_t, etc) + +# v10.7.1 +* add rc_runtime_alloc +* add rc_libretro_memory_find_avail +* extract nginx errors from HTML returned for JSON endpoints +* fix real address for 32X extension RAM +* fix crash attempting to calculate gamecube hash for non-existent file + # v10.7.0 * add hash method and memory map for Gamecube * add console enum, hash method, and memory map for DSi diff --git a/deps/rcheevos/README.md b/deps/rcheevos/README.md index 7148206eccc6..b0b1a8c602ee 100644 --- a/deps/rcheevos/README.md +++ b/deps/rcheevos/README.md @@ -6,13 +6,11 @@ Keep in mind that **rcheevos** does *not* provide HTTP network connections. Clie Not all structures defined by **rcheevos** can be created via the public API, but are exposed to allow interactions beyond just creation, destruction, and testing, such as the ones required by UI code that helps to create them. -Finally, **rcheevos** does *not* allocate or manage memory by itself. All structures that can be returned by it have a function to determine the number of bytes needed to hold the structure, and another one that actually builds the structure using a caller-provided buffer to bake it. - ## Lua -RetroAchievements is considering the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. The current expression-based implementation is often limiting on newer systems. +RetroAchievements previously considered the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. -At this point, to enable Lua support, you must compile with an additional compilation flag: `HAVE_LUA`, as neither the backend nor the UI for editing achievements are currently Lua-enabled. +To enable Lua support, you must compile with an additional compilation flag: `HAVE_LUA`, as neither the backend nor the UI for editing achievements are currently Lua-enabled. We do not foresee enabling it any time soon, but the code has not yet been completely eliminated as many of the low-level API fuctions have parameters for LUA data. > **rcheevos** does *not* create or maintain a Lua state, you have to create your own state and provide it to **rcheevos** to be used when Lua-coded achievements are found. Calls to **rcheevos** may allocate and/or free additional memory as part of the Lua runtime. @@ -28,49 +26,9 @@ An understanding about how achievements are developed may be useful, you can rea Most of the exposed APIs are documented [here](https://github.com/RetroAchievements/rcheevos/wiki) -### User Configuration - -There's only one thing that can be configured by users of **rcheevos**: `RC_ALIGNMENT`. This macro holds the alignment of allocations made in the buffer provided to the parsing functions, and the default value is `sizeof(void*)`. - -If your platform will benefit from a different value, define a new value for it on your compiler flags before compiling the code. It has to be a power of 2, but no checking is done. - ### Return values -Any function in the rcheevos library that returns a success indicator will return one of the following values. - -These are in `rc_error.h`. - -```c -enum { - RC_OK = 0, - RC_INVALID_LUA_OPERAND = -1, - RC_INVALID_MEMORY_OPERAND = -2, - RC_INVALID_CONST_OPERAND = -3, - RC_INVALID_FP_OPERAND = -4, - RC_INVALID_CONDITION_TYPE = -5, - RC_INVALID_OPERATOR = -6, - RC_INVALID_REQUIRED_HITS = -7, - RC_DUPLICATED_START = -8, - RC_DUPLICATED_CANCEL = -9, - RC_DUPLICATED_SUBMIT = -10, - RC_DUPLICATED_VALUE = -11, - RC_DUPLICATED_PROGRESS = -12, - RC_MISSING_START = -13, - RC_MISSING_CANCEL = -14, - RC_MISSING_SUBMIT = -15, - RC_MISSING_VALUE = -16, - RC_INVALID_LBOARD_FIELD = -17, - RC_MISSING_DISPLAY_STRING = -18, - RC_OUT_OF_MEMORY = -19, - RC_INVALID_VALUE_FLAG = -20, - RC_MISSING_VALUE_MEASURED = -21, - RC_MULTIPLE_MEASURED = -22, - RC_INVALID_MEASURED_TARGET = -23, - RC_INVALID_COMPARISON = -24, - RC_INVALID_STATE = -25, - RC_INVALID_JSON = -26 -}; -``` +Any function in the rcheevos library that returns a success indicator will return `RC_OK` or one of the constants defined in `rc_error.h`. To convert the return code into something human-readable, pass it to: ```c @@ -79,189 +37,17 @@ const char* rc_error_str(int ret); ### Console identifiers -This enumeration uniquely identifies each of the supported platforms in RetroAchievements. - -These are in `rc_consoles.h`. - -```c -enum { - RC_CONSOLE_MEGA_DRIVE = 1, - RC_CONSOLE_NINTENDO_64 = 2, - RC_CONSOLE_SUPER_NINTENDO = 3, - RC_CONSOLE_GAMEBOY = 4, - RC_CONSOLE_GAMEBOY_ADVANCE = 5, - RC_CONSOLE_GAMEBOY_COLOR = 6, - RC_CONSOLE_NINTENDO = 7, - RC_CONSOLE_PC_ENGINE = 8, - RC_CONSOLE_SEGA_CD = 9, - RC_CONSOLE_SEGA_32X = 10, - RC_CONSOLE_MASTER_SYSTEM = 11, - RC_CONSOLE_PLAYSTATION = 12, - RC_CONSOLE_ATARI_LYNX = 13, - RC_CONSOLE_NEOGEO_POCKET = 14, - RC_CONSOLE_GAME_GEAR = 15, - RC_CONSOLE_GAMECUBE = 16, - RC_CONSOLE_ATARI_JAGUAR = 17, - RC_CONSOLE_NINTENDO_DS = 18, - RC_CONSOLE_WII = 19, - RC_CONSOLE_WII_U = 20, - RC_CONSOLE_PLAYSTATION_2 = 21, - RC_CONSOLE_XBOX = 22, - RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, - RC_CONSOLE_POKEMON_MINI = 24, - RC_CONSOLE_ATARI_2600 = 25, - RC_CONSOLE_MS_DOS = 26, - RC_CONSOLE_ARCADE = 27, - RC_CONSOLE_VIRTUAL_BOY = 28, - RC_CONSOLE_MSX = 29, - RC_CONSOLE_COMMODORE_64 = 30, - RC_CONSOLE_ZX81 = 31, - RC_CONSOLE_ORIC = 32, - RC_CONSOLE_SG1000 = 33, - RC_CONSOLE_VIC20 = 34, - RC_CONSOLE_AMIGA = 35, - RC_CONSOLE_ATARI_ST = 36, - RC_CONSOLE_AMSTRAD_PC = 37, - RC_CONSOLE_APPLE_II = 38, - RC_CONSOLE_SATURN = 39, - RC_CONSOLE_DREAMCAST = 40, - RC_CONSOLE_PSP = 41, - RC_CONSOLE_CDI = 42, - RC_CONSOLE_3DO = 43, - RC_CONSOLE_COLECOVISION = 44, - RC_CONSOLE_INTELLIVISION = 45, - RC_CONSOLE_VECTREX = 46, - RC_CONSOLE_PC8800 = 47, - RC_CONSOLE_PC9800 = 48, - RC_CONSOLE_PCFX = 49, - RC_CONSOLE_ATARI_5200 = 50, - RC_CONSOLE_ATARI_7800 = 51, - RC_CONSOLE_X68K = 52, - RC_CONSOLE_WONDERSWAN = 53, - RC_CONSOLE_CASSETTEVISION = 54, - RC_CONSOLE_SUPER_CASSETTEVISION = 55, - RC_CONSOLE_NEO_GEO_CD = 56, - RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, - RC_CONSOLE_FM_TOWNS = 58, - RC_CONSOLE_ZX_SPECTRUM = 59, - RC_CONSOLE_GAME_AND_WATCH = 60, - RC_CONSOLE_NOKIA_NGAGE = 61, - RC_CONSOLE_NINTENDO_3DS = 62, - RC_CONSOLE_SUPERVISION = 63, - RC_CONSOLE_SHARPX1 = 64, - RC_CONSOLE_TIC80 = 65, - RC_CONSOLE_THOMSONTO8 = 66 -}; -``` +Platforms supported by RetroAchievements are enumerated in `rc_consoles.h`. Note that some consoles in the enum are not yet fully supported (may require a memory map or some way to uniquely identify games). ## Runtime support -The runtime encapsulates a set of achievements, leaderboards, and rich presence for a game and manages processing them for each frame. When important things occur, events are raised for the caller via a callback. +Provides a set of functions for managing an active game - initializing and processing achievements, leaderboards, and rich presence. When important things occur, events are raised for the caller via a callback. These are in `rc_runtime.h`. -The `rc_runtime_t` structure uses several forward-defines. If you need access to the actual contents of any of the forward-defined structures, those definitions are in `rc_runtime_types.h` +Note: `rc_runtime_t` still requires the client implement all of the logic that calls the APIs to retrieve the data and perform the unlocks. -```c -typedef struct rc_runtime_t { - rc_runtime_trigger_t* triggers; - unsigned trigger_count; - unsigned trigger_capacity; - - rc_runtime_lboard_t* lboards; - unsigned lboard_count; - unsigned lboard_capacity; - - rc_runtime_richpresence_t* richpresence; - - rc_memref_value_t* memrefs; - rc_memref_value_t** next_memref; - - rc_value_t* variables; - rc_value_t** next_variable; -} -rc_runtime_t; -``` - -The runtime must first be initialized. -```c -void rc_runtime_init(rc_runtime_t* runtime); -``` - -Then individual achievements, leaderboards, and even rich presence can be loaded into the runtime. These functions return RC_OK, or one of the negative value error codes listed above. -```c -int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); -``` - -The runtime should be called once per frame to evaluate the state of the active achievements/leaderboards: -```c -void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L); -``` - -The `event_handler` is a callback function that is called for each event that occurs when processing the frame. -```c -typedef struct rc_runtime_event_t { - unsigned id; - int value; - char type; -} -rc_runtime_event_t; - -typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); -``` - -The `event.type` field will be one of the following: -* RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED (id=achievement id) - An achievement starts in the RC_TRIGGER_STATE_WAITING state and cannot trigger until it has been false for at least one frame. This event indicates the achievement is no longer waiting and may trigger on a future frame. -* RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED (id=achievement id) - One or more conditions in the achievement have disabled the achievement. -* RC_RUNTIME_EVENT_ACHIEVEMENT_RESET (id=achievement id) - One or more conditions in the achievement have reset any progress captured in the achievement. -* RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED (id=achievement id) - All conditions for the achievement have been met and the user should be informed. - NOTE: If `rc_runtime_reset` is called without deactivating the achievement, it may trigger again. -* RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED (id=achievement id) - All non-trigger conditions for the achievement have been met. This typically indicates the achievement is a challenge achievement and the challenge is active. -* RC_RUNTIME_EVENT_LBOARD_STARTED (id=leaderboard id, value=leaderboard value) - The leaderboard's start condition has been met and the user should be informed that a leaderboard attempt has started. -* RC_RUNTIME_EVENT_LBOARD_CANCELED (id=leaderboard id, value=leaderboard value) - The leaderboard's cancel condition has been met and the user should be informed that a leaderboard attempt has failed. -* RC_RUNTIME_EVENT_LBOARD_UPDATED (id=leaderboard id, value=leaderboard value) - The leaderboard value has changed. -* RC_RUNTIME_EVENT_LBOARD_TRIGGERED (id=leaderboard id, value=leaderboard value) - The leaderboard's submit condition has been met and the user should be informed that a leaderboard attempt was successful. The value should be submitted. -* RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED (id=achievement id) - The achievement has been disabled by a call to `rc_invalidate_address`. -* RC_RUNTIME_EVENT_LBOARD_DISABLED (id=leaderboard id) - The achievement has been disabled by a call to `rc_invalidate_address`. - -When an achievement triggers, it should be deactivated so it won't trigger again: -```c -void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); -``` -Additionally, the unlock should be submitted to the server. - -When a leaderboard triggers, it should not be deactivated in case the player wants to try again for a better score. The value should be submitted to the server. - -For `RC_RUNTIME_EVENT_LBOARD_UPDATED` and `RC_RUNTIME_EVENT_LBOARD_TRIGGERED` events, there is a helper function to call if you wish to display the leaderboard value on screen. - -```c -int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format); -``` - -`rc_runtime_do_frame` also periodically updates the rich presense string (every 60 frames). To get the current value, call -```c -const char* rc_runtime_get_richpresence(const rc_runtime_t* runtime); -``` - -When the game is reset, the runtime should also be reset: -```c -void rc_runtime_reset(rc_runtime_t* runtime); -``` - -This ensures any active achievements/leaderboards are set back to their initial states and prevents unexpected triggers when the memory changes in atypical way. +The `rc_client_t` functions wrap a `rc_runtime_t` and manage the API calls and other common functionality (like managing the user information, identifying/loading a game, and building the active/inactive achievements list for the UI). Please see [the wiki](https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration) for details on using the `rc_client_t` functions. ## Server Communication @@ -271,6 +57,8 @@ This ensures any active achievements/leaderboards are set back to their initial NOTE: **rapi** is a replacement for **rurl**. **rurl** has been deprecated. +NOTE: `rc_client` is the preferred way to have a client interact with the server. + These are in `rc_api_user.h`, `rc_api_runtime.h` and `rc_api_common.h`. The basic process of making an **rapi** call is to initialize a params object, call a function to convert it to a URL, send that to the server, then pass the response to a function to convert it into a response object, and handle the response values. @@ -291,6 +79,3 @@ These are in `rc_hash.h`. int rc_hash_generate_from_buffer(char hash[33], int console_id, uint8_t* buffer, size_t buffer_size); int rc_hash_generate_from_file(char hash[33], int console_id, const char* path); ``` - - - diff --git a/deps/rcheevos/_config.yml b/deps/rcheevos/_config.yml deleted file mode 100644 index 18854876c67f..000000000000 --- a/deps/rcheevos/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-midnight \ No newline at end of file diff --git a/deps/rcheevos/include/rc_api_info.h b/deps/rcheevos/include/rc_api_info.h index 7979cc391f56..a9586eeb9a23 100644 --- a/deps/rcheevos/include/rc_api_info.h +++ b/deps/rcheevos/include/rc_api_info.h @@ -3,6 +3,7 @@ #include "rc_api_request.h" +#include #include #ifdef __cplusplus @@ -20,13 +21,13 @@ typedef struct rc_api_fetch_achievement_info_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the achievement */ - unsigned achievement_id; + uint32_t achievement_id; /* The 1-based index of the first entry to retrieve */ - unsigned first_entry; + uint32_t first_entry; /* The number of entries to retrieve */ - unsigned count; + uint32_t count; /* Non-zero to only return unlocks earned by the user's friends */ - unsigned friends_only; + uint32_t friends_only; } rc_api_fetch_achievement_info_request_t; @@ -44,18 +45,18 @@ rc_api_achievement_awarded_entry_t; */ typedef struct rc_api_fetch_achievement_info_response_t { /* The unique identifier of the achievement */ - unsigned id; + uint32_t id; /* The unique identifier of the game to which the leaderboard is associated */ - unsigned game_id; + uint32_t game_id; /* The number of times the achievement has been awarded */ - unsigned num_awarded; + uint32_t num_awarded; /* The number of players that have earned at least one achievement for the game */ - unsigned num_players; + uint32_t num_players; /* An array of recently rewarded entries */ rc_api_achievement_awarded_entry_t* recently_awarded; /* The number of items in the recently_awarded array */ - unsigned num_recently_awarded; + uint32_t num_recently_awarded; /* Common server-provided response information */ rc_api_response_t response; @@ -64,6 +65,7 @@ rc_api_fetch_achievement_info_response_t; int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); /* --- Fetch Leaderboard Info --- */ @@ -73,11 +75,11 @@ void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_inf */ typedef struct rc_api_fetch_leaderboard_info_request_t { /* The unique identifier of the leaderboard */ - unsigned leaderboard_id; + uint32_t leaderboard_id; /* The number of entries to retrieve */ - unsigned count; + uint32_t count; /* The 1-based index of the first entry to retrieve */ - unsigned first_entry; + uint32_t first_entry; /* The username of the player around whom the entries should be returned */ const char* username; } @@ -88,11 +90,11 @@ typedef struct rc_api_lboard_info_entry_t { /* The user associated to the entry */ const char* username; /* The rank of the entry */ - unsigned rank; + uint32_t rank; /* The index of the entry */ - unsigned index; + uint32_t index; /* The value of the entry */ - int score; + int32_t score; /* When the entry was submitted */ time_t submitted; } @@ -103,11 +105,11 @@ rc_api_lboard_info_entry_t; */ typedef struct rc_api_fetch_leaderboard_info_response_t { /* The unique identifier of the leaderboard */ - unsigned id; + uint32_t id; /* The format to pass to rc_format_value to format the leaderboard value */ int format; /* If non-zero, indicates that lower scores appear first */ - int lower_is_better; + uint32_t lower_is_better; /* The title of the leaderboard */ const char* title; /* The description of the leaderboard */ @@ -115,7 +117,7 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ const char* definition; /* The unique identifier of the game to which the leaderboard is associated */ - unsigned game_id; + uint32_t game_id; /* The author of the leaderboard */ const char* author; /* When the leaderboard was first uploaded to the server */ @@ -126,7 +128,7 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { /* An array of requested entries */ rc_api_lboard_info_entry_t* entries; /* The number of items in the entries array */ - unsigned num_entries; + uint32_t num_entries; /* Common server-provided response information */ rc_api_response_t response; @@ -135,6 +137,7 @@ rc_api_fetch_leaderboard_info_response_t; int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); /* --- Fetch Games List --- */ @@ -144,14 +147,14 @@ void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_inf */ typedef struct rc_api_fetch_games_list_request_t { /* The unique identifier of the console to query */ - unsigned console_id; + uint32_t console_id; } rc_api_fetch_games_list_request_t; /* A game list entry */ typedef struct rc_api_game_list_entry_t { /* The unique identifier of the game */ - unsigned id; + uint32_t id; /* The name of the game */ const char* name; } @@ -164,7 +167,7 @@ typedef struct rc_api_fetch_games_list_response_t { /* An array of requested entries */ rc_api_game_list_entry_t* entries; /* The number of items in the entries array */ - unsigned num_entries; + uint32_t num_entries; /* Common server-provided response information */ rc_api_response_t response; @@ -173,6 +176,7 @@ rc_api_fetch_games_list_response_t; int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_api_request.h b/deps/rcheevos/include/rc_api_request.h index 64f25ec12abe..578488f3dbdc 100644 --- a/deps/rcheevos/include/rc_api_request.h +++ b/deps/rcheevos/include/rc_api_request.h @@ -2,37 +2,14 @@ #define RC_API_REQUEST_H #include "rc_error.h" +#include "../src/rc_util.h" + +#include #ifdef __cplusplus extern "C" { #endif -/** - * A block of memory for variable length data (like strings and arrays). - */ -typedef struct rc_api_buffer_chunk_t { - /* The current location where data is being written */ - char* write; - /* The first byte past the end of data where writing cannot occur */ - char* end; - /* The first byte of the data */ - char* start; - /* The next block in the allocated memory chain */ - struct rc_api_buffer_chunk_t* next; -} -rc_api_buffer_chunk_t; - -/** - * A preallocated block of memory for variable length data (like strings and arrays). - */ -typedef struct rc_api_buffer_t { - /* The chunk data (will point at the local data member) */ - struct rc_api_buffer_chunk_t chunk; - /* Small chunk of memory pre-allocated for the chunk */ - char data[256]; -} -rc_api_buffer_t; - /** * A constructed request to send to the retroachievements server. */ @@ -41,9 +18,11 @@ typedef struct rc_api_request_t { const char* url; /* Additional query args that should be sent via a POST command. If null, GET may be used */ const char* post_data; + /* The HTTP Content-Type of the POST data. */ + const char* content_type; /* Storage for the url and post_data */ - rc_api_buffer_t buffer; + rc_buffer_t buffer; } rc_api_request_t; @@ -55,9 +34,11 @@ typedef struct rc_api_response_t { int succeeded; /* Server-provided message associated to the failure */ const char* error_message; + /* Server-provided error code associated to the failure */ + const char* error_code; /* Storage for the response data */ - rc_api_buffer_t buffer; + rc_buffer_t buffer; } rc_api_response_t; @@ -66,6 +47,20 @@ void rc_api_destroy_request(rc_api_request_t* request); void rc_api_set_host(const char* hostname); void rc_api_set_image_host(const char* hostname); +typedef struct rc_api_server_response_t { + /* Pointer to the data returned from the server */ + const char* body; + /* Length of data returned from the server (Content-Length) */ + size_t body_length; + /* HTTP status code returned from the server */ + int http_status_code; +} rc_api_server_response_t; + +enum { + RC_API_SERVER_RESPONSE_CLIENT_ERROR = -1, + RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR = -2 +}; + #ifdef __cplusplus } #endif diff --git a/deps/rcheevos/include/rc_api_runtime.h b/deps/rcheevos/include/rc_api_runtime.h index 68f56fd5129a..e6d72a20d6be 100644 --- a/deps/rcheevos/include/rc_api_runtime.h +++ b/deps/rcheevos/include/rc_api_runtime.h @@ -3,6 +3,7 @@ #include "rc_api_request.h" +#include #include #ifdef __cplusplus @@ -19,7 +20,7 @@ typedef struct rc_api_fetch_image_request_t { /* The name of the image to fetch */ const char* image_name; /* The type of image to fetch */ - int image_type; + uint32_t image_type; } rc_api_fetch_image_request_t; @@ -50,7 +51,7 @@ rc_api_resolve_hash_request_t; */ typedef struct rc_api_resolve_hash_response_t { /* The unique identifier of the game, 0 if no match was found */ - unsigned game_id; + uint32_t game_id; /* Common server-provided response information */ rc_api_response_t response; @@ -59,6 +60,7 @@ rc_api_resolve_hash_response_t; int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); /* --- Fetch Game Data --- */ @@ -72,14 +74,14 @@ typedef struct rc_api_fetch_game_data_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; } rc_api_fetch_game_data_request_t; /* A leaderboard definition */ typedef struct rc_api_leaderboard_definition_t { /* The unique identifier of the leaderboard */ - unsigned id; + uint32_t id; /* The format to pass to rc_format_value to format the leaderboard value */ int format; /* The title of the leaderboard */ @@ -89,20 +91,20 @@ typedef struct rc_api_leaderboard_definition_t { /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ const char* definition; /* Non-zero if lower values are better for this leaderboard */ - int lower_is_better; + uint8_t lower_is_better; /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ - int hidden; + uint8_t hidden; } rc_api_leaderboard_definition_t; /* An achievement definition */ typedef struct rc_api_achievement_definition_t { /* The unique identifier of the achievement */ - unsigned id; + uint32_t id; /* The number of points the achievement is worth */ - unsigned points; + uint32_t points; /* The achievement category (core, unofficial) */ - unsigned category; + uint32_t category; /* The title of the achievement */ const char* title; /* The dscription of the achievement */ @@ -128,9 +130,9 @@ rc_api_achievement_definition_t; */ typedef struct rc_api_fetch_game_data_response_t { /* The unique identifier of the game */ - unsigned id; + uint32_t id; /* The console associated to the game */ - unsigned console_id; + uint32_t console_id; /* The title of the game */ const char* title; /* The image name for the game badge */ @@ -141,12 +143,12 @@ typedef struct rc_api_fetch_game_data_response_t { /* An array of achievements for the game */ rc_api_achievement_definition_t* achievements; /* The number of items in the achievements array */ - unsigned num_achievements; + uint32_t num_achievements; /* An array of leaderboards for the game */ rc_api_leaderboard_definition_t* leaderboards; /* The number of items in the leaderboards array */ - unsigned num_leaderboards; + uint32_t num_leaderboards; /* Common server-provided response information */ rc_api_response_t response; @@ -155,6 +157,7 @@ rc_api_fetch_game_data_response_t; int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); /* --- Ping --- */ @@ -168,7 +171,7 @@ typedef struct rc_api_ping_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; /* (optional) The current rich presence evaluation for the user */ const char* rich_presence; } @@ -185,6 +188,7 @@ rc_api_ping_response_t; int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_ping_response(rc_api_ping_response_t* response); /* --- Award Achievement --- */ @@ -198,9 +202,9 @@ typedef struct rc_api_award_achievement_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the achievement */ - unsigned achievement_id; + uint32_t achievement_id; /* Non-zero if the achievement was earned in hardcore */ - int hardcore; + uint32_t hardcore; /* The hash associated to the game being played */ const char* game_hash; } @@ -211,12 +215,14 @@ rc_api_award_achievement_request_t; */ typedef struct rc_api_award_achievement_response_t { /* The unique identifier of the achievement that was awarded */ - unsigned awarded_achievement_id; + uint32_t awarded_achievement_id; /* The updated player score */ - unsigned new_player_score; + uint32_t new_player_score; + /* The updated player softcore score */ + uint32_t new_player_score_softcore; /* The number of achievements the user has not yet unlocked for this game * (in hardcore/non-hardcore per hardcore flag in request) */ - unsigned achievements_remaining; + uint32_t achievements_remaining; /* Common server-provided response information */ rc_api_response_t response; @@ -225,6 +231,7 @@ rc_api_award_achievement_response_t; int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); /* --- Submit Leaderboard Entry --- */ @@ -238,9 +245,9 @@ typedef struct rc_api_submit_lboard_entry_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the leaderboard */ - unsigned leaderboard_id; + uint32_t leaderboard_id; /* The value being submitted */ - int score; + int32_t score; /* The hash associated to the game being played */ const char* game_hash; } @@ -251,9 +258,9 @@ typedef struct rc_api_lboard_entry_t { /* The user associated to the entry */ const char* username; /* The rank of the entry */ - unsigned rank; + uint32_t rank; /* The value of the entry */ - int score; + int32_t score; } rc_api_lboard_entry_t; @@ -262,18 +269,18 @@ rc_api_lboard_entry_t; */ typedef struct rc_api_submit_lboard_entry_response_t { /* The value that was submitted */ - int submitted_score; + int32_t submitted_score; /* The player's best submitted value */ - int best_score; + int32_t best_score; /* The player's new rank within the leaderboard */ - unsigned new_rank; + uint32_t new_rank; /* The total number of entries in the leaderboard */ - unsigned num_entries; + uint32_t num_entries; /* An array of the top entries for the leaderboard */ rc_api_lboard_entry_t* top_entries; /* The number of items in the top_entries array */ - unsigned num_top_entries; + uint32_t num_top_entries; /* Common server-provided response information */ rc_api_response_t response; @@ -282,6 +289,7 @@ rc_api_submit_lboard_entry_response_t; int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_api_user.h b/deps/rcheevos/include/rc_api_user.h index 758842557aeb..9fb348315cdc 100644 --- a/deps/rcheevos/include/rc_api_user.h +++ b/deps/rcheevos/include/rc_api_user.h @@ -3,6 +3,9 @@ #include "rc_api_request.h" +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -32,9 +35,11 @@ typedef struct rc_api_login_response_t { /* The API token to use for all future requests */ const char* api_token; /* The current score of the player */ - unsigned score; + uint32_t score; + /* The current softcore score of the player */ + uint32_t score_softcore; /* The number of unread messages waiting for the player on the web site */ - unsigned num_unread_messages; + uint32_t num_unread_messages; /* The preferred name to display for the player */ const char* display_name; @@ -45,6 +50,7 @@ rc_api_login_response_t; int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_login_response(rc_api_login_response_t* response); /* --- Start Session --- */ @@ -58,14 +64,38 @@ typedef struct rc_api_start_session_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; } rc_api_start_session_request_t; +/** + * Response data for an achievement unlock. + */ +typedef struct rc_api_unlock_entry_t { + /* The unique identifier of the unlocked achievement */ + uint32_t achievement_id; + /* When the achievement was unlocked */ + time_t when; +} +rc_api_unlock_entry_t; + /** * Response data for a start session request. */ typedef struct rc_api_start_session_response_t { + /* An array of hardcore user unlocks */ + rc_api_unlock_entry_t* hardcore_unlocks; + /* An array of user unlocks */ + rc_api_unlock_entry_t* unlocks; + + /* The number of items in the hardcore_unlocks array */ + uint32_t num_hardcore_unlocks; + /* The number of items in the unlocks array */ + uint32_t num_unlocks; + + /* The server timestamp when the response was generated */ + time_t server_now; + /* Common server-provided response information */ rc_api_response_t response; } @@ -73,6 +103,7 @@ rc_api_start_session_response_t; int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); /* --- Fetch User Unlocks --- */ @@ -86,9 +117,9 @@ typedef struct rc_api_fetch_user_unlocks_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; /* Non-zero to fetch hardcore unlocks, 0 to fetch non-hardcore unlocks */ - int hardcore; + uint32_t hardcore; } rc_api_fetch_user_unlocks_request_t; @@ -97,9 +128,9 @@ rc_api_fetch_user_unlocks_request_t; */ typedef struct rc_api_fetch_user_unlocks_response_t { /* An array of achievement IDs previously unlocked by the user */ - unsigned* achievement_ids; + uint32_t* achievement_ids; /* The number of items in the achievement_ids array */ - unsigned num_achievement_ids; + uint32_t num_achievement_ids; /* Common server-provided response information */ rc_api_response_t response; @@ -108,6 +139,7 @@ rc_api_fetch_user_unlocks_response_t; int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_client.h b/deps/rcheevos/include/rc_client.h new file mode 100644 index 000000000000..da0c40bdf1d0 --- /dev/null +++ b/deps/rcheevos/include/rc_client.h @@ -0,0 +1,659 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +/* implementation abstracted in rc_client_internal.h */ +typedef struct rc_client_t rc_client_t; +typedef struct rc_client_async_handle_t rc_client_async_handle_t; + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address into buffer. + * Returns the number of bytes read. A return value of 0 indicates the address was invalid. + */ +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); + +/** + * Internal method passed to rc_client_server_call_t to process the server response. + */ +typedef void (*rc_client_server_callback_t)(const rc_api_server_response_t* server_response, void* callback_data); + +/** + * Callback used to issue a request to the server. + */ +typedef void (*rc_client_server_call_t)(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +/** + * Generic callback for asynchronous eventing. + */ +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +/** + * Callback for logging or displaying a message. + */ +typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function); + +/** + * Releases resources associated to a rc_client_t object. + * Pointer will no longer be valid after making this call. + */ +void rc_client_destroy(rc_client_t* client); + +/** + * Sets whether hardcore is enabled (on by default). + * Can be called with a game loaded. + * Enabling hardcore with a game loaded will raise an RC_CLIENT_EVENT_RESET + * event. Processing will be disabled until rc_client_reset is called. + */ +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +int rc_client_get_hardcore_enabled(const rc_client_t* client); + +/** + * Sets whether encore mode is enabled (off by default). + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +int rc_client_get_encore_mode_enabled(const rc_client_t* client); + +/** + * Sets whether unofficial achievements should be loaded. + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +int rc_client_get_unofficial_enabled(const rc_client_t* client); + +/** + * Sets whether spectator mode is enabled (off by default). + * If enabled, events for achievement unlocks and leaderboard submissions will be + * raised, but server calls to actually perform the unlock/submit will not occur. + * Can be modified while a game is loaded. Evaluated at unlock/submit time. + * Cannot be modified if disabled before a game is loaded. + */ +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +int rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +void rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +void* rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +void rc_client_set_host(const rc_client_t* client, const char* hostname); + +typedef uint64_t rc_clock_t; +typedef rc_clock_t (*rc_get_time_millisecs_func_t)(const rc_client_t* client); + +/** + * Specifies a function that returns a value that increases once per millisecond. + */ +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler); + +/** + * Marks an async process as aborted. The associated callback will not be called. + */ +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback); +enum { + RC_CLIENT_LOG_LEVEL_NONE = 0, + RC_CLIENT_LOG_LEVEL_ERROR = 1, + RC_CLIENT_LOG_LEVEL_WARN = 2, + RC_CLIENT_LOG_LEVEL_INFO = 3, + RC_CLIENT_LOG_LEVEL_VERBOSE = 4, + NUM_RC_CLIENT_LOG_LEVELS = 5 +}; + +/*****************************************************************************\ +| User | +\*****************************************************************************/ + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Logout the user. + */ +void rc_client_logout(rc_client_t* client); + +typedef struct rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; +} rc_client_user_t; + +/** + * Gets information about the logged in user. Will return NULL if the user is not logged in. + */ +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size); + +typedef struct rc_client_user_game_summary_t +{ + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + + uint32_t points_core; + uint32_t points_unlocked; +} rc_client_user_game_summary_t; + +/** + * Gets a breakdown of the number of achievements in the game, and how many the user has unlocked. + * Used for the "You have unlocked X of Y achievements" message shown when the game starts. + */ +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +/** + * Start loading an unidentified game. + */ +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Start loading a game. + */ +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Unloads the current game. + */ +void rc_client_unload_game(rc_client_t* client); + +typedef struct rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} rc_client_game_t; + +/** + * Get information about the current game. Returns NULL if no game is loaded. + */ +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size); + +/** + * Changes the active disc in a multi-disc game. + */ +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +typedef struct rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + + uint32_t num_achievements; + uint32_t num_leaderboards; +} rc_client_subset_t; + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE = 0, /* unprocessed */ + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE = 1, /* eligible to trigger */ + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED = 2, /* earned by user */ + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED = 3, /* not supported by this version of the runtime */ + NUM_RC_CLIENT_ACHIEVEMENT_STATES = 4 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE = 0, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL = (1 << 1), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE | RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL +}; + +enum { + RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0, + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7, + NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 8 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE = 0, + RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE = (1 << 1), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE | RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE +}; + +typedef struct rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size); + +typedef struct rc_client_achievement_bucket_t { + rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_achievement_bucket_t; + +typedef struct rc_client_achievement_list_t { + rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_achievement_list_t; + +enum { + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE = 0, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS = 1 +}; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_get_achievement_list. + */ +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list); + +/** + * Returns non-zero if there are any achievements that can be queried through rc_client_create_achievement_list(). + */ +int rc_client_has_achievements(rc_client_t* client); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_STATE_INACTIVE = 0, + RC_CLIENT_LEADERBOARD_STATE_ACTIVE = 1, + RC_CLIENT_LEADERBOARD_STATE_TRACKING = 2, + RC_CLIENT_LEADERBOARD_STATE_DISABLED = 3, + NUM_RC_CLIENT_LEADERBOARD_STATES = 4 +}; + +enum { + RC_CLIENT_LEADERBOARD_FORMAT_TIME = 0, + RC_CLIENT_LEADERBOARD_FORMAT_SCORE = 1, + RC_CLIENT_LEADERBOARD_FORMAT_VALUE = 2, + NUM_RC_CLIENT_LEADERBOARD_FORMATS = 3 +}; + +#define RC_CLIENT_LEADERBOARD_DISPLAY_SIZE 24 + +typedef struct rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t format; + uint8_t lower_is_better; +} rc_client_leaderboard_t; + +/** + * Get information about a leaderboard. Returns NULL if not found. + */ +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id); + +typedef struct rc_client_leaderboard_tracker_t { + char display[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + uint32_t id; +} rc_client_leaderboard_tracker_t; + +typedef struct rc_client_leaderboard_bucket_t { + rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_leaderboard_bucket_t; + +typedef struct rc_client_leaderboard_list_t { + rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_leaderboard_list_t; + +enum { + RC_CLIENT_LEADERBOARD_BUCKET_UNKNOWN = 0, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE = 1, + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4, + NUM_RC_CLIENT_LEADERBOARD_BUCKETS = 5 +}; + +enum { + RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE = 0, + RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING = 1 +}; + +/** + * Creates a list of leaderboards matching the specified grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_get_leaderboard_list. + */ +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +/** + * Returns non-zero if the current game has any leaderboards. + */ +int rc_client_has_leaderboards(rc_client_t* client); + +typedef struct rc_client_leaderboard_entry_t { + const char* user; + char display[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + time_t submitted; + uint32_t rank; + uint32_t index; +} rc_client_leaderboard_entry_t; + +typedef struct rc_client_leaderboard_entry_list_t { + rc_client_leaderboard_entry_t* entries; + uint32_t num_entries; + int32_t user_index; +} rc_client_leaderboard_entry_list_t; + +typedef void (*rc_client_fetch_leaderboard_entries_callback_t)(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server containing the logged-in user. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Gets the URL for the profile picture of the user associated to a leaderboard entry. + * Returns RC_OK on success. + */ +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size); + +/** + * Destroys a list allocated by rc_client_begin_fetch_leaderboard_entries or rc_client_begin_fetch_leaderboard_entries_around_user. + */ +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list); + +/** + * Used for scoreboard events. Contains the response from the server when a leaderboard entry is submitted. + * NOTE: This structure is only valid within the event callback. If you want to make use of the data outside + * of the callback, you should create copies of both the top entries and usernames within. + */ +typedef struct rc_client_leaderboard_scoreboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + uint32_t rank; + /* The value of the entry */ + char score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; +} rc_client_leaderboard_scoreboard_entry_t; + +typedef struct rc_client_leaderboard_scoreboard_t { + /* The ID of the leaderboard which was submitted */ + uint32_t leaderboard_id; + /* The value that was submitted */ + char submitted_score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + /* The player's best submitted value */ + char best_score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + /* The player's new rank within the leaderboard */ + uint32_t new_rank; + /* The total number of entries in the leaderboard */ + uint32_t num_entries; + + /* An array of the top entries for the leaderboard */ + rc_client_leaderboard_scoreboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + uint32_t num_top_entries; +} rc_client_leaderboard_scoreboard_t; + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +/** + * Returns non-zero if the current game supports rich presence. + */ +int rc_client_has_rich_presence(rc_client_t* client); + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Processing | +\*****************************************************************************/ + +enum { + RC_CLIENT_EVENT_TYPE_NONE = 0, + RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED = 1, /* [achievement] was earned by the player */ + RC_CLIENT_EVENT_LEADERBOARD_STARTED = 2, /* [leaderboard] attempt has started */ + RC_CLIENT_EVENT_LEADERBOARD_FAILED = 3, /* [leaderboard] attempt failed */ + RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED = 4, /* [leaderboard] attempt submitted */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW = 5, /* [achievement] challenge indicator should be shown */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE = 6, /* [achievement] challenge indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW = 7, /* progress indicator should be shown for [achievement] */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE = 8, /* progress indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE = 9, /* progress indicator should be updated to reflect new badge/progress for [achievement] */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 10, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 11, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD = 13, /* [leaderboard_scoreboard] possibly-new ranking received for [leaderboard] */ + RC_CLIENT_EVENT_RESET = 14, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 15, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 16, /* an API response returned a [server_error] and will not be retried */ + RC_CLIENT_EVENT_DISCONNECTED = 17, /* an unlock request could not be completed and is pending */ + RC_CLIENT_EVENT_RECONNECTED = 18 /* all pending unlocks have been completed */ +}; + +typedef struct rc_client_server_error_t +{ + const char* error_message; + const char* api; + int result; + uint32_t related_id; +} rc_client_server_error_t; + +typedef struct rc_client_event_t +{ + uint32_t type; + + rc_client_achievement_t* achievement; + rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_tracker_t* leaderboard_tracker; + rc_client_leaderboard_scoreboard_t* leaderboard_scoreboard; + rc_client_server_error_t* server_error; + +} rc_client_event_t; + +/** + * Callback used to notify the client when certain events occur. + */ +typedef void (*rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); + +/** + * Determines if there are any active achievements/leaderboards/rich presence that need processing. + */ +int rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +void rc_client_do_frame(rc_client_t* client); + +/** + * Processes the periodic queue. + * Called internally by rc_client_do_frame. + * Should be explicitly called if rc_client_do_frame is not being called because emulation is paused. + */ +void rc_client_idle(rc_client_t* client); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +void rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +size_t rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer); + +/** + * Deserializes the runtime state from a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/deps/rcheevos/include/rc_consoles.h b/deps/rcheevos/include/rc_consoles.h index 71e67764a349..e416bc9e08b0 100644 --- a/deps/rcheevos/include/rc_consoles.h +++ b/deps/rcheevos/include/rc_consoles.h @@ -5,11 +5,14 @@ extern "C" { #endif +#include + /*****************************************************************************\ | Console identifiers | \*****************************************************************************/ enum { + RC_CONSOLE_UNKNOWN = 0, RC_CONSOLE_MEGA_DRIVE = 1, RC_CONSOLE_NINTENDO_64 = 2, RC_CONSOLE_SUPER_NINTENDO = 3, @@ -112,21 +115,21 @@ enum { }; typedef struct rc_memory_region_t { - unsigned start_address; /* first address of block as queried by RetroAchievements */ - unsigned end_address; /* last address of block as queried by RetroAchievements */ - unsigned real_address; /* real address for first address of block */ - char type; /* RC_MEMORY_TYPE_ for block */ + uint32_t start_address; /* first address of block as queried by RetroAchievements */ + uint32_t end_address; /* last address of block as queried by RetroAchievements */ + uint32_t real_address; /* real address for first address of block */ + uint8_t type; /* RC_MEMORY_TYPE_ for block */ const char* description; /* short description of block */ } rc_memory_region_t; typedef struct rc_memory_regions_t { const rc_memory_region_t* region; - unsigned num_regions; + uint32_t num_regions; } rc_memory_regions_t; -const rc_memory_regions_t* rc_console_memory_regions(int console_id); +const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_error.h b/deps/rcheevos/include/rc_error.h index 91a98c11cd98..89d762708805 100644 --- a/deps/rcheevos/include/rc_error.h +++ b/deps/rcheevos/include/rc_error.h @@ -36,7 +36,16 @@ enum { RC_INVALID_MEASURED_TARGET = -23, RC_INVALID_COMPARISON = -24, RC_INVALID_STATE = -25, - RC_INVALID_JSON = -26 + RC_INVALID_JSON = -26, + RC_API_FAILURE = -27, + RC_LOGIN_REQUIRED = -28, + RC_NO_GAME_LOADED = -29, + RC_HARDCORE_DISABLED = -30, + RC_ABORTED = -31, + RC_NO_RESPONSE = -32, + RC_ACCESS_DENIED = -33, + RC_INVALID_CREDENTIALS = -34, + RC_EXPIRED_TOKEN = -35 }; const char* rc_error_str(int ret); diff --git a/deps/rcheevos/include/rc_hash.h b/deps/rcheevos/include/rc_hash.h index 6d04536e9fec..ba9ea1c027b4 100644 --- a/deps/rcheevos/include/rc_hash.h +++ b/deps/rcheevos/include/rc_hash.h @@ -27,20 +27,20 @@ extern "C" { /* data for rc_hash_iterate */ - struct rc_hash_iterator + typedef struct rc_hash_iterator { - uint8_t* buffer; + const uint8_t* buffer; size_t buffer_size; uint8_t consoles[12]; int index; const char* path; - }; + } rc_hash_iterator_t; /* initializes a rc_hash_iterator * - path must be provided * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) */ - void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size); + void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); /* releases resources associated to a rc_hash_iterator */ @@ -92,12 +92,12 @@ extern "C" { /* ===================================================== */ - #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) - #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) - #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) - #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ + #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) /* the first data/audio track of the second session */ - /* opens a track from the specified file. track 0 indicates the largest data track should be opened. + /* opens a track from the specified file. see the RC_HASH_CDTRACK_ defines for special tracks. * returns a handle to be passed to the other functions, or NULL if the track could not be opened. */ typedef void* (*rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); diff --git a/deps/rcheevos/include/rc_runtime.h b/deps/rcheevos/include/rc_runtime.h index 92b429635101..2804cc97d952 100644 --- a/deps/rcheevos/include/rc_runtime.h +++ b/deps/rcheevos/include/rc_runtime.h @@ -8,6 +8,7 @@ extern "C" { #include "rc_error.h" #include +#include /*****************************************************************************\ | Forward Declarations (defined in rc_runtime_types.h) | @@ -34,32 +35,32 @@ typedef struct rc_value_t rc_value_t; * num_bytes is greater than 1, the value is read in little-endian from * memory. */ -typedef unsigned (*rc_runtime_peek_t)(unsigned address, unsigned num_bytes, void* ud); +typedef uint32_t(*rc_runtime_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); /*****************************************************************************\ | Runtime | \*****************************************************************************/ typedef struct rc_runtime_trigger_t { - unsigned id; + uint32_t id; rc_trigger_t* trigger; void* buffer; rc_memref_t* invalid_memref; - unsigned char md5[16]; - int serialized_size; - char owns_memrefs; + uint8_t md5[16]; + int32_t serialized_size; + uint8_t owns_memrefs; } rc_runtime_trigger_t; typedef struct rc_runtime_lboard_t { - unsigned id; - int value; + uint32_t id; + int32_t value; rc_lboard_t* lboard; void* buffer; rc_memref_t* invalid_memref; - unsigned char md5[16]; - int serialized_size; - char owns_memrefs; + uint8_t md5[16]; + uint32_t serialized_size; + uint8_t owns_memrefs; } rc_runtime_lboard_t; @@ -67,19 +68,19 @@ typedef struct rc_runtime_richpresence_t { rc_richpresence_t* richpresence; void* buffer; struct rc_runtime_richpresence_t* previous; - unsigned char md5[16]; - char owns_memrefs; + uint8_t md5[16]; + uint8_t owns_memrefs; } rc_runtime_richpresence_t; typedef struct rc_runtime_t { rc_runtime_trigger_t* triggers; - unsigned trigger_count; - unsigned trigger_capacity; + uint32_t trigger_count; + uint32_t trigger_capacity; rc_runtime_lboard_t* lboards; - unsigned lboard_count; - unsigned lboard_capacity; + uint32_t lboard_count; + uint32_t lboard_capacity; rc_runtime_richpresence_t* richpresence; @@ -88,26 +89,29 @@ typedef struct rc_runtime_t { rc_value_t* variables; rc_value_t** next_variable; + + uint8_t owns_self; } rc_runtime_t; +rc_runtime_t* rc_runtime_alloc(void); void rc_runtime_init(rc_runtime_t* runtime); void rc_runtime_destroy(rc_runtime_t* runtime); -int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); -rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* runtime, unsigned id); -int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id, unsigned* measured_value, unsigned* measured_target); -int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned id, char *buffer, size_t buffer_size); +int rc_runtime_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, uint32_t id); +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* runtime, uint32_t id); +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target); +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char *buffer, size_t buffer_size); -int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -void rc_runtime_deactivate_lboard(rc_runtime_t* runtime, unsigned id); -rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* runtime, unsigned id); -int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format); +int rc_runtime_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_lboard(rc_runtime_t* runtime, uint32_t id); +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* runtime, uint32_t id); +int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format); int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); -int rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, unsigned buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L); +int rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L); enum { RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, /* from WAITING, PAUSED, or PRIMED to ACTIVE */ @@ -126,9 +130,9 @@ enum { }; typedef struct rc_runtime_event_t { - unsigned id; - int value; - char type; + uint32_t id; + int32_t value; + uint8_t type; } rc_runtime_event_t; @@ -137,13 +141,13 @@ typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_eve void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L); void rc_runtime_reset(rc_runtime_t* runtime); -typedef int (*rc_runtime_validate_address_t)(unsigned address); +typedef int (*rc_runtime_validate_address_t)(uint32_t address); void rc_runtime_validate_addresses(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); -void rc_runtime_invalidate_address(rc_runtime_t* runtime, unsigned address); +void rc_runtime_invalidate_address(rc_runtime_t* runtime, uint32_t address); int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L); int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L); -int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* serialized, lua_State* L); +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L); #ifdef __cplusplus } diff --git a/deps/rcheevos/include/rc_runtime_types.h b/deps/rcheevos/include/rc_runtime_types.h index dc3ab60c920d..04a0c931ca37 100644 --- a/deps/rcheevos/include/rc_runtime_types.h +++ b/deps/rcheevos/include/rc_runtime_types.h @@ -7,6 +7,9 @@ extern "C" { #include "rc_error.h" +#include +#include + #ifndef RC_RUNTIME_H /* prevents pedantic redefiniton error */ typedef struct lua_State lua_State; @@ -28,7 +31,7 @@ typedef struct rc_value_t rc_value_t; * num_bytes is greater than 1, the value is read in little-endian from * memory. */ -typedef unsigned (*rc_peek_t)(unsigned address, unsigned num_bytes, void* ud); +typedef uint32_t(*rc_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); /*****************************************************************************\ | Memory References | @@ -57,24 +60,25 @@ enum { RC_MEMSIZE_FLOAT, RC_MEMSIZE_MBF32, RC_MEMSIZE_MBF32_LE, + RC_MEMSIZE_FLOAT_BE, RC_MEMSIZE_VARIABLE }; typedef struct rc_memref_value_t { /* The current value of this memory reference. */ - unsigned value; + uint32_t value; /* The last differing value of this memory reference. */ - unsigned prior; + uint32_t prior; /* The size of the value. */ - char size; + uint8_t size; /* True if the value changed this frame. */ - char changed; + uint8_t changed; /* The value type of the value (for variables) */ - char type; + uint8_t type; /* True if the reference will be used in indirection. * NOTE: This is actually a property of the rc_memref_t, but we put it here to save space */ - char is_indirect; + uint8_t is_indirect; } rc_memref_value_t; @@ -83,7 +87,7 @@ struct rc_memref_t { rc_memref_value_t value; /* The memory address of this variable. */ - unsigned address; + uint32_t address; /* The next memory reference in the chain. */ rc_memref_t* next; @@ -111,7 +115,7 @@ typedef struct rc_operand_t { rc_memref_t* memref; /* An integer value. */ - unsigned num; + uint32_t num; /* A floating point value. */ double dbl; @@ -121,10 +125,10 @@ typedef struct rc_operand_t { } value; /* specifies which member of the value union is being used */ - char type; + uint8_t type; /* the actual RC_MEMSIZE of the operand - memref.size may differ */ - char size; + uint8_t size; } rc_operand_t; @@ -182,27 +186,27 @@ struct rc_condition_t { rc_operand_t operand2; /* Required hits to fire this condition. */ - unsigned required_hits; + uint32_t required_hits; /* Number of hits so far. */ - unsigned current_hits; + uint32_t current_hits; /* The next condition in the chain. */ rc_condition_t* next; - /* The type of the condition. */ - char type; + /* The type of the condition. (RC_CONDITION_*) */ + uint8_t type; - /* The comparison operator to use. */ - char oper; /* operator is a reserved word in C++. */ + /* The comparison operator to use. (RC_OPERATOR_*) */ + uint8_t oper; /* operator is a reserved word in C++. */ - /* Set if the condition needs to processed as part of the "check if paused" pass. */ - char pause; + /* Set if the condition needs to processed as part of the "check if paused" pass. (bool) */ + uint8_t pause; - /* Whether or not the condition evaluated true on the last check */ - char is_true; + /* Whether or not the condition evaluated true on the last check. (bool) */ + uint8_t is_true; - /* Unique identifier of optimized comparator to use */ - char optimized_comparator; + /* Unique identifier of optimized comparator to use. (RC_PROCESSING_COMPARE_*) */ + uint8_t optimized_comparator; }; /*****************************************************************************\ @@ -219,13 +223,13 @@ struct rc_condset_t { rc_condition_t* conditions; /* True if any condition in the set is a pause condition. */ - char has_pause; + uint8_t has_pause; /* True if the set is currently paused. */ - char is_paused; + uint8_t is_paused; /* True if the set has indirect memory references. */ - char has_indirect_memrefs; + uint8_t has_indirect_memrefs; }; /*****************************************************************************\ @@ -254,22 +258,22 @@ struct rc_trigger_t { rc_memref_t* memrefs; /* The current state of the MEASURED condition. */ - unsigned measured_value; + uint32_t measured_value; /* The target state of the MEASURED condition */ - unsigned measured_target; + uint32_t measured_target; /* The current state of the trigger */ - char state; + uint8_t state; /* True if at least one condition has a non-zero hit count */ - char has_hits; + uint8_t has_hits; /* True if at least one condition has a non-zero required hit count */ - char has_required_hits; + uint8_t has_required_hits; /* True if the measured value should be displayed as a percentage */ - char measured_as_percent; + uint8_t measured_as_percent; }; int rc_trigger_size(const char* memaddr); @@ -301,7 +305,7 @@ struct rc_value_t { int rc_value_size(const char* memaddr); rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -int rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); +int32_t rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); /*****************************************************************************\ | Leaderboards | @@ -326,12 +330,12 @@ struct rc_lboard_t { rc_value_t* progress; rc_memref_t* memrefs; - char state; + uint8_t state; }; int rc_lboard_size(const char* memaddr); rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -int rc_evaluate_lboard(rc_lboard_t* lboard, int* value, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_evaluate_lboard(rc_lboard_t* lboard, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L); void rc_reset_lboard(rc_lboard_t* lboard); /*****************************************************************************\ @@ -356,7 +360,7 @@ enum { }; int rc_parse_format(const char* format_str); -int rc_format_value(char* buffer, int size, int value, int format); +int rc_format_value(char* buffer, int size, int32_t value, int format); /*****************************************************************************\ | Rich Presence | @@ -365,8 +369,8 @@ int rc_format_value(char* buffer, int size, int value, int format); typedef struct rc_richpresence_lookup_item_t rc_richpresence_lookup_item_t; struct rc_richpresence_lookup_item_t { - unsigned first; - unsigned last; + uint32_t first; + uint32_t last; rc_richpresence_lookup_item_t* left; rc_richpresence_lookup_item_t* right; const char* label; @@ -379,7 +383,7 @@ struct rc_richpresence_lookup_t { rc_richpresence_lookup_t* next; const char* name; const char* default_label; - unsigned short format; + uint8_t format; }; typedef struct rc_richpresence_display_part_t rc_richpresence_display_part_t; @@ -389,7 +393,7 @@ struct rc_richpresence_display_part_t { const char* text; rc_richpresence_lookup_t* lookup; rc_memref_value_t *value; - unsigned short display_type; + uint8_t display_type; }; typedef struct rc_richpresence_display_t rc_richpresence_display_t; @@ -410,9 +414,9 @@ struct rc_richpresence_t { int rc_richpresence_size(const char* script); int rc_richpresence_size_lines(const char* script, int* lines_read); rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx); -int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L); -int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); void rc_reset_richpresence(rc_richpresence_t* self); #ifdef __cplusplus diff --git a/deps/rcheevos/src/rapi/rc_api_common.c b/deps/rcheevos/src/rapi/rc_api_common.c index 42fb40527e8e..481aee07da71 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.c +++ b/deps/rcheevos/src/rapi/rc_api_common.c @@ -2,7 +2,7 @@ #include "rc_api_request.h" #include "rc_api_runtime.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include #include @@ -16,161 +16,176 @@ static char* g_host = NULL; static char* g_imagehost = NULL; -#undef DEBUG_BUFFERS - /* --- rc_json --- */ -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field); +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); + +static int rc_json_match_char(rc_json_iterator_t* iterator, char c) +{ + if (iterator->json < iterator->end && *iterator->json == c) { + ++iterator->json; + return 1; + } + + return 0; +} + +static void rc_json_skip_whitespace(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json)) + ++iterator->json; +} + +static int rc_json_find_closing_quote(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end) { + if (*iterator->json == '"') + return 1; + + if (*iterator->json == '\\') { + ++iterator->json; + if (iterator->json == iterator->end) + return 0; + } + + if (*iterator->json == '\0') + return 0; + + ++iterator->json; + } -static int rc_json_parse_field(const char** json_ptr, rc_json_field_t* field) { + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { int result; - field->value_start = *json_ptr; + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; + + field->value_start = iterator->json; - switch (**json_ptr) + switch (*iterator->json) { case '"': /* quoted string */ - ++(*json_ptr); - while (**json_ptr != '"') { - if (**json_ptr == '\\') - ++(*json_ptr); - - if (**json_ptr == '\0') - return RC_INVALID_JSON; - - ++(*json_ptr); - } - ++(*json_ptr); + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; break; case '-': case '+': /* signed number */ - ++(*json_ptr); + ++iterator->json; /* fallthrough to number */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': /* number */ - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); - if (**json_ptr == '.') { - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + + if (rc_json_match_char(iterator, '.')) { + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; } break; case '[': /* array */ - result = rc_json_parse_array(json_ptr, field); + result = rc_json_parse_array(iterator, field); if (result != RC_OK) - return result; + return result; break; case '{': /* object */ - result = rc_json_parse_object(json_ptr, NULL, 0, &field->array_size); + result = rc_json_parse_object(iterator, NULL, 0, &field->array_size); if (result != RC_OK) return result; break; default: /* non-quoted text [true,false,null] */ - if (!isalpha((unsigned char)**json_ptr)) + if (!isalpha((unsigned char)*iterator->json)) return RC_INVALID_JSON; - do { - ++(*json_ptr); - } while (isalnum((unsigned char)**json_ptr)); + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; break; } - field->value_end = *json_ptr; + field->value_end = iterator->json; return RC_OK; } -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field) { +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { rc_json_field_t unused_field; - const char* json = *json_ptr; int result; - if (*json != '[') + if (!rc_json_match_char(iterator, '[')) return RC_INVALID_JSON; - ++json; field->array_size = 0; - if (*json != ']') { - do - { - while (isspace((unsigned char)*json)) - ++json; - result = rc_json_parse_field(&json, &unused_field); - if (result != RC_OK) - return result; + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; - ++field->array_size; + do + { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; - if (*json != ',') - break; + ++field->array_size; - ++json; - } while (1); + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); - if (*json != ']') - return RC_INVALID_JSON; - } + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; - *json_ptr = ++json; return RC_OK; } -static int rc_json_get_next_field(rc_json_object_field_iterator_t* iterator) { - const char* json = iterator->json; +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; - - if (*json != '"') + if (!rc_json_match_char(iterator, '"')) return RC_INVALID_JSON; - iterator->field.name = ++json; - while (*json != '"') { - if (!*json) + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) return RC_INVALID_JSON; - ++json; + ++iterator->json; } - iterator->name_len = json - iterator->field.name; - ++json; - - while (isspace((unsigned char)*json)) - ++json; - if (*json != ':') + if (iterator->json == iterator->end) return RC_INVALID_JSON; - ++json; + field->name_len = iterator->json - field->name; + ++iterator->json; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ':')) + return RC_INVALID_JSON; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); - if (rc_json_parse_field(&json, &iterator->field) < 0) + if (rc_json_parse_field(iterator, field) < 0) return RC_INVALID_JSON; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); - iterator->json = json; return RC_OK; } -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { - rc_json_object_field_iterator_t iterator; - const char* json = *json_ptr; +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { size_t i; - unsigned num_fields = 0; + uint32_t num_fields = 0; + rc_json_field_t field; int result; if (fields_seen) @@ -179,60 +194,119 @@ static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, for (i = 0; i < field_count; ++i) fields[i].value_start = fields[i].value_end = NULL; - if (*json != '{') + if (!rc_json_match_char(iterator, '{')) return RC_INVALID_JSON; - ++json; - if (*json == '}') { - *json_ptr = ++json; + if (rc_json_match_char(iterator, '}')) /* empty object */ return RC_OK; - } - - memset(&iterator, 0, sizeof(iterator)); - iterator.json = json; do { - result = rc_json_get_next_field(&iterator); + result = rc_json_get_next_field(iterator, &field); if (result != RC_OK) return result; for (i = 0; i < field_count; ++i) { - if (!fields[i].value_start && strncmp(fields[i].name, iterator.field.name, iterator.name_len) == 0 && - fields[i].name[iterator.name_len] == '\0') { - fields[i].value_start = iterator.field.value_start; - fields[i].value_end = iterator.field.value_end; - fields[i].array_size = iterator.field.array_size; + if (!fields[i].value_start && fields[i].name_len == field.name_len && + memcmp(fields[i].name, field.name, field.name_len) == 0) { + fields[i].value_start = field.value_start; + fields[i].value_end = field.value_end; + fields[i].array_size = field.array_size; break; } } ++num_fields; - if (*iterator.json != ',') - break; - ++iterator.json; - } while (1); + } while (rc_json_match_char(iterator, ',')); - if (*iterator.json != '}') + if (!rc_json_match_char(iterator, '}')) return RC_INVALID_JSON; if (fields_seen) *fields_seen = num_fields; - *json_ptr = ++iterator.json; return RC_OK; } -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator) { - if (*iterator->json != ',' && *iterator->json != '{') +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{')) return 0; - ++iterator->json; - return (rc_json_get_next_field(iterator) == RC_OK); + return (rc_json_get_next_field(iterator, field) == RC_OK); +} + +int rc_json_get_object_string_length(const char* json) { + const char* json_start = json; + + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */ + + rc_json_parse_object(&iterator, NULL, 0, NULL); + + return (int)(iterator.json - json_start); } -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count) { +static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) { + const char* json = server_response->body; + const char* end = json; + + const char* title_start = strstr(json, ""); + if (title_start) { + title_start += 7; + if (isdigit((int)*title_start)) { + const char* title_end = strstr(title_start + 7, ""); + if (title_end) { + response->error_message = rc_buffer_strncpy(&response->buffer, title_start, title_end - title_start); + response->succeeded = 0; + return RC_INVALID_JSON; + } + } + } + + while (*end && *end != '\n' && end - json < 200) + ++end; + + if (end > json && end[-1] == '\r') + --end; + + if (end > json) + response->error_message = rc_buffer_strncpy(&response->buffer, json, end - json); + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +static int rc_json_convert_error_code(const char* server_error_code) +{ + switch (server_error_code[0]) { + case 'a': + if (strcmp(server_error_code, "access_denied") == 0) + return RC_ACCESS_DENIED; + break; + + case 'e': + if (strcmp(server_error_code, "expired_token") == 0) + return RC_EXPIRED_TOKEN; + break; + + case 'i': + if (strcmp(server_error_code, "invalid_credentials") == 0) + return RC_INVALID_CREDENTIALS; + break; + + default: + break; + } + + return RC_API_FAILURE; +} + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) { + int result; + #ifndef NDEBUG if (field_count < 2) return RC_INVALID_STATE; @@ -242,37 +316,66 @@ int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_jso return RC_INVALID_STATE; #endif - if (*json == '{') { - int result = rc_json_parse_object(&json, fields, field_count, NULL); + response->error_message = NULL; - rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); - rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + if (!server_response) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } - return result; + if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR || + server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) { + /* client provided error message is passed as the response body */ + response->error_message = server_response->body; + response->succeeded = 0; + return RC_NO_RESPONSE; } - response->error_message = NULL; + if (!server_response->body || !*server_response->body) { + /* expect valid HTTP status codes to have bodies that we can extract the message from, + * but provide some default messages in case they don't. */ + switch (server_response->http_status_code) { + case 504: /* 504 Gateway Timeout */ + case 522: /* 522 Connection Timed Out */ + case 524: /* 524 A Timeout Occurred */ + response->error_message = "Request has timed out."; + break; - if (*json) { - const char* end = json; - while (*end && *end != '\n' && end - json < 200) - ++end; - - if (end > json && end[-1] == '\r') - --end; - - if (end > json) { - char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); - response->error_message = dst; - memcpy(dst, json, end - json); - dst += (end - json); - *dst++ = '\0'; - rc_buf_consume(&response->buffer, response->error_message, dst); + case 521: /* 521 Web Server is Down */ + case 523: /* 523 Origin is Unreachable */ + response->error_message = "Could not connect to server."; + break; + + default: + break; } + + response->succeeded = 0; + return RC_NO_RESPONSE; } - response->succeeded = 0; - return RC_INVALID_JSON; + if (*server_response->body != '{') { + result = rc_json_extract_html_error(response, server_response); + } + else { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + result = rc_json_parse_object(&iterator, fields, field_count, NULL); + + rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); + rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + + /* Code will be the third field in the fields array, but may not always be present */ + if (field_count > 2 && strcmp(fields[2].name, "Code") == 0) { + rc_json_get_optional_string(&response->error_code, response, &fields[2], "Code", NULL); + if (response->error_code != NULL) + result = rc_json_convert_error_code(response->error_code); + } + } + + return result; } static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { @@ -280,14 +383,14 @@ static int rc_json_missing_field(rc_api_response_t* response, const rc_json_fiel const size_t not_found_len = strlen(not_found); const size_t field_len = strlen(field->name); - char* write = rc_buf_reserve(&response->buffer, field_len + not_found_len + 1); + uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1); if (write) { - response->error_message = write; + response->error_message = (char*)write; memcpy(write, field->name, field_len); write += field_len; memcpy(write, not_found, not_found_len + 1); write += not_found_len + 1; - rc_buf_consume(&response->buffer, response->error_message, write); + rc_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write); } response->succeeded = 0; @@ -295,7 +398,8 @@ static int rc_json_missing_field(rc_api_response_t* response, const rc_json_fiel } int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { - const char* json = field->value_start; + rc_json_iterator_t iterator; + #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; @@ -303,45 +407,53 @@ int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_ (void)field_name; #endif - if (!json) + if (!field->value_start) return rc_json_missing_field(response, field); - return (rc_json_parse_object(&json, fields, field_count, &field->array_size) == RC_OK); + memset(&iterator, 0, sizeof(iterator)); + iterator.json = field->value_start; + iterator.end = field->value_end; + return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK); } -static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_field_t* iterator) { - if (!iterator->array_size) - return 0; +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (iterator->json >= iterator->end) + return 0; - rc_json_parse_field(&iterator->value_start, field); + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + rc_json_skip_whitespace(iterator); - ++iterator->value_start; /* skip , or ] */ + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - --iterator->array_size; return 1; } -int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { - rc_json_field_t iterator; +int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_iterator_t iterator; + rc_json_field_t array; rc_json_field_t value; unsigned* entry; - if (!rc_json_get_required_array(num_entries, &iterator, response, field, field_name)) + memset(&array, 0, sizeof(array)); + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) return RC_MISSING_VALUE; if (*num_entries) { - *entries = (unsigned*)rc_buf_alloc(&response->buffer, *num_entries * sizeof(unsigned)); + *entries = (unsigned*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(unsigned)); if (!*entries) return RC_OUT_OF_MEMORY; value.name = field_name; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array.value_start; + iterator.end = array.value_end; + entry = *entries; while (rc_json_get_array_entry_value(&value, &iterator)) { if (!rc_json_get_unum(entry, &value, field_name)) @@ -357,7 +469,19 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r return RC_OK; } -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!rc_json_get_optional_array(num_entries, array_field, response, field, field_name)) + return rc_json_missing_field(response, field); + + return 1; +} + +int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; @@ -367,44 +491,43 @@ int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, if (!field->value_start || *field->value_start != '[') { *num_entries = 0; - return rc_json_missing_field(response, field); + return 0; } - memcpy(iterator, field, sizeof(*iterator)); - ++iterator->value_start; /* skip [ */ + memcpy(array_field, field, sizeof(*array_field)); + ++array_field->value_start; /* skip [ */ *num_entries = field->array_size; return 1; } -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator) { - if (!iterator->array_size) - return 0; +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (iterator->json >= iterator->end) + return 0; - rc_json_parse_object(&iterator->value_start, fields, field_count, NULL); + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + rc_json_skip_whitespace(iterator); - ++iterator->value_start; /* skip , or ] */ + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - --iterator->array_size; return 1; } -static unsigned rc_json_decode_hex4(const char* input) { +static uint32_t rc_json_decode_hex4(const char* input) { char hex[5]; memcpy(hex, input, 4); hex[4] = '\0'; - return (unsigned)strtoul(hex, NULL, 16); + return (uint32_t)strtoul(hex, NULL, 16); } -static int rc_json_ucs32_to_utf8(unsigned char* dst, unsigned ucs32_char) { +static int rc_json_ucs32_to_utf8(uint8_t* dst, uint32_t ucs32_char) { if (ucs32_char < 0x80) { dst[0] = (ucs32_char & 0x7F); return 1; @@ -449,7 +572,7 @@ static int rc_json_ucs32_to_utf8(unsigned char* dst, unsigned ucs32_char) { return 6; } -int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; size_t len = field->value_end - field->value_start; char* dst; @@ -480,7 +603,7 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ return 1; } - *out = dst = rc_buf_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ + *out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ do { if (*src == '\\') { @@ -501,13 +624,13 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ if (*src == 'u') { /* unicode character */ - unsigned ucs32_char = rc_json_decode_hex4(src + 1); + uint32_t ucs32_char = rc_json_decode_hex4(src + 1); src += 5; if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) { /* surrogate lead - look for surrogate tail */ if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') { - const unsigned surrogate = rc_json_decode_hex4(src + 2); + const uint32_t surrogate = rc_json_decode_hex4(src + 2); src += 6; if (surrogate >= 0xDC00 && surrogate < 0xE000) { @@ -540,13 +663,13 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ } while (*src != '\"'); } else { - *out = dst = rc_buf_reserve(buffer, len + 1); /* +1 for null terminator */ + *out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */ memcpy(dst, src, len); dst += len; } *dst++ = '\0'; - rc_buf_consume(buffer, *out, dst); + rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst); return 1; } @@ -562,9 +685,9 @@ int rc_json_get_required_string(const char** out, rc_api_response_t* response, c return rc_json_missing_field(response, field); } -int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; - int value = 0; + int32_t value = 0; int negative = 0; #ifndef NDEBUG @@ -604,21 +727,21 @@ int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_na return 1; } -void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { +void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value) { if (!rc_json_get_num(out, field, field_name)) *out = default_value; } -int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { if (rc_json_get_num(out, field, field_name)) return 1; return rc_json_missing_field(response, field); } -int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; - int value = 0; + uint32_t value = 0; #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) @@ -648,12 +771,12 @@ int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* fi return 1; } -void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value) { +void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value) { if (!rc_json_get_unum(out, field, field_name)) *out = default_value; } -int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { if (rc_json_get_unum(out, field, field_name)) return 1; @@ -746,121 +869,27 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js return rc_json_missing_field(response, field); } -/* --- rc_buf --- */ - -void rc_buf_init(rc_api_buffer_t* buffer) { - buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; - buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; - buffer->chunk.next = NULL; -} - -void rc_buf_destroy(rc_api_buffer_t* buffer) { - rc_api_buffer_chunk_t *chunk; -#ifdef DEBUG_BUFFERS - int count = 0; - int wasted = 0; - int total = 0; -#endif - - /* first chunk is not allocated. skip it. */ - chunk = buffer->chunk.next; - - /* deallocate any additional buffers */ - while (chunk) { - rc_api_buffer_chunk_t* next = chunk->next; -#ifdef DEBUG_BUFFERS - total += (int)(chunk->end - chunk->data); - wasted += (int)(chunk->end - chunk->write); - ++count; -#endif - free(chunk); - chunk = next; - } - -#ifdef DEBUG_BUFFERS - printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, - total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); -#endif -} - -char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount) { - rc_api_buffer_chunk_t* chunk = &buffer->chunk; - size_t remaining; - while (chunk) { - remaining = chunk->end - chunk->write; - if (remaining >= amount) - return chunk->write; - - if (!chunk->next) { - /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated - * to the chunk header, and the remaining will be used for data. - */ - const size_t chunk_header_size = sizeof(rc_api_buffer_chunk_t); - const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; - chunk->next = (rc_api_buffer_chunk_t*)malloc(alloc_size); - if (!chunk->next) - break; - - chunk->next->start = (char*)chunk->next + chunk_header_size; - chunk->next->write = chunk->next->start; - chunk->next->end = (char*)chunk->next + alloc_size; - chunk->next->next = NULL; - } - - chunk = chunk->next; - } - - return NULL; -} - -void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end) { - rc_api_buffer_chunk_t* chunk = &buffer->chunk; - do { - if (chunk->write == start) { - size_t offset = (end - chunk->start); - offset = (offset + 7) & ~7; - chunk->write = &chunk->start[offset]; - - if (chunk->write > chunk->end) - chunk->write = chunk->end; - break; - } - - chunk = chunk->next; - } while (chunk); -} - -void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { - char* ptr = rc_buf_reserve(buffer, amount); - rc_buf_consume(buffer, ptr, ptr + amount); - return (void*)ptr; -} - -void rc_api_destroy_request(rc_api_request_t* request) { - rc_buf_destroy(&request->buffer); -} +/* --- rc_api_request --- */ -void rc_api_format_md5(char checksum[33], const unsigned char digest[16]) { - snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", - digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], - digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] - ); +void rc_api_destroy_request(rc_api_request_t* request) +{ + rc_buffer_destroy(&request->buffer); } /* --- rc_url_builder --- */ -void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size) { - rc_api_buffer_chunk_t* used_buffer; +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size) { + rc_buffer_chunk_t* used_buffer; memset(builder, 0, sizeof(*builder)); builder->buffer = buffer; - builder->write = builder->start = rc_buf_reserve(buffer, estimated_size); + builder->write = builder->start = (char*)rc_buffer_reserve(buffer, estimated_size); used_buffer = &buffer->chunk; - while (used_buffer && used_buffer->write != builder->write) + while (used_buffer && used_buffer->write != (uint8_t*)builder->write) used_buffer = used_buffer->next; - builder->end = (used_buffer) ? used_buffer->end : builder->start + estimated_size; + builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size; } const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { @@ -869,7 +898,7 @@ const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { if (builder->result != RC_OK) return NULL; - rc_buf_consume(builder->buffer, builder->start, builder->write); + rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write); return builder->start; } @@ -879,7 +908,7 @@ static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) if (remaining < amount) { const size_t used = builder->write - builder->start; const size_t current_size = builder->end - builder->start; - const size_t buffer_prefix_size = sizeof(rc_api_buffer_chunk_t); + const size_t buffer_prefix_size = sizeof(rc_buffer_chunk_t); char* new_start; size_t new_size = (current_size < 256) ? 256 : current_size * 2; do { @@ -890,11 +919,11 @@ static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) new_size *= 2; } while (1); - /* rc_buf_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ + /* rc_buffer_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ if ((remaining - amount) > buffer_prefix_size) new_size -= buffer_prefix_size; - new_start = rc_buf_reserve(builder->buffer, new_size); + new_start = (char*)rc_buffer_reserve(builder->buffer, new_size); if (!new_start) { builder->result = RC_OUT_OF_MEMORY; return RC_OUT_OF_MEMORY; @@ -985,7 +1014,7 @@ static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, con return builder->result; } -void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value) { +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value) { if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { char num[16]; int chars = snprintf(num, sizeof(num), "%u", value); @@ -993,7 +1022,7 @@ void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* } } -void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value) { +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value) { if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { char num[16]; int chars = snprintf(num, sizeof(num), "%d", value); @@ -1008,7 +1037,7 @@ void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* void rc_api_url_build_dorequest_url(rc_api_request_t* request) { #define DOREQUEST_ENDPOINT "/dorequest.php" - rc_buf_init(&request->buffer); + rc_buffer_init(&request->buffer); if (!g_host) { request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; @@ -1017,13 +1046,13 @@ void rc_api_url_build_dorequest_url(rc_api_request_t* request) { const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT); const size_t host_len = strlen(g_host); const size_t url_len = host_len + endpoint_len; - char* url = rc_buf_reserve(&request->buffer, url_len); + uint8_t* url = rc_buffer_reserve(&request->buffer, url_len); memcpy(url, g_host, host_len); memcpy(url + host_len, DOREQUEST_ENDPOINT, endpoint_len); - rc_buf_consume(&request->buffer, url, url + url_len); + rc_buffer_consume(&request->buffer, url, url + url_len); - request->url = url; + request->url = (char*)url; } #undef DOREQUEST_ENDPOINT } @@ -1097,7 +1126,7 @@ void rc_api_set_image_host(const char* hostname) { int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) { rc_api_url_builder_t builder; - rc_buf_init(&request->buffer); + rc_buffer_init(&request->buffer); rc_url_builder_init(&builder, &request->buffer, 64); if (g_imagehost) { diff --git a/deps/rcheevos/src/rapi/rc_api_common.h b/deps/rcheevos/src/rapi/rc_api_common.h index fa18b7798edb..9fbb8a23d069 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.h +++ b/deps/rcheevos/src/rapi/rc_api_common.h @@ -10,72 +10,69 @@ extern "C" { #endif +#define RC_CONTENT_TYPE_URLENCODED "application/x-www-form-urlencoded" + typedef struct rc_api_url_builder_t { char* write; char* start; char* end; - /* pointer to a preallocated rc_api_buffer_t */ - rc_api_buffer_t* buffer; + /* pointer to a preallocated rc_buffer_t */ + rc_buffer_t* buffer; int result; } rc_api_url_builder_t; -void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size); +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size); void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); -#define RC_JSON_NEW_FIELD(n) {n,0,0,0} +#define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} typedef struct rc_json_field_t { - const char* name; const char* value_start; const char* value_end; - unsigned array_size; + const char* name; + size_t name_len; + uint32_t array_size; } rc_json_field_t; -typedef struct rc_json_object_field_iterator_t { - rc_json_field_t field; +typedef struct rc_json_iterator_t { const char* json; - size_t name_len; + const char* end; } -rc_json_object_field_iterator_t; +rc_json_iterator_t; -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count); -int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); -int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name); -int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name); +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count); +int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name); void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value); -void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value); -void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value); +void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value); +void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value); void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); -int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator); -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator); - -void rc_buf_init(rc_api_buffer_t* buffer); -void rc_buf_destroy(rc_api_buffer_t* buffer); -char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount); -void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end); -void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount); +int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); +int rc_json_get_object_string_length(const char* json); void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); -void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value); -void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value); +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value); +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value); void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value); void rc_api_url_build_dorequest_url(rc_api_request_t* request); int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token); -void rc_api_format_md5(char checksum[33], const unsigned char digest[16]); #ifdef __cplusplus } diff --git a/deps/rcheevos/src/rapi/rc_api_info.c b/deps/rcheevos/src/rapi/rc_api_info.c index 774220adcced..2b9f882625b1 100644 --- a/deps/rcheevos/src/rapi/rc_api_info.c +++ b/deps/rcheevos/src/rapi/rc_api_info.c @@ -27,15 +27,27 @@ int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_achievement_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_awarded_entry_t* entry; - rc_json_field_t iterator; - unsigned timet; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; int result; rc_json_field_t fields[] = { @@ -63,9 +75,9 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -81,14 +93,18 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_array(&response->num_recently_awarded, &iterator, &response->response, &response_fields[3], "RecentWinner")) + if (!rc_json_get_required_array(&response->num_recently_awarded, &array_field, &response->response, &response_fields[3], "RecentWinner")) return RC_MISSING_VALUE; if (response->num_recently_awarded) { - response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); + response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); if (!response->recently_awarded) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->recently_awarded; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -106,7 +122,7 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info } void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch Leaderboard Info --- */ @@ -130,14 +146,26 @@ int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_leaderboard_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_info_entry_t* entry; - rc_json_field_t iterator; - unsigned timet; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; int result; size_t len; char format[16]; @@ -178,9 +206,9 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -189,7 +217,7 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info if (!rc_json_get_required_unum(&response->id, &response->response, &leaderboarddata_fields[0], "LBID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_num(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) + if (!rc_json_get_required_unum(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) return RC_MISSING_VALUE; if (!rc_json_get_required_string(&response->title, &response->response, &leaderboarddata_fields[3], "LBTitle")) return RC_MISSING_VALUE; @@ -218,14 +246,18 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info response->format = RC_FORMAT_VALUE; } - if (!rc_json_get_required_array(&response->num_entries, &iterator, &response->response, &leaderboarddata_fields[10], "Entries")) + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &leaderboarddata_fields[10], "Entries")) return RC_MISSING_VALUE; if (response->num_entries) { - response->entries = (rc_api_lboard_info_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); + response->entries = (rc_api_lboard_info_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); if (!response->entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -252,7 +284,7 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info } void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch Games List --- */ @@ -270,13 +302,25 @@ int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_games_list_server_response(response, &response_obj); +} + +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { rc_api_game_list_entry_t* entry; - rc_json_object_field_iterator_t iterator; + rc_json_iterator_t iterator; + rc_json_field_t field; int result; char* end; @@ -287,9 +331,9 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -300,21 +344,22 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* } response->num_entries = fields[2].array_size; - rc_buf_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); + rc_buffer_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); - response->entries = (rc_api_game_list_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); + response->entries = (rc_api_game_list_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); if (!response->entries) return RC_OUT_OF_MEMORY; memset(&iterator, 0, sizeof(iterator)); iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; entry = response->entries; - while (rc_json_get_next_object_field(&iterator)) { - entry->id = strtol(iterator.field.name, &end, 10); + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->id = strtol(field.name, &end, 10); - iterator.field.name = ""; - if (!rc_json_get_string(&entry->name, &response->response.buffer, &iterator.field, "")) + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) return RC_MISSING_VALUE; ++entry; @@ -324,5 +369,5 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* } void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } diff --git a/deps/rcheevos/src/rapi/rc_api_runtime.c b/deps/rcheevos/src/rapi/rc_api_runtime.c index 43e72b79349b..27aeac13ded1 100644 --- a/deps/rcheevos/src/rapi/rc_api_runtime.c +++ b/deps/rcheevos/src/rapi/rc_api_runtime.c @@ -3,7 +3,7 @@ #include "rc_runtime.h" #include "rc_runtime_types.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include "../rhash/md5.h" #include @@ -24,11 +24,22 @@ int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_res rc_url_builder_append_str_param(&builder, "r", "gameid"); rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_resolve_hash_server_response(response, &response_obj); +} + +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), @@ -37,9 +48,9 @@ int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* respons }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -48,7 +59,7 @@ int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* respons } void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch Game Data --- */ @@ -65,21 +76,33 @@ int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_ if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_game_data_server_response(response, &response_obj); +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_definition_t* achievement; rc_api_leaderboard_definition_t* leaderboard; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; const char* last_author = ""; const char* last_author_field = ""; size_t last_author_len = 0; size_t len; - unsigned timet; + uint32_t timet; int result; char format[16]; @@ -127,9 +150,9 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -168,21 +191,25 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* leaderboards */ patchdata_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); - rc_buf_reserve(&response->response.buffer, len); + rc_buffer_reserve(&response->response.buffer, len); /* end estimation */ rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[4], "RichPresencePatch", ""); if (!response->rich_presence_script) response->rich_presence_script = ""; - if (!rc_json_get_required_array(&response->num_achievements, &iterator, &response->response, &patchdata_fields[5], "Achievements")) + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements")) return RC_MISSING_VALUE; if (response->num_achievements) { - response->achievements = (rc_api_achievement_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); + response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); if (!response->achievements) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + achievement = response->achievements; while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID")) @@ -224,14 +251,18 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r } } - if (!rc_json_get_required_array(&response->num_leaderboards, &iterator, &response->response, &patchdata_fields[6], "Leaderboards")) + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards")) return RC_MISSING_VALUE; if (response->num_leaderboards) { - response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); if (!response->leaderboards) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + leaderboard = response->leaderboards; while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID")) @@ -242,8 +273,10 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r return RC_MISSING_VALUE; if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem")) return RC_MISSING_VALUE; - rc_json_get_optional_bool(&leaderboard->lower_is_better, &leaderboard_fields[5], "LowerIsBetter", 0); - rc_json_get_optional_bool(&leaderboard->hidden, &leaderboard_fields[6], "Hidden", 0); + rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0); + leaderboard->lower_is_better = (uint8_t)result; + rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0); + leaderboard->hidden = (uint8_t)result; if (!leaderboard_fields[4].value_end) return RC_MISSING_VALUE; @@ -265,7 +298,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r } void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Ping --- */ @@ -286,25 +319,36 @@ int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_reques rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_ping_server_response(response, &response_obj); +} + +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); } void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Award Achievement --- */ @@ -335,29 +379,41 @@ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_ap snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0); md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); md5_finish(&md5, digest); - rc_api_format_md5(buffer, digest); + rc_format_md5(buffer, digest); rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_award_achievement_server_response(response, &response_obj); +} + +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("AchievementID"), RC_JSON_NEW_FIELD("AchievementsRemaining") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -375,14 +431,15 @@ int rc_api_process_award_achievement_response(rc_api_award_achievement_response_ } rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); - rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[3], "AchievementID", 0); - rc_json_get_optional_unum(&response->achievements_remaining, &fields[4], "AchievementsRemaining", (unsigned)-1); + rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1); return RC_OK; } void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Submit Leaderboard Entry --- */ @@ -414,18 +471,30 @@ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_ snprintf(buffer, sizeof(buffer), "%d", api_params->score); md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); md5_finish(&md5, digest); - rc_api_format_md5(buffer, digest); + rc_format_md5(buffer, digest); rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_submit_lboard_entry_server_response(response, &response_obj); +} + +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; int result; @@ -476,9 +545,9 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -497,14 +566,18 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo return RC_MISSING_VALUE; response->num_entries = (unsigned)atoi(str); - if (!rc_json_get_required_array(&response->num_top_entries, &iterator, &response->response, &response_fields[3], "TopEntries")) + if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries")) return RC_MISSING_VALUE; if (response->num_top_entries) { - response->top_entries = (rc_api_lboard_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); + response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); if (!response->top_entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->top_entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -524,5 +597,5 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo } void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } diff --git a/deps/rcheevos/src/rapi/rc_api_user.c b/deps/rcheevos/src/rapi/rc_api_user.c index 546a8a057806..0fa279c48e14 100644 --- a/deps/rcheevos/src/rapi/rc_api_user.c +++ b/deps/rcheevos/src/rapi/rc_api_user.c @@ -1,7 +1,7 @@ #include "rc_api_user.h" #include "rc_api_common.h" -#include "../rcheevos/rc_version.h" +#include "../rc_version.h" #include @@ -16,7 +16,7 @@ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_requ return RC_INVALID_STATE; rc_url_builder_init(&builder, &request->buffer, 48); - rc_url_builder_append_str_param(&builder, "r", "login"); + rc_url_builder_append_str_param(&builder, "r", "login2"); rc_url_builder_append_str_param(&builder, "u", api_params->username); if (api_params->password && api_params->password[0]) @@ -27,44 +27,58 @@ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_requ return RC_INVALID_STATE; request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_login_server_response(response, &response_obj); +} + +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), RC_JSON_NEW_FIELD("User"), RC_JSON_NEW_FIELD("Token"), RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("Messages"), RC_JSON_NEW_FIELD("DisplayName") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_string(&response->username, &response->response, &fields[2], "User")) + if (!rc_json_get_required_string(&response->username, &response->response, &fields[3], "User")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[3], "Token")) + if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[4], "Token")) return RC_MISSING_VALUE; - rc_json_get_optional_unum(&response->score, &fields[4], "Score", 0); - rc_json_get_optional_unum(&response->num_unread_messages, &fields[5], "Messages", 0); + rc_json_get_optional_unum(&response->score, &fields[5], "Score", 0); + rc_json_get_optional_unum(&response->score_softcore, &fields[6], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[7], "Messages", 0); - rc_json_get_optional_string(&response->display_name, &response->response, &fields[6], "DisplayName", response->username); + rc_json_get_optional_string(&response->display_name, &response->response, &fields[8], "DisplayName", response->username); return RC_OK; } void rc_api_destroy_login_response(rc_api_login_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Start Session --- */ @@ -78,37 +92,103 @@ int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_st return RC_INVALID_STATE; rc_url_builder_init(&builder, &request->buffer, 48); - if (rc_api_url_build_dorequest(&builder, "postactivity", api_params->username, api_params->api_token)) { - /* activity type enum (only 3 is used ) - * 1 = earned achievement - handled by awardachievement - * 2 = logged in - handled by login - * 3 = started playing - * 4 = uploaded achievement - handled by uploadachievement - * 5 = modified achievement - handled by uploadachievement - */ - rc_url_builder_append_unum_param(&builder, "a", 3); - rc_url_builder_append_unum_param(&builder, "m", api_params->game_id); + if (rc_api_url_build_dorequest(&builder, "startsession", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_start_session_server_response(response, &response_obj); +} + +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_unlock_entry_t* unlock; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; + int result; + rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error") + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Unlocks"), + RC_JSON_NEW_FIELD("HardcoreUnlocks"), + RC_JSON_NEW_FIELD("ServerNow") + }; + + rc_json_field_t unlock_entry_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("When") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (rc_json_get_optional_array(&response->num_unlocks, &array_field, &response->response, &fields[2], "Unlocks") && response->num_unlocks) { + response->unlocks = (rc_api_unlock_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + if (rc_json_get_optional_array(&response->num_hardcore_unlocks, &array_field, &response->response, &fields[3], "HardcoreUnlocks") && response->num_hardcore_unlocks) { + response->hardcore_unlocks = (rc_api_unlock_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_hardcore_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->hardcore_unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->hardcore_unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + rc_json_get_optional_unum(&timet, &fields[4], "ServerNow", 0); + response->server_now = (time_t)timet; + + return RC_OK; } void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch User Unlocks --- */ @@ -123,12 +203,23 @@ int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_a rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_user_unlocks_server_response(response, &response_obj); +} + +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), @@ -141,9 +232,9 @@ int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_respons }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -152,5 +243,5 @@ int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_respons } void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c new file mode 100644 index 000000000000..98d8c1fe8590 --- /dev/null +++ b/deps/rcheevos/src/rc_client.c @@ -0,0 +1,5097 @@ +#include "rc_client_internal.h" + +#include "rc_api_info.h" +#include "rc_api_runtime.h" +#include "rc_api_user.h" +#include "rc_consoles.h" +#include "rc_hash.h" + +#include "rapi/rc_api_common.h" + +#include "rcheevos/rc_internal.h" + +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif + +#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 +#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +enum { + RC_CLIENT_ASYNC_NOT_ABORTED = 0, + RC_CLIENT_ASYNC_ABORTED = 1, + RC_CLIENT_ASYNC_DESTROYED = 2 +}; + +typedef struct rc_client_generic_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_generic_callback_data_t; + +typedef struct rc_client_pending_media_t +{ + const char* file_path; + uint8_t* data; + size_t data_size; + rc_client_callback_t callback; + void* callback_userdata; +} rc_client_pending_media_t; + +typedef struct rc_client_load_state_t +{ + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + + rc_client_game_info_t* game; + rc_client_subset_info_t* subset; + rc_client_game_hash_t* hash; + + rc_hash_iterator_t hash_iterator; + rc_client_pending_media_t* pending_media; + + rc_api_start_session_response_t *start_session_response; + + rc_client_async_handle_t async_handle; + + uint8_t progress; + uint8_t outstanding_requests; + uint8_t hash_console_id; +} rc_client_load_state_t; + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when); +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +/* ===== Construction/Destruction ===== */ + +static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ +} + +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function) +{ + rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t)); + if (!client) + return NULL; + + client->state.hardcore = 1; + + client->callbacks.read_memory = read_memory_function; + client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_dummy_event_handler; + rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); + rc_client_set_get_time_millisecs_function(client, NULL); + + rc_mutex_init(&client->state.mutex); + + rc_buffer_init(&client->state.buffer); + + return client; +} + +void rc_client_destroy(rc_client_t* client) +{ + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + { + size_t i; + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i]) + client->state.async_handles[i]->aborted = RC_CLIENT_ASYNC_DESTROYED; + } + + if (client->state.load) { + client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_DESTROYED; + client->state.load = NULL; + } + } + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + rc_buffer_destroy(&client->state.buffer); + + rc_mutex_destroy(&client->state.mutex); + + free(client); +} + +/* ===== Logging ===== */ + +static rc_client_t* g_hash_client = NULL; + +static void rc_client_log_hash_message(const char* message) { + rc_client_log_message(g_hash_client, message); +} + +void rc_client_log_message(const rc_client_t* client, const char* message) +{ + if (client->callbacks.log_call) + client->callbacks.log_call(message, client); +} + +static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args) +{ + if (client->callbacks.log_call) { + char buffer[2048]; + +#ifdef __STDC_WANT_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + vsprintf(buffer, format, args); +#endif + + client->callbacks.log_call(buffer, client); + } +} + +#ifdef RC_NO_VARIADIC_MACROS + +void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +#else + +void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...) +{ + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); +} + +#endif /* RC_NO_VARIADIC_MACROS */ + +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + client->callbacks.log_call = callback; + client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE; +} + +/* ===== Common ===== */ + +static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + + /* round nanoseconds to nearest millisecond and add to seconds */ + return ((rc_clock_t)now.tv_sec * 1000 + ((rc_clock_t)now.tv_nsec / 1000000)); +#elif defined(_WIN32) + static LARGE_INTEGER freq; + LARGE_INTEGER ticks; + + /* Frequency is the number of ticks per second and is guaranteed to not change. */ + if (!freq.QuadPart) { + if (!QueryPerformanceFrequency(&freq)) + return 0; + + /* convert to number of ticks per millisecond to simplify later calculations */ + freq.QuadPart /= 1000; + } + + if (!QueryPerformanceCounter(&ticks)) + return 0; + + return (rc_clock_t)(ticks.QuadPart / freq.QuadPart); +#else + const clock_t clock_now = clock(); + if (sizeof(clock_t) == 4) { + static uint32_t clock_wraps = 0; + static clock_t last_clock = 0; + static time_t last_timet = 0; + const time_t time_now = time(NULL); + + if (last_timet != 0) { + const time_t seconds_per_clock_t = (time_t)(((uint64_t)1 << 32) / CLOCKS_PER_SEC); + if (clock_now < last_clock) { + /* clock() has wrapped */ + ++clock_wraps; + } + else if (time_now - last_timet > seconds_per_clock_t) { + /* it's been long enough that clock() has wrapped and is higher than the last time it was read */ + ++clock_wraps; + } + } + + last_timet = time_now; + last_clock = clock_now; + + return (rc_clock_t)((((uint64_t)clock_wraps << 32) | clock_now) / (CLOCKS_PER_SEC / 1000)); + } + else { + return (rc_clock_t)(clock_now / (CLOCKS_PER_SEC / 1000)); + } +#endif +} + +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler) +{ + client->callbacks.get_time_millisecs = handler ? handler : rc_client_clock_get_now_millisecs; +} + +static void rc_client_begin_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + size_t i; + + rc_mutex_lock(&client->state.mutex); + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (!client->state.async_handles[i]) { + client->state.async_handles[i] = async_handle; + break; + } + } + rc_mutex_unlock(&client->state.mutex); +} + +static int rc_client_end_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted = async_handle->aborted; + + /* if client was destroyed, mutex doesn't exist and we don't need to remove the handle from the collection */ + if (aborted != RC_CLIENT_ASYNC_DESTROYED) { + size_t i; + + rc_mutex_lock(&client->state.mutex); + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i] == async_handle) { + client->state.async_handles[i] = NULL; + break; + } + } + aborted = async_handle->aborted; + + rc_mutex_unlock(&client->state.mutex); + } + + return aborted; +} + +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + if (async_handle && client) { + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = RC_CLIENT_ASYNC_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } +} + +static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) +{ + if (!response->succeeded) { + if (*result == RC_OK) { + *result = RC_API_FAILURE; + if (!response->error_message) + return "Unexpected API failure with no error message"; + } + + if (response->error_message) + return response->error_message; + } + + if (*result != RC_OK) + return rc_error_str(*result); + + return NULL; +} + +static void rc_client_raise_server_error_event(rc_client_t* client, + const char* api, uint32_t related_id, int result, const char* error_message) +{ + rc_client_server_error_t server_error; + rc_client_event_t client_event; + + server_error.api = api; + server_error.error_message = error_message; + server_error.result = result; + server_error.related_id = related_id; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_SERVER_ERROR; + client_event.server_error = &server_error; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_update_disconnect_state(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + uint8_t new_state = RC_CLIENT_DISCONNECT_HIDDEN; + + rc_mutex_lock(&client->state.mutex); + + scheduled_callback = client->state.scheduled_callbacks; + for (; scheduled_callback; scheduled_callback = scheduled_callback->next) { + if (scheduled_callback->callback == rc_client_award_achievement_retry || + scheduled_callback->callback == rc_client_submit_leaderboard_entry_retry) { + new_state = RC_CLIENT_DISCONNECT_VISIBLE; + break; + } + } + + if ((client->state.disconnect & RC_CLIENT_DISCONNECT_VISIBLE) != new_state) { + if (new_state == RC_CLIENT_DISCONNECT_VISIBLE) + client->state.disconnect = RC_CLIENT_DISCONNECT_HIDDEN | RC_CLIENT_DISCONNECT_SHOW_PENDING; + else + client->state.disconnect = RC_CLIENT_DISCONNECT_VISIBLE | RC_CLIENT_DISCONNECT_HIDE_PENDING; + } + else { + client->state.disconnect = new_state; + } + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_raise_disconnect_events(rc_client_t* client) +{ + rc_client_event_t client_event; + uint8_t new_state; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.disconnect & RC_CLIENT_DISCONNECT_SHOW_PENDING) + new_state = RC_CLIENT_DISCONNECT_VISIBLE; + else + new_state = RC_CLIENT_DISCONNECT_HIDDEN; + client->state.disconnect = new_state; + + rc_mutex_unlock(&client->state.mutex); + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = (new_state == RC_CLIENT_DISCONNECT_VISIBLE) ? + RC_CLIENT_EVENT_DISCONNECTED : RC_CLIENT_EVENT_RECONNECTED; + client->callbacks.event_handler(&client_event, client); +} + +static int rc_client_should_retry(const rc_api_server_response_t* server_response) +{ + switch (server_response->http_status_code) { + case 502: /* 502 Bad Gateway */ + /* nginx connection pool full */ + return 1; + + case 503: /* 503 Service Temporarily Unavailable */ + /* site is in maintenance mode */ + return 1; + + case 504: /* 504 Gateway Timeout */ + /* timeout between web server and database server */ + return 1; + + case 429: /* 429 Too Many Requests */ + /* too many unlocks occurred at the same time */ + return 1; + + case 521: /* 521 Web Server is Down */ + /* cloudfare could not find the server */ + return 1; + + case 522: /* 522 Connection Timed Out */ + /* timeout connecting to server from cloudfare */ + return 1; + + case 523: /* 523 Origin is Unreachable */ + /* cloudfare cannot find server */ + return 1; + + case 524: /* 524 A Timeout Occurred */ + /* connection to server from cloudfare was dropped before request was completed */ + return 1; + + case RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR: + /* client provided non-HTTP error (explicitly retryable) */ + return 1; + + case RC_API_SERVER_RESPONSE_CLIENT_ERROR: + /* client provided non-HTTP error (implicitly non-retryable) */ + return 0; + + default: + /* assume any error not handled above where no response was received should be retried */ + if (server_response->body_length == 0 || !server_response->body || !server_response->body[0]) + return 1; + + return 0; + } +} + +static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + if (!buffer) + return RC_INVALID_STATE; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result == RC_OK) + snprintf(buffer, buffer_size, "%s", request.url); + + rc_api_destroy_request(&request); + return result; +} + +/* ===== User ===== */ + +static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data; + rc_client_t* client = login_callback_data->client; + rc_api_login_response_t login_response; + rc_client_load_state_t* load_state; + const char* error_message; + int result; + + result = rc_client_end_async(client, &login_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + rc_client_logout(client); /* logout will reset the user state and call the load game callback */ + + free(login_callback_data); + return; + } + + if (client->state.user == RC_CLIENT_USER_STATE_NONE) { + /* logout was called */ + if (login_callback_data->callback) + login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata); + + free(login_callback_data); + /* logout call will immediately abort load game before this callback gets called */ + return; + } + + result = rc_api_process_login_server_response(&login_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response); + if (error_message) { + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_NONE; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message); + if (login_callback_data->callback) + login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + } + else { + client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username); + + if (strcmp(login_response.username, login_response.display_name) == 0) + client->user.display_name = client->user.username; + else + client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name); + + client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token); + client->user.score = login_response.score; + client->user.score_softcore = login_response.score_softcore; + client->user.num_unread_messages = login_response.num_unread_messages; + + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + + if (login_callback_data->callback) + login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); + } + + rc_api_destroy_login_response(&login_response); + free(login_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, + const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_generic_callback_data_t* callback_data; + rc_api_request_t request; + int result = rc_api_init_login_request(&request, login_request); + const char* error_message = rc_error_str(result); + + if (result == RC_OK) { + rc_mutex_lock(&client->state.mutex); + + if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) { + error_message = "Login already in progress"; + result = RC_INVALID_STATE; + } + client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + rc_mutex_unlock(&client->state.mutex); + } + + if (result != RC_OK) { + callback(result, error_message, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!password || !password[0]) { + callback(RC_INVALID_STATE, "password is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.password = password; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!token || !token[0]) { + callback(RC_INVALID_STATE, "token is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.api_token = token; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +void rc_client_logout(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + if (!client) + return; + + switch (client->state.user) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name); + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + RC_CLIENT_LOG_INFO(client, "Aborting login"); + break; + } + + rc_mutex_lock(&client->state.mutex); + + client->state.user = RC_CLIENT_USER_STATE_NONE; + memset(&client->user, 0, sizeof(client->user)); + + load_state = client->state.load; + + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_load_error(load_state, RC_ABORTED, "Login aborted"); +} + +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) +{ + return (client && client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; +} + +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size) +{ + if (!user) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); +} + +static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset, + rc_client_user_game_summary_t* summary, const uint8_t unlock_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + switch (achievement->public_.category) { + case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + ++summary->num_core_achievements; + summary->points_core += achievement->public_.points; + + if (achievement->public_.unlocked & unlock_bit) { + ++summary->num_unlocked_achievements; + summary->points_unlocked += achievement->public_.points; + } + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { + ++summary->num_unsupported_achievements; + } + + break; + + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + ++summary->num_unofficial_achievements; + break; + + default: + continue; + } + } +} + +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) +{ + const uint8_t unlock_bit = (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + if (!summary) + return; + + memset(summary, 0, sizeof(*summary)); + if (!client || !client->game) + return; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit); + + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ +} + +/* ===== Game ===== */ + +static void rc_client_free_game(rc_client_game_info_t* game) +{ + rc_runtime_destroy(&game->runtime); + + rc_buffer_destroy(&game->buffer); + + free(game); +} + +static void rc_client_free_load_state(rc_client_load_state_t* load_state) +{ + if (load_state->game) + rc_client_free_game(load_state->game); + + if (load_state->start_session_response) { + rc_api_destroy_start_session_response(load_state->start_session_response); + free(load_state->start_session_response); + } + + free(load_state); +} + +static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests) +{ + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = state; + load_state->outstanding_requests += num_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); +} + +static int rc_client_end_load_state(rc_client_load_state_t* load_state) +{ + int remaining_requests = 0; + int aborted = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + if (load_state->outstanding_requests > 0) + --load_state->outstanding_requests; + remaining_requests = load_state->outstanding_requests; + + if (load_state->client->state.load != load_state) + aborted = 1; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (aborted) { + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. As they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) { + /* if one of the callbacks called rc_client_load_error, progress will be set to + * RC_CLIENT_LOAD_STATE_UNKNOWN. There's no need to call the callback with RC_ABORTED + * in that case, as it will have already been called with something more appropriate. */ + if (load_state->progress != RC_CLIENT_LOAD_STATE_UNKNOWN_GAME && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + } + + return -1; + } + + return remaining_requests; +} + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message) +{ + int remaining_requests = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + if (load_state->client->state.load == load_state) + load_state->client->state.load = NULL; + + remaining_requests = load_state->outstanding_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(load_state->client, "Load failed (%d): %s", result, error_message); + + if (load_state->callback) + load_state->callback(result, error_message, load_state->client, load_state->callback_userdata); + + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. as they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) + rc_client_free_load_state(load_state); +} + +static void rc_client_load_aborted(rc_client_load_state_t* load_state) +{ + /* prevent callback from being called when manually aborted */ + load_state->callback = NULL; + + /* mark the game as no longer being loaded */ + rc_client_load_error(load_state, RC_ABORTED, NULL); + + /* decrement the async counter and potentially free the load_state object */ + rc_client_end_load_state(load_state); +} + +static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(achievement->trigger, memref)) { + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + + if (achievement->trigger) + achievement->trigger->state = RC_TRIGGER_STATE_DISABLED; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address); + } + } + } +} + +static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_value_contains_memref(&leaderboard->lboard->value, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else + continue; + + if (leaderboard->lboard) + leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address); + } + } +} + +static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id); + const uint32_t max_address = (regions && regions->num_regions > 0) ? + regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF; + uint8_t buffer[8]; + uint32_t total_count = 0; + uint32_t invalid_count = 0; + + rc_memref_t** last_memref = &game->runtime.memrefs; + rc_memref_t* memref = game->runtime.memrefs; + for (; memref; memref = memref->next) { + if (!memref->value.is_indirect) { + total_count++; + + if (memref->address > max_address || + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + /* invalid address, remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch 0. */ + *last_memref = memref->next; + + rc_client_invalidate_memref_achievements(game, client, memref); + rc_client_invalidate_memref_leaderboards(game, client, memref); + + invalid_count++; + continue; + } + } + + last_memref = &memref->next; + } + + game->max_valid_address = max_address; + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count); +} + +static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_runtime_trigger_t* trigger; + rc_client_subset_info_t* subset; + + if (active_count <= game->runtime.trigger_capacity) { + if (active_count != 0) + memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t)); + } else { + if (game->runtime.triggers) + free(game->runtime.triggers); + + game->runtime.trigger_capacity = active_count; + game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t)); + } + + trigger = game->runtime.triggers; + if (!trigger) { + /* malloc failed, no way to report error, just bail */ + game->runtime.trigger_count = 0; + return; + } + + for (subset = game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + trigger->id = achievement->public_.id; + memcpy(trigger->md5, achievement->md5, 16); + trigger->trigger = achievement->trigger; + ++trigger; + } + } + } + } + + game->runtime.trigger_count = active_count; +} + +static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + ++active_count; + } + + return active_count; +} + +void rc_client_update_active_achievements(rc_client_game_info_t* game) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_count_active_achievements(subset); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if ((achievement->public_.unlocked & active_bit) == 0) { + switch (achievement->public_.state) { + case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: + rc_reset_trigger(achievement->trigger); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + ++active_count; + break; + + case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: + ++active_count; + break; + } + } + else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + + /* if it's active despite being unlocked, and we're in encore mode, leave it active */ + if (client->state.encore_mode) { + ++active_count; + continue; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? + achievement->unlock_time_hardcore : achievement->unlock_time_softcore; + + if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client_event.achievement = &achievement->public_; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state)) + achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; + } + } + + return active_count; +} + +static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client) +{ + const uint8_t active_bit = (client->state.encore_mode) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_client_toggle_hardcore_achievements(game, client, active_bit); +} + +static void rc_client_update_legacy_runtime_leaderboards(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_subset_info_t* subset; + rc_runtime_lboard_t* lboard; + + if (active_count <= game->runtime.lboard_capacity) { + if (active_count != 0) + memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t)); + } else { + if (game->runtime.lboards) + free(game->runtime.lboards); + + game->runtime.lboard_capacity = active_count; + game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t)); + } + + lboard = game->runtime.lboards; + if (!lboard) { + /* malloc failed. no way to report error, just bail */ + game->runtime.lboard_count = 0; + return; + } + + for (subset = game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + lboard->id = leaderboard->public_.id; + memcpy(lboard->md5, leaderboard->md5, 16); + lboard->lboard = leaderboard->lboard; + ++lboard; + } + } + } + } + + game->runtime.lboard_count = active_count; +} + +void rc_client_update_active_leaderboards(rc_client_game_info_t* game) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) + { + switch (leaderboard->public_.state) + { + case RC_CLIENT_LEADERBOARD_STATE_ACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + ++active_count; + break; + } + } + } + + rc_client_update_legacy_runtime_leaderboards(game, active_count); +} + +static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + if (client->state.hardcore) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (client->state.hardcore) + ++active_count; + else + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + rc_client_update_legacy_runtime_leaderboards(game, active_count); +} + +static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(client->game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + game->runtime.lboard_count = 0; +} + +static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, rc_api_unlock_entry_t* unlocks, uint32_t num_unlocks, uint8_t mode) +{ + rc_client_achievement_info_t* start = subset->achievements; + rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; + rc_client_achievement_info_t* scan; + rc_api_unlock_entry_t* unlock = unlocks; + rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks; + + for (; unlock < unlock_stop; ++unlock) { + for (scan = start; scan < stop; ++scan) { + if (scan->public_.id == unlock->achievement_id) { + scan->public_.unlocked |= mode; + + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) + scan->unlock_time_hardcore = unlock->when; + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE) + scan->unlock_time_softcore = unlock->when; + + if (scan == start) + ++start; + else if (scan + 1 == stop) + --stop; + break; + } + } + } +} + +static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response) +{ + rc_client_t* client = load_state->client; + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_STATE_DONE : RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + /* unlocks not available - assume malloc failed */ + if (load_state->callback) + load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); + } + else { + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks, + start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks, + start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load == NULL) + client->game = load_state->game; + rc_mutex_unlock(&client->state.mutex); + + if (client->game != load_state->game) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* if a change media request is pending, kick it off */ + rc_client_pending_media_t* pending_media; + + rc_mutex_lock(&load_state->client->state.mutex); + pending_media = load_state->pending_media; + load_state->pending_media = NULL; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (pending_media) { + rc_client_begin_change_media(client, pending_media->file_path, + pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + /* client->game must be set before calling this function so it can query the console_id */ + rc_client_validate_addresses(load_state->game, client); + + rc_client_activate_achievements(load_state->game, client); + rc_client_activate_leaderboards(load_state->game, client); + + if (load_state->hash->hash[0] != '[') { + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { + /* schedule the periodic ping */ + rc_client_scheduled_callback_data_t* callback_data = rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(callback_data, 0, sizeof(*callback_data)); + callback_data->callback = rc_client_ping; + callback_data->related_id = load_state->game->public_.id; + callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000; + rc_client_schedule_callback(client, callback_data); + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcore %s%s", load_state->game->public_.id, + client->state.hardcore ? "enabled" : "disabled", + (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id); + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + + /* detach the game object so it doesn't get freed by free_load_state */ + load_state->game = NULL; + } + } + + rc_client_free_load_state(load_state); +} + +static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_start_session_response_t start_session_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(load_state->client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_start_session_server_response(&start_session_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else if (outstanding_requests == 0) { + rc_client_activate_game(load_state, &start_session_response); + } + else { + load_state->start_session_response = + (rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t)); + + if (!load_state->start_session_response) { + rc_client_load_error(callback_data, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + } + else { + /* safer to parse the response again than to try to copy it */ + rc_api_process_start_session_response(load_state->start_session_response, server_response->body); + } + } + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void rc_client_begin_start_session(rc_client_load_state_t* load_state) +{ + rc_api_start_session_request_t start_session_params; + rc_client_t* client = load_state->client; + rc_api_request_t start_session_request; + int result; + + memset(&start_session_params, 0, sizeof(start_session_params)); + start_session_params.username = client->user.username; + start_session_params.api_token = client->user.token; + start_session_params.game_id = load_state->hash->game_id; + + result = rc_api_init_start_session_request(&start_session_request, &start_session_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); + rc_api_destroy_request(&start_session_request); + } +} + +static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements) +{ + const rc_api_achievement_definition_t* read; + const rc_api_achievement_definition_t* stop; + rc_client_achievement_info_t* achievements; + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* scan; + rc_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + size_t size; + int trigger_size; + + subset->achievements = NULL; + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + + stop = achievement_definitions + num_achievements; + + /* if not testing unofficial, filter them out */ + if (!load_state->client->state.unofficial_enabled) { + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) + --num_achievements; + } + + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + } + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */ + + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */ + + sizeof(rc_client_achievement_info_t); + rc_buffer_reserve(&load_state->game->buffer, size * num_achievements); + + /* allocate the achievement array */ + size = sizeof(rc_client_achievement_info_t) * num_achievements; + buffer = &load_state->game->buffer; + achievement = achievements = rc_buffer_alloc(buffer, size); + memset(achievements, 0, size); + + /* copy the achievement data */ + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled) + continue; + + achievement->public_.title = rc_buffer_strcpy(buffer, read->title); + achievement->public_.description = rc_buffer_strcpy(buffer, read->description); + snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name); + achievement->public_.id = read->id; + achievement->public_.points = read->points; + achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ? + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, achievement->md5); + + trigger_size = rc_trigger_size(memaddr); + if (trigger_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buffer_reserve(buffer, trigger_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + achievement->trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset); + achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + achievement->created_time = read->created; + achievement->updated_time = read->updated; + + scan = achievement; + while (scan > achievements) { + --scan; + if (strcmp(scan->author, read->author) == 0) { + achievement->author = scan->author; + break; + } + } + if (!achievement->author) + achievement->author = rc_buffer_strcpy(buffer, read->author); + + ++achievement; + } + + subset->achievements = achievements; +} + +uint8_t rc_client_map_leaderboard_format(int format) +{ + switch (format) { + case RC_FORMAT_SECONDS: + case RC_FORMAT_CENTISECS: + case RC_FORMAT_MINUTES: + case RC_FORMAT_SECONDS_AS_MINUTES: + case RC_FORMAT_FRAMES: + return RC_CLIENT_LEADERBOARD_FORMAT_TIME; + + case RC_FORMAT_SCORE: + return RC_CLIENT_LEADERBOARD_FORMAT_SCORE; + + case RC_FORMAT_VALUE: + case RC_FORMAT_FLOAT1: + case RC_FORMAT_FLOAT2: + case RC_FORMAT_FLOAT3: + case RC_FORMAT_FLOAT4: + case RC_FORMAT_FLOAT5: + case RC_FORMAT_FLOAT6: + default: + return RC_CLIENT_LEADERBOARD_FORMAT_VALUE; + } +} + +static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards) +{ + const rc_api_leaderboard_definition_t* read; + const rc_api_leaderboard_definition_t* stop; + rc_client_leaderboard_info_t* leaderboards; + rc_client_leaderboard_info_t* leaderboard; + rc_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + const char* ptr; + size_t size; + int lboard_size; + + subset->leaderboards = NULL; + subset->public_.num_leaderboards = num_leaderboards; + + if (num_leaderboards == 0) + return; + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_lboard_t) /* lboard container */ + + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */ + + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */ + + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */ + + sizeof(rc_client_leaderboard_info_t); + rc_buffer_reserve(&load_state->game->buffer, size * num_leaderboards); + + /* allocate the achievement array */ + size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; + buffer = &load_state->game->buffer; + leaderboard = leaderboards = rc_buffer_alloc(buffer, size); + memset(leaderboards, 0, size); + + /* copy the achievement data */ + read = leaderboard_definitions; + stop = read + num_leaderboards; + do { + leaderboard->public_.title = rc_buffer_strcpy(buffer, read->title); + leaderboard->public_.description = rc_buffer_strcpy(buffer, read->description); + leaderboard->public_.id = read->id; + leaderboard->public_.format = rc_client_map_leaderboard_format(read->format); + leaderboard->public_.lower_is_better = read->lower_is_better; + leaderboard->format = (uint8_t)read->format; + leaderboard->hidden = (uint8_t)read->hidden; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, leaderboard->md5); + + ptr = strstr(memaddr, "VAL:"); + if (ptr != NULL) { + /* calculate the DJB2 hash of the VAL portion of the string*/ + uint32_t hash = 5381; + ptr += 4; /* skip 'VAL:' */ + while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) + hash = (hash << 5) + hash + *ptr++; + leaderboard->value_djb2 = hash; + } + + lboard_size = rc_lboard_size(memaddr); + if (lboard_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buffer_reserve(buffer, lboard_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset); + leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + ++leaderboard; + ++read; + } while (read < stop); + + subset->leaderboards = leaderboards; +} + +static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title) +{ + const char* subset_prefix = strstr(title, "[Subset - "); + if (subset_prefix) { + const char* start = subset_prefix + 10; + const char* stop = strstr(start, "]"); + const size_t len = stop - start; + char* result = (char*)rc_buffer_alloc(&game->buffer, len + 1); + + memcpy(result, start, len); + result[len] = '\0'; + return result; + } + + return NULL; +} + +static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_game_data_response_t fetch_game_data_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(load_state->client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(load_state, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = fetch_game_data_response.id; + subset->active = 1; + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", fetch_game_data_response.image_name); + load_state->subset = subset; + + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && + fetch_game_data_response.console_id != load_state->game->public_.console_id) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", + fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + } + + /* kick off the start session request while we process the game data */ + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + /* we can't unlock achievements without a session, lock spectator mode for the game */ + load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED; + } + else { + rc_client_begin_start_session(load_state); + } + + /* process the game data */ + rc_client_copy_achievements(load_state, subset, + fetch_game_data_response.achievements, fetch_game_data_response.num_achievements); + rc_client_copy_leaderboards(load_state, subset, + fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards); + + if (!load_state->game->subsets) { + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + load_state->game->subsets = subset; + load_state->game->public_.badge_name = subset->public_.badge_name; + load_state->game->public_.console_id = fetch_game_data_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); + + subset->public_.title = load_state->game->public_.title; + + if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); + } + } + } + else { + rc_client_subset_info_t* scan; + + /* subset - extract subset title */ + subset->public_.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title); + if (!subset->public_.title) { + const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public_.title); + if (core_subset_title) { + rc_client_subset_info_t* scan = load_state->game->subsets; + for (; scan; scan = scan->next) { + if (scan->public_.title == load_state->game->public_.title) { + scan->public_.title = core_subset_title; + break; + } + } + } + + subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + } + + /* append to subset list */ + scan = load_state->game->subsets; + while (scan->next) + scan = scan->next; + scan->next = subset; + } + + if (load_state->client->callbacks.post_process_game_data_response) { + load_state->client->callbacks.post_process_game_data_response(server_response, + &fetch_game_data_response, load_state->client, load_state->callback_userdata); + } + + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (outstanding_requests == 0) + rc_client_activate_game(load_state, load_state->start_session_response); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + + if (load_state->hash->game_id == 0) { + char hash[33]; + + if (rc_hash_iterate(hash, &load_state->hash_iterator)) { + /* found another hash to try */ + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + rc_client_load_game(load_state, hash, NULL); + return; + } + + if (load_state->game->media_hash && + load_state->game->media_hash->game_hash && + load_state->game->media_hash->game_hash->next) { + /* multiple hashes were tried, create a CSV */ + struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash; + int count = 1; + char* ptr; + size_t size; + + size = strlen(game_hash->hash) + 1; + while (game_hash->next) { + game_hash = game_hash->next; + size += strlen(game_hash->hash) + 1; + count++; + } + + ptr = (char*)rc_buffer_alloc(&load_state->game->buffer, size); + ptr += size - 1; + *ptr = '\0'; + game_hash = load_state->game->media_hash->game_hash; + do { + const size_t hash_len = strlen(game_hash->hash); + ptr -= hash_len; + memcpy(ptr, game_hash->hash, hash_len); + + game_hash = game_hash->next; + if (!game_hash) + break; + + ptr--; + *ptr = ','; + } while (1); + + load_state->game->public_.hash = ptr; + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + } else { + /* only a single hash was tried, capture it */ + load_state->game->public_.console_id = load_state->hash_console_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + load_state->game->public_.title = "Unknown Game"; + load_state->game->public_.badge_name = ""; + client->game = load_state->game; + load_state->game = NULL; + + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + + if (load_state->hash->hash[0] != '[') { + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + /* done with the hashing code, release the global pointer */ + g_hash_client = NULL; + + rc_mutex_lock(&client->state.mutex); + result = client->state.user; + if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + load_state->progress = RC_CLIENT_LOAD_STATE_AWAIT_LOGIN; + rc_mutex_unlock(&client->state.mutex); + + switch (result) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + /* do nothing, this function will be called again after login completes */ + return; + + default: + rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED)); + return; + } + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = client->user.username; + fetch_game_data_request.api_token = client->user.token; + fetch_game_data_request.game_id = load_state->hash->game_id; + + result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, 1); + + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id); + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + + rc_api_destroy_request(&request); +} + +static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (error_message) { + rc_client_end_load_state(load_state); + rc_client_load_error(load_state, result, error_message); + } + else { + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = resolve_hash_response.game_id; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + /* have to call end_load_state after updating hash in case the load_state gets free'd */ + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_begin_fetch_game_data(load_state); + } + } + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) +{ + rc_client_game_hash_t* game_hash; + + rc_mutex_lock(&client->state.mutex); + game_hash = client->hashes; + while (game_hash) { + if (strcasecmp(game_hash->hash, hash) == 0) + break; + + game_hash = game_hash->next; + } + + if (!game_hash) { + game_hash = rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); + memset(game_hash, 0, sizeof(*game_hash)); + snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash); + game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID; + game_hash->next = client->hashes; + client->hashes = game_hash; + } + rc_mutex_unlock(&client->state.mutex); + + return game_hash; +} + +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, + const char* hash, const char* file_path) +{ + rc_client_t* client = load_state->client; + rc_client_game_hash_t* old_hash; + + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + rc_buffer_init(&load_state->game->buffer); + rc_runtime_init(&load_state->game->runtime); + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + old_hash = load_state->hash; + load_state->hash = rc_client_find_game_hash(client, hash); + + if (file_path) { + rc_client_media_hash_t* media_hash = + (rc_client_media_hash_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(*media_hash)); + media_hash->game_hash = load_state->hash; + media_hash->path_djb2 = rc_djb2(file_path); + media_hash->next = load_state->game->media_hash; + load_state->game->media_hash = media_hash; + } + else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) { + load_state->game->media_hash->game_hash = load_state->hash; + } + + if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return NULL; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, 1); + + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client); + + rc_api_destroy_request(&request); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + rc_client_begin_fetch_game_data(load_state); + } + + return (client->state.load == load_state) ? &load_state->async_handle : NULL; +} + +rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) +{ + if (client && client->state.load) + return &client->state.load->hash_iterator; + + return NULL; +} + +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + return rc_client_load_game(load_state, hash, NULL); +} + +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + char hash[33]; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (data) { + if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data); + } + } + else if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path); + } + else { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (!file_path) + file_path = "?"; + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + if (console_id == RC_CONSOLE_UNKNOWN) { + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + + if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + } + else { + /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ + load_state->hash_console_id = console_id; + + if (data != NULL) { + if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + else { + if (!rc_hash_generate_from_file(hash, console_id, file_path)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + rc_client_subset_info_t* subset; + + for (subset = game->subsets; subset; subset = subset->next) { + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE && + achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } + + rc_client_hide_progress_tracker(client, game); +} + +void rc_client_unload_game(rc_client_t* client) +{ + rc_client_game_info_t* game; + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + game = client->game; + client->game = NULL; + + if (client->state.load) { + /* this mimics rc_client_abort_async without nesting the lock */ + client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED; + client->state.load = NULL; + } + + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) + client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; + + if (game != NULL) + rc_client_game_mark_ui_to_be_hidden(client, game); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; + + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) { + *last = next->next; + continue; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); + + if (game != NULL) { + rc_client_raise_pending_events(client, game); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id); + rc_client_free_game(game); + } +} + +static void rc_client_change_media(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + if (game_hash->game_id == client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash); + } + else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + RC_CLIENT_LOG_INFO(client, "Switching to unknown media"); + } + else if (game_hash->game_id == 0) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash); + } + + client->game->public_.hash = game_hash->hash; + callback(RC_OK, NULL, client, callback_userdata); +} + +static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + + int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + const int async_aborted = rc_client_end_async(client, &load_state->async_handle); + if (async_aborted) { + if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Media change aborted"); + /* if lookup succeeded, still capture the new hash */ + if (result == RC_OK) + load_state->hash->game_id = resolve_hash_response.game_id; + } + } + else if (client->game != load_state->game) { + /* loaded game changed. return success regardless of result */ + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (error_message) { + load_state->callback(result, error_message, client, load_state->callback_userdata); + } + else { + load_state->hash->game_id = resolve_hash_response.game_id; + + if (resolve_hash_response.game_id == 0 && client->state.hardcore) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", load_state->hash->hash); + rc_client_set_hardcore_enabled(client, 0); + client->game->public_.hash = load_state->hash->hash; /* do still update the loaded hash */ + load_state->callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, load_state->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + rc_client_change_media(client, load_state->hash, load_state->callback, load_state->callback_userdata); + } + } + + free(load_state); + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_game_hash_t* game_hash = NULL; + rc_client_media_hash_t* media_hash; + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + uint32_t path_djb2; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!data && !file_path) { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) { + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + pending_media->file_path = strdup(file_path); + pending_media->callback = callback; + pending_media->callback_userdata = callback_userdata; + if (data && data_size) { + pending_media->data_size = data_size; + pending_media->data = (uint8_t*)malloc(data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + memcpy(pending_media->data, data, data_size); + } + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return NULL; + } + + /* still waiting for game data */ + if (pending_media) + return NULL; + + /* check to see if we've already hashed this file */ + path_djb2 = rc_djb2(file_path); + rc_mutex_lock(&client->state.mutex); + for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) { + if (media_hash->path_djb2 == path_djb2) { + game_hash = media_hash->game_hash; + break; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!game_hash) { + char hash[33]; + int result; + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (data != NULL) + result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); + else + result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); + + g_hash_client = NULL; + + if (!result) { + /* when changing discs, if the disc is not supported by the system, allow it. this is + * primarily for games that support user-provided audio CDs, but does allow using discs + * from other systems for games that leverage user-provided discs. */ + strcpy_s(hash, sizeof(hash), "[NO HASH]"); + } + + game_hash = rc_client_find_game_hash(client, hash); + + media_hash = (rc_client_media_hash_t*)rc_buffer_alloc(&game->buffer, sizeof(*media_hash)); + media_hash->game_hash = game_hash; + media_hash->path_djb2 = path_djb2; + + rc_mutex_lock(&client->state.mutex); + media_hash->next = game->media_hash; + game->media_hash = media_hash; + rc_mutex_unlock(&client->state.mutex); + + if (!result) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + } + + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + else { + /* call the server to make sure the hash is valid for the loaded game */ + rc_client_load_state_t* callback_data; + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = game_hash->hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + callback(result, rc_error_str(result), client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client = client; + callback_data->hash = game_hash; + callback_data->game = game; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; + } +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + return (client && client->game) ? &client->game->public_ : NULL; +} + +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size) +{ + if (!game) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); +} + +/* ===== Subsets ===== */ + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata) +{ + char buffer[32]; + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return; + } + + if (!client->game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return; + } + + snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)subset_id); + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + load_state->game = client->game; + load_state->hash = rc_client_find_game_hash(client, buffer); + load_state->hash->game_id = subset_id; + client->state.load = load_state; + + rc_client_begin_fetch_game_data(load_state); +} + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->public_.id == subset_id) + return &subset->public_; + } + + return NULL; +} + +/* ===== Achievements ===== */ + +static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) +{ + uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN; + uint32_t new_measured_value = 0; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + return; + + achievement->public_.measured_progress[0] = '\0'; + + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { + /* achievement unlocked */ + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + } + else { + /* active achievement */ + new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + if (achievement->trigger) { + if (achievement->trigger->measured_target) { + if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) { + /* value hasn't been initialized yet, leave progress string empty */ + } + else if (achievement->trigger->measured_value == 0) { + /* value is 0, leave progress string empty. update progress to 0.0 */ + achievement->public_.measured_percent = 0.0; + } + else { + /* clamp measured value at target (can't get more than 100%) */ + new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ? + achievement->trigger->measured_target : achievement->trigger->measured_value; + + achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target; + + if (!achievement->trigger->measured_as_percent) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target); + } + else if (achievement->public_.measured_percent >= 1.0) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu%%", (unsigned long)achievement->public_.measured_percent); + } + } + } + + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE; + else if (achievement->public_.measured_percent >= 80.0) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE; + } + } + + if (new_bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED && achievement->public_.unlock_time >= recent_unlock_time) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + + achievement->public_.bucket = new_bucket; +} + +static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_achievement_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static int rc_client_compare_achievement_unlock_times(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + if (unlock_b->unlock_time == unlock_a->unlock_time) + return 0; + return (unlock_b->unlock_time < unlock_a->unlock_time) ? -1 : 1; +} + +static int rc_client_compare_achievement_progress(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + if (unlock_b->measured_percent == unlock_a->measured_percent) { + if (unlock_a->id == unlock_b->id) + return 0; + return (unlock_a->id < unlock_b->id) ? -1 : 1; + } + return (unlock_b->measured_percent < unlock_a->measured_percent) ? -1 : 1; +} + +static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) +{ + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { + switch (bucket) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + default: + return bucket; + } + } + + return bucket; +} + +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_client_achievement_t** bucket_achievements; + rc_client_achievement_t** achievement_ptr; + rc_client_achievement_bucket_t* bucket_ptr; + rc_client_achievement_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[16]; + uint32_t num_buckets; + uint32_t num_achievements; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED + }; + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_achievement_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + } + } + } + + num_buckets = 0; + num_achievements = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_achievements += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) { + ++num_buckets; + break; + } + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); + + list = (rc_client_achievement_list_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + bucket_ptr = list->buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + + if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) + qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE) + qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); + + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_has_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + result = 0; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + if (subset->public_.num_achievements > 0) { + result = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static const rc_client_achievement_t* rc_client_subset_get_achievement_info( + rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.id == id) { + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_mutex_lock((rc_mutex_t*)(&client->state.mutex)); + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex)); + return &achievement->public_; + } + } + + return NULL; +} + +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id); + if (achievement != NULL) + return achievement; + } + + return NULL; +} + +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) +{ + const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? + RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + if (!achievement || !achievement->badge_name[0]) + return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); +} + +typedef struct rc_client_award_achievement_callback_data_t +{ + uint32_t id; + uint32_t retry_count; + uint8_t hardcore; + const char* game_hash; + time_t unlock_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_award_achievement_callback_data_t; + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); + +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data->data; + + rc_client_award_achievement_server_call(ach_data); +} + +static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data; + rc_api_award_achievement_response_t award_achievement_response; + + int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response); + + if (error_message) { + if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, result, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message); + rc_client_award_achievement_server_call(ach_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (ach_data->retry_count > 8) ? 120 : (1 << (ach_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay); + + if (!ach_data->scheduled_callback_data) { + ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data)); + if (!ach_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry; + ach_data->scheduled_callback_data->data = ach_data; + ach_data->scheduled_callback_data->related_id = ach_data->id; + } + + ach_data->scheduled_callback_data->when = + ach_data->client->callbacks.get_time_millisecs(ach_data->client) + delay * 1000; + + rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data); + + rc_client_update_disconnect_state(ach_data->client); + return; + } + } + else { + ach_data->client->user.score = award_achievement_response.new_player_score; + ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore; + + if (award_achievement_response.awarded_achievement_id != ach_data->id) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message); + } + else { + if (award_achievement_response.response.error_message) { + /* previously unlocked achievements are returned as a success with an error message */ + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u", + ach_data->id, ach_data->retry_count + 1, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u", + ach_data->id, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + + if (award_achievement_response.achievements_remaining == 0) { + rc_client_subset_info_t* subset; + for (subset = ach_data->client->game->subsets; subset; subset = subset->next) { + if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE && + rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) { + if (subset->public_.id == ach_data->client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + + /* TODO: subset mastery notification */ + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + } + } + } + } + } + } + + if (ach_data->retry_count) + rc_client_update_disconnect_state(ach_data->client); + + if (ach_data->scheduled_callback_data) + free(ach_data->scheduled_callback_data); + free(ach_data); +} + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data) +{ + rc_api_award_achievement_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = ach_data->client->user.username; + api_params.api_token = ach_data->client->user.token; + api_params.achievement_id = ach_data->id; + api_params.hardcore = ach_data->hardcore; + api_params.game_hash = ach_data->game_hash; + + result = rc_api_init_award_achievement_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); + free(ach_data); + return; + } + + ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement) +{ + rc_client_award_achievement_callback_data_t* callback_data; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.hardcore) { + achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL); + if (achievement->unlock_time_softcore == 0) + achievement->unlock_time_softcore = achievement->unlock_time_hardcore; + + /* adjust score now - will get accurate score back from server */ + client->user.score += achievement->public_.points; + } + else { + achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL); + + /* adjust score now - will get accurate score back from server */ + client->user.score_softcore += achievement->public_.points; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlocked |= (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_unlock(&client->state.mutex); + + if (client->callbacks.can_submit_achievement_unlock && + !client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id); + return; + } + + /* can't unlock unofficial achievements on the server */ + if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + /* don't actually unlock achievements when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id); + rc_client_raise_server_error_event(client, "award_achievement", achievement->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = achievement->public_.id; + callback_data->hardcore = client->state.hardcore; + callback_data->game_hash = client->game->public_.hash; + callback_data->unlock_time = achievement->public_.unlock_time; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); + rc_client_award_achievement_server_call(callback_data); +} + +static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + + rc_reset_trigger(trigger); + } +} + +static void rc_client_reset_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_achievements(subset); +} + +/* ===== Leaderboards ===== */ + +static rc_client_leaderboard_info_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.id == id) + return leaderboard; + } + + return NULL; +} + +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_leaderboard_info_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id); + if (leaderboard != NULL) + return &leaderboard->public_; + } + + return NULL; +} + +static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive"; + case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active"; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_leaderboard_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping) +{ + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED; + + default: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + } +} + +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_leaderboard_t** bucket_leaderboards; + rc_client_leaderboard_t** leaderboard_ptr; + rc_client_leaderboard_bucket_t* bucket_ptr; + rc_client_leaderboard_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[8]; + uint32_t num_buckets; + uint32_t num_leaderboards; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ALL, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED + }; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_leaderboard_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->hidden) + continue; + + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + bucket_counts[leaderboard->bucket]++; + } + } + + num_buckets = 0; + num_leaderboards = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_leaderboards += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); + + list = (rc_client_leaderboard_list_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + bucket_ptr = list->buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_has_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + result = 0; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + if (subset->public_.num_leaderboards > 0) { + result = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker; + rc_client_leaderboard_tracker_info_t* available_tracker = NULL; + + for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) { + if (tracker->reference_count == 0) { + if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + available_tracker = tracker; + + continue; + } + + if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format) + continue; + + if (tracker->raw_value != leaderboard->value) { + /* if the value comes from tracking hits, we can't assume the trackers started in the + * same frame, so we can't share the tracker */ + if (tracker->value_from_hits) + continue; + + /* value has changed. prepare an update event */ + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } + + /* attach to the existing tracker */ + ++tracker->reference_count; + tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + leaderboard->tracker = tracker; + leaderboard->public_.tracker_value = tracker->public_.display; + return; + } + + if (!available_tracker) { + rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers; + + available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buffer_alloc(&game->buffer, sizeof(*available_tracker)); + memset(available_tracker, 0, sizeof(*available_tracker)); + available_tracker->public_.id = 1; + + for (tracker = *next; tracker; next = &tracker->next, tracker = *next) + available_tracker->public_.id++; + + *next = available_tracker; + } + + /* update the claimed tracker */ + available_tracker->reference_count = 1; + available_tracker->value_djb2 = leaderboard->value_djb2; + available_tracker->format = leaderboard->format; + available_tracker->raw_value = leaderboard->value; + available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW; + available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value); + leaderboard->tracker = available_tracker; + leaderboard->public_.tracker_value = available_tracker->public_.display; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; +} + +void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + leaderboard->tracker = NULL; + + if (tracker && --tracker->reference_count == 0) { + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + if (tracker && tracker->raw_value != leaderboard->value) { + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +typedef struct rc_client_submit_leaderboard_entry_callback_data_t +{ + uint32_t id; + int32_t score; + uint32_t retry_count; + const char* game_hash; + time_t submit_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_submit_leaderboard_entry_callback_data_t; + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data); + +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; + + rc_client_submit_leaderboard_entry_server_call(lboard_data); +} + +static void rc_client_raise_scoreboard_event(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data, + const rc_api_submit_lboard_entry_response_t* response) +{ + rc_client_leaderboard_scoreboard_t sboard; + rc_client_event_t client_event; + rc_client_subset_info_t* subset; + rc_client_t* client = lboard_data->client; + rc_client_leaderboard_info_t* leaderboard = NULL; + + if (!client || !client->game) + return; + + for (subset = client->game->subsets; subset; subset = subset->next) { + leaderboard = rc_client_subset_get_leaderboard_info(subset, lboard_data->id); + if (leaderboard != NULL) + break; + } + if (leaderboard == NULL) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Trying to raise scoreboard for unknown leaderboard %u", lboard_data->id); + return; + } + + memset(&sboard, 0, sizeof(sboard)); + sboard.leaderboard_id = lboard_data->id; + rc_format_value(sboard.submitted_score, sizeof(sboard.submitted_score), response->submitted_score, leaderboard->format); + rc_format_value(sboard.best_score, sizeof(sboard.best_score), response->best_score, leaderboard->format); + sboard.new_rank = response->new_rank; + sboard.num_entries = response->num_entries; + sboard.num_top_entries = response->num_top_entries; + if (sboard.num_top_entries > 0) { + sboard.top_entries = (rc_client_leaderboard_scoreboard_entry_t*)calloc( + response->num_top_entries, sizeof(rc_client_leaderboard_scoreboard_entry_t)); + if (sboard.top_entries != NULL) { + uint32_t i; + for (i = 0; i < response->num_top_entries; i++) { + sboard.top_entries[i].username = response->top_entries[i].username; + sboard.top_entries[i].rank = response->top_entries[i].rank; + rc_format_value(sboard.top_entries[i].score, sizeof(sboard.top_entries[i].score), response->top_entries[i].score, + leaderboard->format); + } + } + } + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD; + client_event.leaderboard = &leaderboard->public_; + client_event.leaderboard_scoreboard = &sboard; + + lboard_data->client->callbacks.event_handler(&client_event, lboard_data->client); + + if (sboard.top_entries != NULL) { + free(sboard.top_entries); + } +} + +static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data; + rc_api_submit_lboard_entry_response_t submit_lboard_entry_response; + + int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response); + + if (error_message) { + if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, result, submit_lboard_entry_response.response.error_message); + } + else if (lboard_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message); + rc_client_submit_leaderboard_entry_server_call(lboard_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (lboard_data->retry_count > 8) ? 120 : (1 << (lboard_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay); + + if (!lboard_data->scheduled_callback_data) { + lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data)); + if (!lboard_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry; + lboard_data->scheduled_callback_data->data = lboard_data; + lboard_data->scheduled_callback_data->related_id = lboard_data->id; + } + + lboard_data->scheduled_callback_data->when = + lboard_data->client->callbacks.get_time_millisecs(lboard_data->client) + delay * 1000; + + rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data); + + rc_client_update_disconnect_state(lboard_data->client); + return; + } + } + else { + /* raise event for scoreboard */ + if (lboard_data->retry_count < 2) { + rc_client_raise_scoreboard_event(lboard_data, &submit_lboard_entry_response); + } + + /* not currently doing anything with the response */ + if (lboard_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts", + lboard_data->id, lboard_data->score, lboard_data->retry_count); + } + } + + if (lboard_data->retry_count) + rc_client_update_disconnect_state(lboard_data->client); + + if (lboard_data->scheduled_callback_data) + free(lboard_data->scheduled_callback_data); + free(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data) +{ + rc_api_submit_lboard_entry_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = lboard_data->client->user.username; + api_params.api_token = lboard_data->client->user.token; + api_params.leaderboard_id = lboard_data->id; + api_params.score = lboard_data->score; + api_params.game_hash = lboard_data->game_hash; + + result = rc_api_init_submit_lboard_entry_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); + return; + } + + lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_submit_leaderboard_entry_callback_data_t* callback_data; + + if (client->callbacks.can_submit_leaderboard_entry && + !client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id); + return; + } + + /* don't actually submit leaderboard entries when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + return; + } + + callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id); + rc_client_raise_server_error_event(client, "submit_lboard_entry", leaderboard->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = leaderboard->public_.id; + callback_data->score = leaderboard->value; + callback_data->game_hash = client->game->public_.hash; + callback_data->submit_time = time(NULL); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + rc_client_submit_leaderboard_entry_server_call(callback_data); +} + +static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard) + continue; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_reset_lboard(lboard); + break; + } + } +} + +static void rc_client_reset_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_leaderboards(client->game, subset); +} + +typedef struct rc_client_fetch_leaderboard_entries_callback_data_t { + rc_client_t* client; + rc_client_fetch_leaderboard_entries_callback_t callback; + void* callback_userdata; + uint32_t leaderboard_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_leaderboard_entries_callback_data_t; + +static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data; + rc_client_t* client = lbinfo_callback_data->client; + rc_api_fetch_leaderboard_info_response_t lbinfo_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &lbinfo_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted"); + } + free(lbinfo_callback_data); + return; + } + + result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message); + lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries; + size_t needed_size = list_size; + uint32_t i; + + for (i = 0; i < lbinfo_response.num_entries; i++) + needed_size += strlen(lbinfo_response.entries[i].username) + 1; + + list = (rc_client_leaderboard_entry_list_t*)malloc(needed_size); + if (!list) { + lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)list + sizeof(*list)); + char* user = (char*)((uint8_t*)list + list_size); + const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries; + const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries; + const size_t logged_in_user_len = strlen(client->user.display_name) + 1; + list->user_index = -1; + + for (; lbentry < stop; ++lbentry, ++entry) { + const size_t len = strlen(lbentry->username) + 1; + entry->user = user; + memcpy(user, lbentry->username, len); + user += len; + + if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0) + list->user_index = (int)(entry - list->entries); + + entry->index = lbentry->index; + entry->rank = lbentry->rank; + entry->submitted = lbentry->submitted; + + rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format); + } + + list->num_entries = lbinfo_response.num_entries; + + lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response); + free(lbinfo_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client, + const rc_api_fetch_leaderboard_info_request_t* lbinfo_request, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_api_request_t request; + int result; + const char* error_message; + + result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->leaderboard_id = lbinfo_request->leaderboard_id; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.first_entry = first_entry; + lbinfo_request.count = count; + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.username = client->user.username; + lbinfo_request.count = count; + + if (!lbinfo_request.username) { + callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata); + return NULL; + } + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size) +{ + if (!entry) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user); +} + +/* ===== Rich Presence ===== */ + +static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_t* client = (rc_client_t*)callback_data; + rc_api_ping_response_t response; + + int result = rc_api_process_ping_server_response(&response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response); + if (error_message) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message); + } + + rc_api_destroy_ping_response(&response); +} + +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_api_ping_request_t api_params; + rc_api_request_t request; + char buffer[256]; + int result; + + if (!client->callbacks.rich_presence_override || + !client->callbacks.rich_presence_override(client, buffer, sizeof(buffer))) { + rc_mutex_lock(&client->state.mutex); + + rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer), + client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + } + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public_.id; + api_params.rich_presence = buffer; + + result = rc_api_init_ping_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); + } + else { + client->callbacks.server_call(&request, rc_client_ping_callback, client, client); + } + + callback_data->when = now + 120 * 1000; + rc_client_schedule_callback(client, callback_data); +} + +int rc_client_has_rich_presence(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (!client->game->runtime.richpresence || !client->game->runtime.richpresence->richpresence) + return 0; + + return 1; +} + +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size) +{ + int result; + + if (!client || !client->game || !buffer) + return 0; + + rc_mutex_lock(&client->state.mutex); + + result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size, + client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + if (result == 0) + result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title); + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (client) + client->callbacks.event_handler = handler; +} + +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + if (client) + client->callbacks.read_memory = handler; +} + +static void rc_client_invalidate_processing_memref(rc_client_t* client) +{ + rc_memref_t** next_memref = &client->game->runtime.memrefs; + rc_memref_t* memref; + + /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ + if (!client->state.processing_memref) + return; + + /* invalid memref. remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch the current value. */ + while ((memref = *next_memref) != NULL) { + if (memref == client->state.processing_memref) { + *next_memref = memref->next; + break; + } + next_memref = &memref->next; + } + + rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); + rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); + + client->state.processing_memref = NULL; +} + +static uint32_t rc_client_peek_le(uint32_t address, uint32_t num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint32_t value = 0; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + if (num_bytes <= sizeof(value)) { + num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client); + if (num_read == num_bytes) + return value; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +static uint32_t rc_client_peek(uint32_t address, uint32_t num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint8_t buffer[4]; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + switch (num_bytes) { + case 1: + num_read = client->callbacks.read_memory(address, buffer, 1, client); + if (num_read == 1) + return buffer[0]; + break; + case 2: + num_read = client->callbacks.read_memory(address, buffer, 2, client); + if (num_read == 2) + return buffer[0] | (buffer[1] << 8); + break; + case 3: + num_read = client->callbacks.read_memory(address, buffer, 3, client); + if (num_read == 3) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16); + break; + case 4: + num_read = client->callbacks.read_memory(address, buffer, 4, client); + if (num_read == 4) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + break; + default: + break; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +void rc_client_set_legacy_peek(rc_client_t* client, int method) +{ + if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { + union { + uint32_t whole; + uint8_t parts[4]; + } u; + u.whole = 1; + method = (u.parts[0] == 1) ? + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED; + } + + client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ? + rc_client_peek_le : rc_client_peek; +} + +int rc_client_is_processing_required(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (client->game->runtime.trigger_count || client->game->runtime.lboard_count) + return 1; + + return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence); +} + +static void rc_client_update_memref_values(rc_client_t* client) +{ + rc_memref_t* memref = client->game->runtime.memrefs; + uint32_t value; + int invalidated_memref = 0; + + for (; memref; memref = memref->next) { + if (memref->value.is_indirect) + continue; + + client->state.processing_memref = memref; + + value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + + if (client->state.processing_memref) { + rc_update_memref_value(&memref->value, value); + } + else { + /* if the peek function cleared the processing_memref, the memref was invalidated */ + invalidated_memref = 1; + } + } + + client->state.processing_memref = NULL; + + if (invalidated_memref) + rc_client_update_active_achievements(client->game); +} + +static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + int old_state, new_state; + uint32_t old_measured_value; + + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL); + + /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_value <= trigger->measured_target && + rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { + + /* only show a popup for the achievement closest to triggering */ + float progress = (float)trigger->measured_value / (float)trigger->measured_target; + + if (trigger->measured_as_percent) { + /* if reporting the measured value as a percentage, only show the popup if the percentage changes */ + const uint32_t old_percent = (uint32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const uint32_t new_percent = (uint32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent == new_percent) + progress = -1.0; + } + + if (progress > client->game->progress_tracker.progress) { + client->game->progress_tracker.progress = progress; + client->game->progress_tracker.achievement = achievement; + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE; + } + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + + /* raise events for each of the possible new states */ + if (new_state == RC_TRIGGER_STATE_TRIGGERED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED; + else if (new_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } +} + +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (game->progress_tracker.hide_callback && + game->progress_tracker.hide_callback->when && + game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0); + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + } +} + +static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + + rc_mutex_lock(&client->state.mutex); + if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + client->game->progress_tracker.hide_callback->when = 0; + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + } + rc_mutex_unlock(&client->state.mutex); + + if (client_event.type) + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + if (!game->progress_tracker.hide_callback) { + game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) + rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t)); + game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed; + } + + if (game->progress_tracker.hide_callback->when == 0) + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW; + else + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; + + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, + client->callbacks.get_time_millisecs(client) + 2 * 1000); +} + +static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + switch (game->progress_tracker.action) { + case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + break; + case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + break; + default: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + break; + } + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE; + + client_event.achievement = &game->progress_tracker.achievement->public_; + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + rc_client_event_t client_event; + time_t recent_unlock_time = 0; + + memset(&client_event, 0, sizeof(client_event)); + + for (; achievement < stop; ++achievement) { + if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE) + continue; + + /* kick off award achievement request first */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + rc_client_award_achievement(client, achievement); + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS; + } + + /* update display state */ + if (recent_unlock_time == 0) + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + + /* raise events */ + client_event.achievement = &achievement->public_; + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + client->callbacks.event_handler(&client_event, client); + } + + /* clear pending flags */ + achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + int old_state, new_state; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + default: + if (!lboard) + continue; + + break; + } + + old_state = lboard->state; + new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL); + + switch (new_state) { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (old_state != RC_LBOARD_STATE_STARTED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED; + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + } + else { + rc_client_update_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (old_state != RC_LBOARD_STATE_CANCELED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED; + + if (old_state != RC_LBOARD_STATE_STARTED) + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + else + rc_client_update_leaderboard_tracker(client->game, leaderboard); + + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + } + + if (leaderboard->pending_events) + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } +} + +static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = game->leaderboard_trackers; + for (; tracker; tracker = tracker->next) { + if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard_tracker = &tracker->public_; + + /* update display text for new trackers or updated trackers */ + if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE)) + rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format); + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) { + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + /* request to show and hide in the same frame - ignore the event */ + } + else { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + client->callbacks.event_handler(&client_event, client); + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + client->callbacks.event_handler(&client_event, client); + } + + tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard = &leaderboard->public_; + + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) { + /* kick off submission request before raising event */ + rc_client_submit_leaderboard_entry(client, leaderboard); + + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + client->callbacks.event_handler(&client_event, client); + } + + leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE; + } +} + +static void rc_client_reset_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; + + for (subset = client->game->subsets; subset; subset = subset->next) + subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE; +} + +static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + /* raise any pending achievement events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT) + rc_client_raise_achievement_events(client, subset); + + /* raise any pending leaderboard events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD) + rc_client_raise_leaderboard_events(client, subset); + + /* raise mastery event if pending */ + if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING) + rc_client_raise_mastery_event(client, subset); +} + +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client, game); + + for (subset = game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* raise progress tracker events after achievement events so formatted values are updated for tracker event */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_raise_progress_tracker_events(client, game); + + /* if any achievements were unlocked, resync the active achievements list */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(game); + rc_mutex_unlock(&client->state.mutex); + } + + game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; +} + +void rc_client_do_frame(rc_client_t* client) +{ + if (!client) + return; + + if (client->game && !client->game->waiting_for_reset) { + rc_runtime_richpresence_t* richpresence; + rc_client_subset_info_t* subset; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + rc_client_update_memref_values(client); + rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL); + + client->game->progress_tracker.progress = 0.0; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_do_frame_update_progress_tracker(client, client->game); + + if (client->state.hardcore) { + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_leaderboards(client, subset); + } + } + + richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + const rc_clock_t now = client->callbacks.get_time_millisecs(client); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (scheduled_callback->when > now) { + /* not time for next callback yet, ignore it */ + scheduled_callback = NULL; + } + else { + /* remove the callback from the queue while we process it. callback can requeue if desired */ + client->state.scheduled_callbacks = scheduled_callback->next; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!scheduled_callback) + break; + + scheduled_callback->callback(scheduled_callback, client, now); + } while (1); + } + + if (client->state.disconnect & ~RC_CLIENT_DISCONNECT_VISIBLE) + rc_client_raise_disconnect_events(client); +} + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_mutex_lock(&client->state.mutex); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next || scheduled_callback->when < next->when) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reschedule_callback(rc_client_t* client, + rc_client_scheduled_callback_data_t* callback, rc_clock_t when) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + /* ASSERT: this should only be called if the mutex is held */ + + callback->when = when; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + + if (next == callback) { + if (when == 0) { + /* request to unschedule the callback */ + *last = next->next; + next->next = NULL; + break; + } + + if (!next->next) { + /* end of list, just append it */ + break; + } + + if (when < next->next->when) { + /* already in the correct place */ + break; + } + + /* remove from current position - will insert later */ + *last = next->next; + next->next = NULL; + continue; + } + + if (!next || when < next->when) { + /* insert here */ + callback->next = next; + *last = callback; + break; + } + + last = &next->next; + } while (1); +} + +static void rc_client_reset_richpresence(rc_client_t* client) +{ + rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_reset_richpresence(richpresence->richpresence); +} + +static void rc_client_reset_variables(rc_client_t* client) +{ + rc_value_t* variable = client->game->runtime.variables; + for (; variable; variable = variable->next) + rc_reset_value(variable); +} + +static void rc_client_reset_all(rc_client_t* client) +{ + rc_client_reset_achievements(client); + rc_client_reset_leaderboards(client); + rc_client_reset_richpresence(client); + rc_client_reset_variables(client); +} + +void rc_client_reset(rc_client_t* client) +{ + rc_client_game_hash_t* game_hash; + if (!client || !client->game) + return; + + game_hash = rc_client_find_game_hash(client, client->game->public_.hash); + if (game_hash && game_hash->game_id != client->game->public_.id) { + /* current media is not for loaded game. unload game */ + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)", + (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash); + rc_client_unload_game(client); + return; + } + + RC_CLIENT_LOG_INFO(client, "Resetting runtime"); + + rc_mutex_lock(&client->state.mutex); + + client->game->waiting_for_reset = 0; + rc_client_reset_pending_events(client); + + rc_client_hide_progress_tracker(client, client->game); + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_progress_size(&client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer) +{ + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + if (!buffer) + return RC_INVALID_STATE; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_serialize_progress(buffer, &client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any visible challenge indicators to be hidden */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED && + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + /* flag any visible trackers to be hidden */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (lboard && lboard->state == RC_LBOARD_STATE_STARTED && + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } + } +} + +static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any challenge indicators that should be shown */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + /* if it's already shown, just keep it. otherwise flag it to be shown */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + } + else { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + /* ASSERT: only active achievements are serialized, so we don't have to worry about + * deserialization deactiving them. */ + } + + /* flag any trackers that need to be shown */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (lboard->state == RC_LBOARD_STATE_STARTED) { + leaderboard->value = (int)lboard->value.value.value; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + + /* if it's already being tracked, just update tracker. otherwise, allocate one */ + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_update_leaderboard_tracker(game, leaderboard); + } + else { + rc_client_allocate_leaderboard_tracker(game, leaderboard); + } + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + /* deallocate the tracker (don't actually raise the failed event) */ + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } +} + +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_before_deserialize_progress(subset); + + rc_client_hide_progress_tracker(client, client->game); + + if (!serialized) { + rc_client_reset_all(client); + result = RC_OK; + } + else { + result = rc_runtime_deserialize_progress(&client->game->runtime, serialized, NULL); + } + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_after_deserialize_progress(client->game, subset); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + + return result; +} + +/* ===== Toggles ===== */ + +static void rc_client_enable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 1; + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); + rc_client_activate_leaderboards(client->game, client); + + /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */ + RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset"); + client->game->waiting_for_reset = 1; + } + else { + RC_CLIENT_LOG_INFO(client, "Hardcore enabled"); + } +} + +static void rc_client_disable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 0; + RC_CLIENT_LOG_INFO(client, "Hardcore disabled"); + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_deactivate_leaderboards(client->game, client); + } +} + +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled) +{ + int changed = 0; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + enabled = enabled ? 1 : 0; + if (client->state.hardcore != enabled) { + if (enabled) + rc_client_enable_hardcore(client); + else + rc_client_disable_hardcore(client); + + changed = 1; + } + + rc_mutex_unlock(&client->state.mutex); + + /* events must be raised outside of lock */ + if (changed && client->game) { + if (enabled) { + /* if enabling hardcore, notify client that a reset is requested */ + if (client->game->waiting_for_reset) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + client->callbacks.event_handler(&client_event, client); + } + } + else { + /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */ + rc_client_raise_pending_events(client, client->game); + } + } +} + +int rc_client_get_hardcore_enabled(const rc_client_t* client) +{ + return client && client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled"); + client->state.unofficial_enabled = enabled ? 1 : 0; + } +} + +int rc_client_get_unofficial_enabled(const rc_client_t* client) +{ + return client && client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled"); + client->state.encore_mode = enabled ? 1 : 0; + } +} + +int rc_client_get_encore_mode_enabled(const rc_client_t* client) +{ + return client && client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) { + RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game."); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled"); + client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF; + } +} + +int rc_client_get_spectator_mode_enabled(const rc_client_t* client) +{ + return client && (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1; +} + +void rc_client_set_userdata(rc_client_t* client, void* userdata) +{ + if (client) + client->callbacks.client_data = userdata; +} + +void* rc_client_get_userdata(const rc_client_t* client) +{ + return client ? client->callbacks.client_data : NULL; +} + +void rc_client_set_host(const rc_client_t* client, const char* hostname) +{ + /* if empty, just pass NULL */ + if (hostname && !hostname[0]) + hostname = NULL; + + /* clear the image host so it'll use the custom host for images too */ + rc_api_set_image_host(NULL); + + /* set the custom host */ + if (hostname && client) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + } + rc_api_set_host(hostname); +} diff --git a/deps/rcheevos/src/rc_client_internal.h b/deps/rcheevos/src/rc_client_internal.h new file mode 100644 index 000000000000..c3ff588215f8 --- /dev/null +++ b/deps/rcheevos/src/rc_client_internal.h @@ -0,0 +1,350 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_client.h" + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +struct rc_api_fetch_game_data_response_t; +typedef void (*rc_client_post_process_game_data_response_t)(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata); +typedef int (*rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); +typedef int (*rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); +typedef int (*rc_client_rich_presence_override_t)(rc_client_t* client, char buffer[], size_t buffersize); + +typedef struct rc_client_callbacks_t { + rc_client_read_memory_func_t read_memory; + rc_client_event_handler_t event_handler; + rc_client_server_call_t server_call; + rc_client_message_callback_t log_call; + rc_get_time_millisecs_func_t get_time_millisecs; + rc_client_post_process_game_data_response_t post_process_game_data_response; + rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock; + rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry; + rc_client_rich_presence_override_t rich_presence_override; + + void* client_data; +} rc_client_callbacks_t; + +struct rc_client_scheduled_callback_data_t; +typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + rc_clock_t when; + uint32_t related_id; + rc_client_scheduled_callback_t callback; + void* data; + struct rc_client_scheduled_callback_data_t* next; +} rc_client_scheduled_callback_data_t; + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE = 0, + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED = (1 << 1), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW = (1 << 2), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE = (1 << 3), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE = (1 << 4) /* not a real event, just triggers update */ +}; + +typedef struct rc_client_achievement_info_t { + rc_client_achievement_t public_; + + rc_trigger_t* trigger; + uint8_t md5[16]; + + time_t unlock_time_hardcore; + time_t unlock_time_softcore; + + uint8_t pending_events; + + const char* author; + time_t created_time; + time_t updated_time; +} rc_client_achievement_info_t; + +enum { + RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW, + RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE +}; + +typedef struct rc_client_progress_tracker_t { + rc_client_achievement_info_t* achievement; + float progress; + + rc_client_scheduled_callback_data_t* hide_callback; + uint8_t action; +} rc_client_progress_tracker_t; + +/*****************************************************************************\ +| Leaderboard Trackers | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE = (1 << 1), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW = (1 << 2), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE = (1 << 3) +}; + +typedef struct rc_client_leaderboard_tracker_info_t { + rc_client_leaderboard_tracker_t public_; + struct rc_client_leaderboard_tracker_info_t* next; + int32_t raw_value; + + uint32_t value_djb2; + + uint8_t format; + uint8_t pending_events; + uint8_t reference_count; + uint8_t value_from_hits; +} rc_client_leaderboard_tracker_info_t; + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED = (1 << 1), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED = (1 << 2), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED = (1 << 3) +}; + +typedef struct rc_client_leaderboard_info_t { + rc_client_leaderboard_t public_; + + rc_lboard_t* lboard; + uint8_t md5[16]; + + rc_client_leaderboard_tracker_info_t* tracker; + + uint32_t value_djb2; + int32_t value; + + uint8_t format; + uint8_t pending_events; + uint8_t bucket; + uint8_t hidden; +} rc_client_leaderboard_info_t; + +uint8_t rc_client_map_leaderboard_format(int format); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +enum { + RC_CLIENT_SUBSET_PENDING_EVENT_NONE = 0, + RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT = (1 << 1), + RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD = (1 << 2) +}; + +typedef struct rc_client_subset_info_t { + rc_client_subset_t public_; + + rc_client_achievement_info_t* achievements; + rc_client_leaderboard_info_t* leaderboards; + + struct rc_client_subset_info_t* next; + + const char* all_label; + const char* inactive_label; + const char* locked_label; + const char* unlocked_label; + const char* unofficial_label; + const char* unsupported_label; + + uint8_t active; + uint8_t mastery; + uint8_t pending_events; +} rc_client_subset_info_t; + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +typedef struct rc_client_game_hash_t { + char hash[33]; + uint32_t game_id; + struct rc_client_game_hash_t* next; +} rc_client_game_hash_t; + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash); + +typedef struct rc_client_media_hash_t { + rc_client_game_hash_t* game_hash; + struct rc_client_media_hash_t* next; + uint32_t path_djb2; +} rc_client_media_hash_t; + +enum { + RC_CLIENT_GAME_PENDING_EVENT_NONE = 0, + RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER = (1 << 1), + RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS = (1 << 2), + RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER = (1 << 3) +}; + +typedef struct rc_client_game_info_t { + rc_client_game_t public_; + rc_client_leaderboard_tracker_info_t* leaderboard_trackers; + rc_client_progress_tracker_t progress_tracker; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + + uint32_t max_valid_address; + + uint8_t waiting_for_reset; + uint8_t pending_events; + + rc_buffer_t buffer; +} rc_client_game_info_t; + +void rc_client_update_active_achievements(rc_client_game_info_t* game); +void rc_client_update_active_leaderboards(rc_client_game_info_t* game); + +/*****************************************************************************\ +| Client | +\*****************************************************************************/ + +enum { + RC_CLIENT_LOAD_STATE_NONE, + RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_STATE_AWAIT_LOGIN, + RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_STATE_STARTING_SESSION, + RC_CLIENT_LOAD_STATE_DONE, + RC_CLIENT_LOAD_STATE_UNKNOWN_GAME +}; + +enum { + RC_CLIENT_USER_STATE_NONE, + RC_CLIENT_USER_STATE_LOGIN_REQUESTED, + RC_CLIENT_USER_STATE_LOGGED_IN +}; + +enum { + RC_CLIENT_MASTERY_STATE_NONE, + RC_CLIENT_MASTERY_STATE_PENDING, + RC_CLIENT_MASTERY_STATE_SHOWN +}; + +enum { + RC_CLIENT_SPECTATOR_MODE_OFF, + RC_CLIENT_SPECTATOR_MODE_ON, + RC_CLIENT_SPECTATOR_MODE_LOCKED +}; + +enum { + RC_CLIENT_DISCONNECT_HIDDEN = 0, + RC_CLIENT_DISCONNECT_VISIBLE = (1 << 0), + RC_CLIENT_DISCONNECT_SHOW_PENDING = (1 << 1), + RC_CLIENT_DISCONNECT_HIDE_PENDING = (1 << 2) +}; + +struct rc_client_load_state_t; + +typedef struct rc_client_state_t { + rc_mutex_t mutex; + rc_buffer_t buffer; + + rc_client_scheduled_callback_data_t* scheduled_callbacks; + + uint8_t hardcore; + uint8_t encore_mode; + uint8_t spectator_mode; + uint8_t unofficial_enabled; + uint8_t log_level; + uint8_t user; + uint8_t disconnect; + + struct rc_client_load_state_t* load; + struct rc_client_async_handle_t* async_handles[4]; + rc_memref_t* processing_memref; + + rc_peek_t legacy_peek; +} rc_client_state_t; + +struct rc_client_t { + rc_client_game_info_t* game; + rc_client_game_hash_t* hashes; + + rc_client_user_t user; + + rc_client_callbacks_t callbacks; + + rc_client_state_t state; +}; + +/*****************************************************************************\ +| Helpers | +\*****************************************************************************/ + +#ifdef RC_NO_VARIADIC_MACROS + void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...); +#else + void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...); + #define RC_CLIENT_LOG_ERR_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_WARN_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_INFO_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_VERBOSE_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message_formatted(client, format, __VA_ARGS__); } +#endif + +void rc_client_log_message(const rc_client_t* client, const char* message); +#define RC_CLIENT_LOG_ERR(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_WARN(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_INFO(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_VERBOSE(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message(client, message); } + +/* internals pulled from runtime.c */ +void rc_runtime_checksum(const char* memaddr, uint8_t* md5); +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref); +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref); +/* end runtime.c internals */ + +/* helper functions for unit tests */ +struct rc_hash_iterator; +struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +/* end helper functions for unit tests */ + +enum { + RC_CLIENT_LEGACY_PEEK_AUTO, + RC_CLIENT_LEGACY_PEEK_CONSTRUCTED, + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS +}; + +void rc_client_set_legacy_peek(rc_client_t* client, int method); + +void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_CLIENT_INTERNAL_H */ diff --git a/deps/rcheevos/src/rcheevos/compat.c b/deps/rcheevos/src/rc_compat.c similarity index 61% rename from deps/rcheevos/src/rcheevos/compat.c rename to deps/rcheevos/src/rc_compat.c index ce635fc1eef7..0ba7601ad218 100644 --- a/deps/rcheevos/src/rcheevos/compat.c +++ b/deps/rcheevos/src/rc_compat.c @@ -83,3 +83,57 @@ struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) } #endif + +#ifndef RC_NO_THREADS +#ifdef _WIN32 + +/* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */ + +#define WIN32_LEAN_AND_MEAN +#include + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* default security, not owned by calling thread, unnamed */ + mutex->handle = CreateMutex(NULL, FALSE, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + CloseHandle(mutex->handle); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + WaitForSingleObject(mutex->handle, 0xFFFFFFFF); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + ReleaseMutex(mutex->handle); +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + pthread_mutex_init(mutex, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + pthread_mutex_destroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + pthread_mutex_lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + pthread_mutex_unlock(mutex); +} + +#endif +#endif /* RC_NO_THREADS */ diff --git a/deps/rcheevos/src/rcheevos/rc_compat.h b/deps/rcheevos/src/rc_compat.h similarity index 71% rename from deps/rcheevos/src/rcheevos/rc_compat.h rename to deps/rcheevos/src/rc_compat.h index f396726db5f5..e22f9b84e50a 100644 --- a/deps/rcheevos/src/rcheevos/rc_compat.h +++ b/deps/rcheevos/src/rc_compat.h @@ -13,6 +13,8 @@ extern "C" { /* MinGW redefinitions */ +#define RC_NO_VARIADIC_MACROS 1 + #elif defined(_MSC_VER) /* Visual Studio redefinitions */ @@ -32,6 +34,8 @@ extern "C" { /* C89 redefinitions */ #define RC_C89_HELPERS 1 +#define RC_NO_VARIADIC_MACROS 1 + #ifndef snprintf extern int rc_snprintf(char* buffer, size_t size, const char* format, ...); #define snprintf rc_snprintf @@ -65,6 +69,29 @@ extern "C" { #define gmtime_s rc_gmtime_s #endif +#ifdef RC_NO_THREADS + typedef int rc_mutex_t; + + #define rc_mutex_init(mutex) + #define rc_mutex_destroy(mutex) + #define rc_mutex_lock(mutex) + #define rc_mutex_unlock(mutex) +#else + #ifdef _WIN32 + typedef struct rc_mutex_t { + void* handle; /* HANDLE is defined as "void*" */ + } rc_mutex_t; + #else + #include + typedef pthread_mutex_t rc_mutex_t; + #endif + + void rc_mutex_init(rc_mutex_t* mutex); + void rc_mutex_destroy(rc_mutex_t* mutex); + void rc_mutex_lock(rc_mutex_t* mutex); + void rc_mutex_unlock(rc_mutex_t* mutex); +#endif + #ifdef __cplusplus } #endif diff --git a/deps/rcheevos/src/rcheevos/rc_libretro.c b/deps/rcheevos/src/rc_libretro.c similarity index 87% rename from deps/rcheevos/src/rcheevos/rc_libretro.c rename to deps/rcheevos/src/rc_libretro.c index 541f62a4baa7..4facc14af461 100644 --- a/deps/rcheevos/src/rcheevos/rc_libretro.c +++ b/deps/rcheevos/src/rc_libretro.c @@ -255,7 +255,7 @@ const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* l typedef struct rc_disallowed_core_systems_t { const char* library_name; - const int disallowed_consoles[4]; + const uint32_t disallowed_consoles[4]; } rc_disallowed_core_systems_t; static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { @@ -264,7 +264,7 @@ static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { { NULL, { 0 } } }; -int rc_libretro_is_system_allowed(const char* library_name, int console_id) { +int rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id) { const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; size_t library_name_length; size_t i; @@ -288,8 +288,8 @@ int rc_libretro_is_system_allowed(const char* library_name, int console_id) { return 1; } -unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail) { - unsigned i; +uint8_t* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail) { + uint32_t i; for (i = 0; i < regions->count; ++i) { const size_t size = regions->size[i]; @@ -298,12 +298,12 @@ unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* break; if (avail) - *avail = (unsigned)(size - address); + *avail = (uint32_t)(size - address); return ®ions->data[i][address]; } - address -= (unsigned)size; + address -= (uint32_t)size; } if (avail) @@ -312,10 +312,35 @@ unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* return NULL; } -unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address) { +uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address) { return rc_libretro_memory_find_avail(regions, address, NULL); } +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, + uint8_t* buffer, uint32_t num_bytes) { + uint32_t i; + uint32_t avail; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + avail = (unsigned)(size - address); + if (avail < num_bytes) + return avail; + + memcpy(buffer, ®ions->data[i][address], num_bytes); + return num_bytes; + } + + address -= (unsigned)size; + } + + return 0; +} + void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { rc_libretro_verbose_message_callback = callback; } @@ -342,7 +367,7 @@ static const char* rc_memory_type_str(int type) { } static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, - unsigned char* data, size_t size, const char* description) { + uint8_t* data, size_t size, const char* description) { if (size == 0) return; @@ -394,7 +419,7 @@ static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); } -static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, unsigned real_address, size_t* offset) +static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, uint32_t real_address, size_t* offset) { const struct retro_memory_descriptor* desc = mmap->descriptors; const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; @@ -412,14 +437,14 @@ static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(c /* address is in the block if (addr & select) == (start & select) */ if (((desc->start ^ real_address) & desc->select) == 0) { /* get the relative offset of the address from the start of the memory block */ - unsigned reduced_address = real_address - (unsigned)desc->start; + uint32_t reduced_address = real_address - (unsigned)desc->start; /* remove any bits from the reduced_address that correspond to the bits in the disconnect * mask and collapse the remaining bits. this code was copied from the mmap_reduce function * in RetroArch. i'm not exactly sure how it works, but it does. */ - unsigned disconnect_mask = (unsigned)desc->disconnect; + uint32_t disconnect_mask = (unsigned)desc->disconnect; while (disconnect_mask) { - const unsigned tmp = (disconnect_mask - 1) & ~disconnect_mask; + const uint32_t tmp = (disconnect_mask - 1) & ~disconnect_mask; reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; } @@ -441,24 +466,24 @@ static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(c static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, const rc_memory_regions_t* console_regions) { char description[64]; - unsigned i; - unsigned char* region_start; - unsigned char* desc_start; + uint32_t i; + uint8_t* region_start; + uint8_t* desc_start; size_t desc_size; size_t offset; for (i = 0; i < console_regions->num_regions; ++i) { const rc_memory_region_t* console_region = &console_regions->region[i]; size_t console_region_size = console_region->end_address - console_region->start_address + 1; - unsigned real_address = console_region->real_address; - unsigned disconnect_size = 0; + uint32_t real_address = console_region->real_address; + uint32_t disconnect_size = 0; while (console_region_size > 0) { const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); if (!desc) { if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { snprintf(description, sizeof(description), "Could not map region starting at $%06X", - real_address - console_region->real_address + console_region->start_address); + (unsigned)(real_address - console_region->real_address + console_region->start_address)); rc_libretro_verbose(description); } @@ -498,7 +523,7 @@ static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t if (desc_size == 0) { if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { snprintf(description, sizeof(description), "Could not map region starting at $%06X", - real_address - console_region->real_address + console_region->start_address); + (unsigned)(real_address - console_region->real_address + console_region->start_address)); rc_libretro_verbose(description); } @@ -519,7 +544,7 @@ static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t } } -static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { +static uint32_t rc_libretro_memory_console_region_to_ram_type(uint8_t region_type) { switch (region_type) { case RC_MEMORY_TYPE_SAVE_RAM: @@ -536,15 +561,15 @@ static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { char description[64]; - unsigned i, j; + uint32_t i, j; rc_libretro_core_memory_info_t info; size_t offset; for (i = 0; i < console_regions->num_regions; ++i) { const rc_memory_region_t* console_region = &console_regions->region[i]; const size_t console_region_size = console_region->end_address - console_region->start_address + 1; - const unsigned type = rc_libretro_memory_console_region_to_ram_type(console_region->type); - unsigned base_address = 0; + const uint32_t type = rc_libretro_memory_console_region_to_ram_type(console_region->type); + uint32_t base_address = 0; for (j = 0; j <= i; ++j) { const rc_memory_region_t* console_region2 = &console_regions->region[j]; @@ -570,7 +595,7 @@ static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regi } else { if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { - snprintf(description, sizeof(description), "Could not map region starting at $%06X", console_region->start_address); + snprintf(description, sizeof(description), "Could not map region starting at $%06X", (unsigned)console_region->start_address); rc_libretro_verbose(description); } @@ -595,7 +620,7 @@ int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); rc_libretro_memory_regions_t new_regions; int has_valid_region = 0; - unsigned i; + uint32_t i; if (!regions) return 0; @@ -650,47 +675,50 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, file_len = rc_file_tell(file_handle); rc_file_seek(file_handle, 0, SEEK_SET); - m3u_contents = (char*)malloc(file_len + 1); - rc_file_read(file_handle, m3u_contents, (int)file_len); - m3u_contents[file_len] = '\0'; - - rc_file_close(file_handle); - - ptr = m3u_contents; - do + m3u_contents = (char*)malloc((size_t)file_len + 1); + if (m3u_contents) { - /* ignore whitespace */ - while (isspace((int)*ptr)) - ++ptr; + rc_file_read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + rc_file_close(file_handle); - if (*ptr == '#') + ptr = m3u_contents; + do { - /* ignore comment unless it's the special SAVEDISK extension */ - if (memcmp(ptr, "#SAVEDISK:", 10) == 0) + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') { - /* get the path to the save disk from the frontend, assign it a bogus hash so - * it doesn't get hashed later */ - if (get_image_path(index, image_path, sizeof(image_path))) + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) { - const char save_disk_hash[33] = "[SAVE DISK]"; - rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); - ++index; + /* get the path to the save disk from the frontend, assign it a bogus hash so + * it doesn't get hashed later */ + if (get_image_path(index, image_path, sizeof(image_path))) + { + const char save_disk_hash[33] = "[SAVE DISK]"; + rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); + ++index; + } } } - } - else - { - /* non-empty line, tally a file */ - ++index; - } + else + { + /* non-empty line, tally a file */ + ++index; + } - /* find the end of the line */ - while (*ptr && *ptr != '\n') - ++ptr; + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; - } while (*ptr); + } while (*ptr); - free(m3u_contents); + free(m3u_contents); + } if (hash_set->entries_count > 0) { @@ -707,9 +735,9 @@ void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { memset(hash_set, 0, sizeof(*hash_set)); } -static unsigned rc_libretro_djb2(const char* input) +static uint32_t rc_libretro_djb2(const char* input) { - unsigned result = 5381; + uint32_t result = 5381; char c; while ((c = *input++) != '\0') @@ -719,8 +747,8 @@ static unsigned rc_libretro_djb2(const char* input) } void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, - const char* path, int game_id, const char hash[33]) { - const unsigned path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; + const char* path, uint32_t game_id, const char hash[33]) { + const uint32_t path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; struct rc_libretro_hash_entry_t* entry = NULL; struct rc_libretro_hash_entry_t* scan; struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; @@ -765,7 +793,7 @@ void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) { - const unsigned path_djb2 = rc_libretro_djb2(path); + const uint32_t path_djb2 = rc_libretro_djb2(path); struct rc_libretro_hash_entry_t* scan = hash_set->entries; struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; for (; scan < stop; ++scan) diff --git a/deps/rcheevos/src/rcheevos/rc_libretro.h b/deps/rcheevos/src/rc_libretro.h similarity index 79% rename from deps/rcheevos/src/rcheevos/rc_libretro.h rename to deps/rcheevos/src/rc_libretro.h index 642e3cbdc0f1..ceb92983f8a0 100644 --- a/deps/rcheevos/src/rcheevos/rc_libretro.h +++ b/deps/rcheevos/src/rc_libretro.h @@ -1,13 +1,16 @@ #ifndef RC_LIBRETRO_H #define RC_LIBRETRO_H +#ifdef __cplusplus +extern "C" { +#endif + /* this file comes from the libretro repository, which is not an explicit submodule. * the integration must set up paths appropriately to find it. */ #include -#ifdef __cplusplus -extern "C" { -#endif +#include +#include /*****************************************************************************\ | Disallowed Settings | @@ -21,7 +24,7 @@ typedef struct rc_disallowed_setting_t const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name); int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); -int rc_libretro_is_system_allowed(const char* library_name, int console_id); +int rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id); /*****************************************************************************\ | Memory Mapping | @@ -34,26 +37,27 @@ void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback call #define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 typedef struct rc_libretro_memory_regions_t { - unsigned char* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + uint8_t* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; size_t total_size; - unsigned count; + uint32_t count; } rc_libretro_memory_regions_t; typedef struct rc_libretro_core_memory_info_t { - unsigned char* data; + uint8_t* data; size_t size; } rc_libretro_core_memory_info_t; -typedef void (*rc_libretro_get_core_memory_info_func)(unsigned id, rc_libretro_core_memory_info_t* info); +typedef void (*rc_libretro_get_core_memory_info_func)(uint32_t id, rc_libretro_core_memory_info_t* info); int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id); void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); -unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address); -unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail); +uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address); +uint8_t* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail); +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, uint8_t* buffer, uint32_t num_bytes); /*****************************************************************************\ | Disk Identification | @@ -62,7 +66,7 @@ unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* typedef struct rc_libretro_hash_entry_t { uint32_t path_djb2; - int game_id; + uint32_t game_id; char hash[33]; } rc_libretro_hash_entry_t; @@ -73,14 +77,14 @@ typedef struct rc_libretro_hash_set_t uint16_t entries_size; } rc_libretro_hash_set_t; -typedef int (*rc_libretro_get_image_path_func)(unsigned index, char* buffer, size_t buffer_size); +typedef int (*rc_libretro_get_image_path_func)(uint32_t index, char* buffer, size_t buffer_size); void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, const char* m3u_path, rc_libretro_get_image_path_func get_image_path); void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, - const char* path, int game_id, const char hash[33]); + const char* path, uint32_t game_id, const char hash[33]); const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); diff --git a/deps/rcheevos/src/rc_util.c b/deps/rcheevos/src/rc_util.c new file mode 100644 index 000000000000..9deee91b4562 --- /dev/null +++ b/deps/rcheevos/src/rc_util.c @@ -0,0 +1,188 @@ +#include "rc_util.h" + +#include "rc_compat.h" +#include "rc_error.h" + +#include +#include + +#undef DEBUG_BUFFERS + +/* --- rc_buffer --- */ + +void rc_buffer_init(rc_buffer_t* buffer) +{ + buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; + buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; + buffer->chunk.next = NULL; + /* leave buffer->data uninitialized */ +} + +void rc_buffer_destroy(rc_buffer_t* buffer) +{ + rc_buffer_chunk_t* chunk; +#ifdef DEBUG_BUFFERS + int count = 0; + int wasted = 0; + int total = 0; +#endif + + /* first chunk is not allocated. skip it. */ + chunk = buffer->chunk.next; + + /* deallocate any additional buffers */ + while (chunk) + { + rc_buffer_chunk_t* next = chunk->next; +#ifdef DEBUG_BUFFERS + total += (int)(chunk->end - chunk->data); + wasted += (int)(chunk->end - chunk->write); + ++count; +#endif + free(chunk); + chunk = next; + } + +#ifdef DEBUG_BUFFERS + printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, + total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); +#endif +} + +uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount) +{ + rc_buffer_chunk_t* chunk = &buffer->chunk; + size_t remaining; + while (chunk) + { + remaining = chunk->end - chunk->write; + if (remaining >= amount) + return chunk->write; + + if (!chunk->next) + { + /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated + * to the chunk header, and the remaining will be used for data. + */ + const size_t chunk_header_size = sizeof(rc_buffer_chunk_t); + const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; + chunk->next = (rc_buffer_chunk_t*)malloc(alloc_size); + if (!chunk->next) + break; + + chunk->next->start = (uint8_t*)chunk->next + chunk_header_size; + chunk->next->write = chunk->next->start; + chunk->next->end = (uint8_t*)chunk->next + alloc_size; + chunk->next->next = NULL; + } + + chunk = chunk->next; + } + + return NULL; +} + +void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end) +{ + rc_buffer_chunk_t* chunk = &buffer->chunk; + do + { + if (chunk->write == start) + { + size_t offset = (end - chunk->start); + offset = (offset + 7) & ~7; + chunk->write = &chunk->start[offset]; + + if (chunk->write > chunk->end) + chunk->write = chunk->end; + break; + } + + chunk = chunk->next; + } while (chunk); +} + +void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount) +{ + uint8_t* ptr = rc_buffer_reserve(buffer, amount); + rc_buffer_consume(buffer, ptr, ptr + amount); + return (void*)ptr; +} + +char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len) +{ + uint8_t* dst = rc_buffer_reserve(buffer, len + 1); + memcpy(dst, src, len); + dst[len] = '\0'; + rc_buffer_consume(buffer, dst, dst + len + 2); + return (char*)dst; +} + +char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src) +{ + return rc_buffer_strncpy(buffer, src, strlen(src)); +} + +/* --- other --- */ + +void rc_format_md5(char checksum[33], const uint8_t digest[16]) +{ + snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); +} + +uint32_t rc_djb2(const char* input) +{ + uint32_t result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +const char* rc_error_str(int ret) +{ + switch (ret) { + case RC_OK: return "OK"; + case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; + case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; + case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; + case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; + case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; + case RC_INVALID_OPERATOR: return "Invalid operator"; + case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; + case RC_DUPLICATED_START: return "Duplicated start condition"; + case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; + case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; + case RC_DUPLICATED_VALUE: return "Duplicated value expression"; + case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; + case RC_MISSING_START: return "Missing start condition"; + case RC_MISSING_CANCEL: return "Missing cancel condition"; + case RC_MISSING_SUBMIT: return "Missing submit condition"; + case RC_MISSING_VALUE: return "Missing value expression"; + case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; + case RC_MISSING_DISPLAY_STRING: return "Missing display string"; + case RC_OUT_OF_MEMORY: return "Out of memory"; + case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; + case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; + case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; + case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; + case RC_INVALID_COMPARISON: return "Invalid comparison"; + case RC_INVALID_STATE: return "Invalid state"; + case RC_INVALID_JSON: return "Invalid JSON"; + case RC_API_FAILURE: return "API call failed"; + case RC_LOGIN_REQUIRED: return "Login required"; + case RC_NO_GAME_LOADED: return "No game loaded"; + case RC_HARDCORE_DISABLED: return "Hardcore disabled"; + case RC_ABORTED: return "Aborted"; + case RC_NO_RESPONSE: return "No response"; + case RC_ACCESS_DENIED: return "Access denied"; + case RC_INVALID_CREDENTIALS: return "Invalid credentials"; + case RC_EXPIRED_TOKEN: return "Expired token"; + default: return "Unknown error"; + } +} diff --git a/deps/rcheevos/src/rc_util.h b/deps/rcheevos/src/rc_util.h new file mode 100644 index 000000000000..34f75da912a7 --- /dev/null +++ b/deps/rcheevos/src/rc_util.h @@ -0,0 +1,53 @@ +#ifndef RC_UTIL_H +#define RC_UTIL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_buffer_chunk_t { + /* The current location where data is being written */ + uint8_t* write; + /* The first byte past the end of data where writing cannot occur */ + uint8_t* end; + /* The first byte of the data */ + uint8_t* start; + /* The next block in the allocated memory chain */ + struct rc_buffer_chunk_t* next; +} +rc_buffer_chunk_t; + +/** + * A preallocated block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_buffer_t { + /* The chunk data (will point at the local data member) */ + struct rc_buffer_chunk_t chunk; + /* Small chunk of memory pre-allocated for the chunk */ + uint8_t data[256]; +} +rc_buffer_t; + +void rc_buffer_init(rc_buffer_t* buffer); +void rc_buffer_destroy(rc_buffer_t* buffer); +uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount); +void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end); +void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount); +char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src); +char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len); + +uint32_t rc_djb2(const char* input); + +void rc_format_md5(char checksum[33], const uint8_t digest[16]); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_UTIL_H */ diff --git a/deps/rcheevos/src/rc_version.h b/deps/rcheevos/src/rc_version.h new file mode 100644 index 000000000000..19be5671aaef --- /dev/null +++ b/deps/rcheevos/src/rc_version.h @@ -0,0 +1,29 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define RCHEEVOS_VERSION_MAJOR 11 +#define RCHEEVOS_VERSION_MINOR 0 +#define RCHEEVOS_VERSION_PATCH 0 + +#define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) +#define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) + +#define RCHEEVOS_MAKE_STRING(num) #num +#define RCHEEVOS_MAKE_VERSION_STRING(major, minor, patch) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) "." RCHEEVOS_MAKE_STRING(patch) +#define RCHEEVOS_MAKE_VERSION_STRING_SHORT(major, minor) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) + +#if RCHEEVOS_VERSION_PATCH > 0 + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) +#else + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING_SHORT(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VERSION_H */ diff --git a/deps/rcheevos/src/rcheevos/alloc.c b/deps/rcheevos/src/rcheevos/alloc.c index c361eecee6bd..7c2af1f38af3 100644 --- a/deps/rcheevos/src/rcheevos/alloc.c +++ b/deps/rcheevos/src/rcheevos/alloc.c @@ -3,7 +3,7 @@ #include #include -void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset) +void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset) { rc_scratch_buffer_t* buffer; @@ -13,7 +13,7 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s /* update how much space will be required in the real buffer */ { - const int aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); + const int32_t aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); *offset += (aligned_offset - *offset); *offset += size; } @@ -21,8 +21,8 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s /* find a scratch buffer to hold the temporary data */ buffer = &scratch->buffer; do { - const int aligned_buffer_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); - const int remaining = sizeof(buffer->buffer) - aligned_buffer_offset; + const uint32_t aligned_buffer_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const uint32_t remaining = sizeof(buffer->buffer) - aligned_buffer_offset; if (remaining >= size) { /* claim the required space from an existing buffer */ @@ -36,13 +36,13 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s } while (1); /* not enough space in any existing buffer, allocate more */ - if (size > (int)sizeof(buffer->buffer)) { + if (size > (uint32_t)sizeof(buffer->buffer)) { /* caller is asking for more than we can fit in a standard rc_scratch_buffer_t. * leverage the fact that the buffer is the last field and extend its size. * this chunk will be exactly large enough to hold the needed data, and since offset * will exceed sizeof(buffer->buffer), it will never be eligible to hold anything else. */ - const int needed = sizeof(rc_scratch_buffer_t) - sizeof(buffer->buffer) + size; + const size_t needed = sizeof(rc_scratch_buffer_t) - sizeof(buffer->buffer) + size; buffer->next = (rc_scratch_buffer_t*)malloc(needed); } else { @@ -62,7 +62,7 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s return rc_alloc(buffer->buffer, &buffer->offset, size, alignment, NULL, -1); } -void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset) { +void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset) { void* ptr; *offset = (*offset + alignment - 1) & ~(alignment - 1); @@ -89,8 +89,8 @@ void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t return ptr; } -char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length) { - int used = 0; +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length) { + int32_t used = 0; char* ptr; rc_scratch_string_t** next = &parse->scratch.strings; @@ -109,7 +109,7 @@ char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length) { } *next = (rc_scratch_string_t*)rc_alloc_scratch(NULL, &used, sizeof(rc_scratch_string_t), RC_ALIGNOF(rc_scratch_string_t), &parse->scratch, RC_OFFSETOF(parse->scratch.objs, __rc_scratch_string_t)); - ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, length + 1, RC_ALIGNOF(char), &parse->scratch, -1); + ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, (uint32_t)length + 1, RC_ALIGNOF(char), &parse->scratch, -1); if (!ptr || !*next) { if (parse->offset >= 0) @@ -158,38 +158,3 @@ void rc_destroy_parse_state(rc_parse_state_t* parse) buffer = next; } } - -const char* rc_error_str(int ret) -{ - switch (ret) { - case RC_OK: return "OK"; - case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; - case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; - case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; - case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; - case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; - case RC_INVALID_OPERATOR: return "Invalid operator"; - case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; - case RC_DUPLICATED_START: return "Duplicated start condition"; - case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; - case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; - case RC_DUPLICATED_VALUE: return "Duplicated value expression"; - case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; - case RC_MISSING_START: return "Missing start condition"; - case RC_MISSING_CANCEL: return "Missing cancel condition"; - case RC_MISSING_SUBMIT: return "Missing submit condition"; - case RC_MISSING_VALUE: return "Missing value expression"; - case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; - case RC_MISSING_DISPLAY_STRING: return "Missing display string"; - case RC_OUT_OF_MEMORY: return "Out of memory"; - case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; - case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; - case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; - case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; - case RC_INVALID_COMPARISON: return "Invalid comparison"; - case RC_INVALID_STATE: return "Invalid state"; - case RC_INVALID_JSON: return "Invalid JSON"; - - default: return "Unknown error"; - } -} diff --git a/deps/rcheevos/src/rcheevos/condition.c b/deps/rcheevos/src/rcheevos/condition.c index 55a8084e7199..236dff373478 100644 --- a/deps/rcheevos/src/rcheevos/condition.c +++ b/deps/rcheevos/src/rcheevos/condition.c @@ -3,7 +3,7 @@ #include #include -static int rc_test_condition_compare(unsigned value1, unsigned value2, char oper) { +static int rc_test_condition_compare(uint32_t value1, uint32_t value2, uint8_t oper) { switch (oper) { case RC_OPERATOR_EQ: return value1 == value2; case RC_OPERATOR_NE: return value1 != value2; @@ -149,7 +149,7 @@ static int rc_parse_operator(const char** memaddr) { } } -rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect) { +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect) { rc_condition_t* self; const char* aux; int result; @@ -323,23 +323,23 @@ int rc_condition_is_combining(const rc_condition_t* self) { } static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { - const unsigned value1 = self->operand1.value.memref->value.value; - const unsigned value2 = self->operand2.value.num; + const uint32_t value1 = self->operand1.value.memref->value.value; + const uint32_t value2 = self->operand2.value.num; assert(self->operand1.size == self->operand1.value.memref->value.size); return rc_test_condition_compare(value1, value2, self->oper); } static int rc_test_condition_compare_delta_to_const(rc_condition_t* self) { const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; - const unsigned value1 = (memref1->changed) ? memref1->prior : memref1->value; - const unsigned value2 = self->operand2.value.num; + const uint32_t value1 = (memref1->changed) ? memref1->prior : memref1->value; + const uint32_t value2 = self->operand2.value.num; assert(self->operand1.size == self->operand1.value.memref->value.size); return rc_test_condition_compare(value1, value2, self->oper); } static int rc_test_condition_compare_memref_to_memref(rc_condition_t* self) { - const unsigned value1 = self->operand1.value.memref->value.value; - const unsigned value2 = self->operand2.value.memref->value.value; + const uint32_t value1 = self->operand1.value.memref->value.value; + const uint32_t value2 = self->operand2.value.memref->value.value; assert(self->operand1.size == self->operand1.value.memref->value.size); assert(self->operand2.size == self->operand2.value.memref->value.size); return rc_test_condition_compare(value1, value2, self->oper); @@ -387,7 +387,7 @@ static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { rc_typed_value_t value1; - const unsigned value2 = self->operand2.value.num; + const uint32_t value2 = self->operand2.value.num; value1.type = RC_VALUE_TYPE_UNSIGNED; value1.value.u32 = self->operand1.value.memref->value.value; @@ -399,7 +399,7 @@ static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* static int rc_test_condition_compare_delta_to_const_transformed(rc_condition_t* self) { rc_typed_value_t value1; const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; - const unsigned value2 = self->operand2.value.num; + const uint32_t value2 = self->operand2.value.num; value1.type = RC_VALUE_TYPE_UNSIGNED; value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; diff --git a/deps/rcheevos/src/rcheevos/condset.c b/deps/rcheevos/src/rcheevos/condset.c index e77a2358152b..4c2e7ea37e24 100644 --- a/deps/rcheevos/src/rcheevos/condset.c +++ b/deps/rcheevos/src/rcheevos/condset.c @@ -28,7 +28,7 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in rc_condset_t* self; rc_condition_t** next; int in_add_address; - unsigned measured_target = 0; + uint32_t measured_target = 0; self = RC_ALLOC(rc_condset_t, parse); self->has_pause = self->is_paused = self->has_indirect_memrefs = 0; @@ -181,7 +181,7 @@ static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc rc_typed_value_t value; int set_valid, cond_valid, and_next, or_next, reset_next, measured_from_hits, can_measure; rc_typed_value_t measured_value; - unsigned total_hits; + uint32_t total_hits; measured_value.type = RC_VALUE_TYPE_NONE; measured_from_hits = 0; diff --git a/deps/rcheevos/src/rcheevos/consoleinfo.c b/deps/rcheevos/src/rcheevos/consoleinfo.c index 10905ad9024d..985c42922085 100644 --- a/deps/rcheevos/src/rcheevos/consoleinfo.c +++ b/deps/rcheevos/src/rcheevos/consoleinfo.c @@ -444,11 +444,13 @@ static const rc_memory_regions_t rc_memory_regions_gameboy = { _rc_memory_region static const rc_memory_regions_t rc_memory_regions_gameboy_color = { _rc_memory_regions_gameboy, 17 }; /* ===== GameBoy Advance ===== */ +/* http://problemkaputt.de/gbatek-gba-memory-map.htm */ static const rc_memory_region_t _rc_memory_regions_gameboy_advance[] = { - { 0x000000U, 0x007FFFU, 0x03000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, - { 0x008000U, 0x047FFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + { 0x000000U, 0x007FFFU, 0x03000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* 32KB Internal Work RAM */ + { 0x008000U, 0x047FFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* 256KB External Work RAM */ + { 0x048000U, 0x057FFFU, 0x0E000000U, RC_MEMORY_TYPE_SAVE_RAM, "Save RAM" } /* 64KB Game Pak SRAM */ }; -static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 2 }; +static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 3 }; /* ===== GameCube ===== */ /* https://wiibrew.org/wiki/Memory_map */ @@ -541,11 +543,11 @@ static const rc_memory_region_t _rc_memory_regions_megadrive[] = { static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; /* ===== MegaDrive 32X (Genesis 32X) ===== */ -/* https://en.wikibooks.org/wiki/Genesis_Programming/68K_Memory_map/ */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { - { 0x000000U, 0x00FFFFU, 0xFF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, - { 0x010000U, 0x04FFFFU, 0x200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, - { 0x050000U, 0x05FFFFU, 0x000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Main MegaDrive RAM */ + { 0x010000U, 0x04FFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, /* Additional 32X RAM */ + { 0x050000U, 0x05FFFFU, 0x00000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } }; static const rc_memory_regions_t rc_memory_regions_megadrive_32x = { _rc_memory_regions_megadrive_32x, 3 }; @@ -759,16 +761,19 @@ static const rc_memory_region_t _rc_memory_regions_sg1000[] = { static const rc_memory_regions_t rc_memory_regions_sg1000 = { _rc_memory_regions_sg1000, 4 }; /* ===== Super Cassette Vision ===== */ +/* https://github.com/mamedev/mame/blob/f32bb79e8541ba96d3a8144b220c48fb7536ba4b/src/mame/epoch/scv.cpp#L78-L86 */ +/* SCV only has 128 bytes of system RAM, any additional memory is provided on the individual carts and is + * not backed up by battery. */ +/* http://www.videogameconsolelibrary.com/pg80-super_cass_vis.htm#page=specs */ static const rc_memory_region_t _rc_memory_regions_scv[] = { - { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_READONLY, "System ROM" }, + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_READONLY, "System ROM" }, /* BIOS */ { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, - { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, /* only really goes to $33FF? */ { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_UNUSED, "" }, - { 0x008000U, 0x00DFFFU, 0x008000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM" }, - { 0x00E000U, 0x00FF7FU, 0x00E000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x008000U, 0x00FF7FU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, { 0x00FF80U, 0x00FFFFU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } }; -static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_scv, 7 }; +static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_scv, 6 }; /* ===== Super Nintendo ===== */ /* https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM */ @@ -877,7 +882,7 @@ static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_reg /* ===== default ===== */ static const rc_memory_regions_t rc_memory_regions_none = { 0, 0 }; -const rc_memory_regions_t* rc_console_memory_regions(int console_id) +const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) { switch (console_id) { diff --git a/deps/rcheevos/src/rcheevos/format.c b/deps/rcheevos/src/rcheevos/format.c index 1129f950ba9f..c797faeb422f 100644 --- a/deps/rcheevos/src/rcheevos/format.c +++ b/deps/rcheevos/src/rcheevos/format.c @@ -1,6 +1,6 @@ #include "rc_internal.h" -#include "rc_compat.h" +#include "../rc_compat.h" #include #include @@ -75,16 +75,16 @@ int rc_parse_format(const char* format_str) { return RC_FORMAT_VALUE; } -static int rc_format_value_minutes(char* buffer, int size, unsigned minutes) { - unsigned hours; +static int rc_format_value_minutes(char* buffer, size_t size, uint32_t minutes) { + uint32_t hours; hours = minutes / 60; minutes -= hours * 60; return snprintf(buffer, size, "%uh%02u", hours, minutes); } -static int rc_format_value_seconds(char* buffer, int size, unsigned seconds) { - unsigned hours, minutes; +static int rc_format_value_seconds(char* buffer, size_t size, uint32_t seconds) { + uint32_t hours, minutes; /* apply modulus math to split the seconds into hours/minutes/seconds */ minutes = seconds / 60; @@ -98,8 +98,8 @@ static int rc_format_value_seconds(char* buffer, int size, unsigned seconds) { return snprintf(buffer, size, "%uh%02u:%02u", hours, minutes, seconds); } -static int rc_format_value_centiseconds(char* buffer, int size, unsigned centiseconds) { - unsigned seconds; +static int rc_format_value_centiseconds(char* buffer, size_t size, uint32_t centiseconds) { + uint32_t seconds; int chars, chars2; /* modulus off the centiseconds */ @@ -119,7 +119,7 @@ static int rc_format_value_centiseconds(char* buffer, int size, unsigned centise return chars; } -int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, int format) { +int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format) { int chars; rc_typed_value_t converted_value; @@ -197,7 +197,7 @@ int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, return chars; } -int rc_format_value(char* buffer, int size, int value, int format) { +int rc_format_value(char* buffer, int size, int32_t value, int format) { rc_typed_value_t typed_value; typed_value.value.i32 = value; diff --git a/deps/rcheevos/src/rcheevos/lboard.c b/deps/rcheevos/src/rcheevos/lboard.c index f7b1ec5a76c1..0191336a2115 100644 --- a/deps/rcheevos/src/rcheevos/lboard.c +++ b/deps/rcheevos/src/rcheevos/lboard.c @@ -164,7 +164,7 @@ rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, in return (parse.offset >= 0) ? self : 0; } -int rc_evaluate_lboard(rc_lboard_t* self, int* value, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L) { int start_ok, cancel_ok, submit_ok; rc_update_memref_values(self->memrefs, peek, peek_ud); @@ -262,6 +262,9 @@ int rc_lboard_state_active(int state) { } void rc_reset_lboard(rc_lboard_t* self) { + if (!self) + return; + self->state = RC_LBOARD_STATE_WAITING; rc_reset_trigger(&self->start); diff --git a/deps/rcheevos/src/rcheevos/memref.c b/deps/rcheevos/src/rcheevos/memref.c index 964f68490d69..87f6ec0bf1bd 100644 --- a/deps/rcheevos/src/rcheevos/memref.c +++ b/deps/rcheevos/src/rcheevos/memref.c @@ -6,7 +6,7 @@ #define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF -rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect) { +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size, uint8_t is_indirect) { rc_memref_t** next_memref; rc_memref_t* memref; @@ -41,7 +41,7 @@ rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char siz return memref; } -int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address) { const char* aux = *memaddr; char* end; unsigned long value; @@ -94,6 +94,7 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { ++aux; switch (*aux++) { case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; + case 'b': case 'B': *size = RC_MEMSIZE_FLOAT_BE; break; case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; @@ -113,15 +114,15 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { if (value > 0xffffffffU) value = 0xffffffffU; - *address = (unsigned)value; + *address = (uint32_t)value; *memaddr = end; return RC_OK; } -static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { +static float rc_build_float(uint32_t mantissa_bits, int32_t exponent, int sign) { /* 32-bit float has a 23-bit mantissa and 8-bit exponent */ - const unsigned implied_bit = 1 << 23; - const unsigned mantissa = mantissa_bits | implied_bit; + const uint32_t implied_bit = 1 << 23; + const uint32_t mantissa = mantissa_bits | implied_bit; double dbl = ((double)mantissa) / ((double)implied_bit); if (exponent > 127) { @@ -178,20 +179,32 @@ static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { static void rc_transform_memref_float(rc_typed_value_t* value) { /* decodes an IEEE 754 float */ - const unsigned mantissa = (value->value.u32 & 0x7FFFFF); - const int exponent = (int)((value->value.u32 >> 23) & 0xFF) - 127; + const uint32_t mantissa = (value->value.u32 & 0x7FFFFF); + const int32_t exponent = (int32_t)((value->value.u32 >> 23) & 0xFF) - 127; const int sign = (value->value.u32 & 0x80000000); value->value.f32 = rc_build_float(mantissa, exponent, sign); value->type = RC_VALUE_TYPE_FLOAT; } +static void rc_transform_memref_float_be(rc_typed_value_t* value) { + /* decodes an IEEE 754 float in big endian format */ + const uint32_t mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00007F00) << 8); + const int32_t exponent = (int32_t)(((value->value.u32 & 0x0000007F) << 1) | + ((value->value.u32 & 0x00008000) >> 15)) - 127; + const int sign = (value->value.u32 & 0x00000080); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + static void rc_transform_memref_mbf32(rc_typed_value_t* value) { /* decodes a Microsoft Binary Format float */ /* NOTE: 32-bit MBF is stored in memory as big endian (at least for Apple II) */ - const unsigned mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + const uint32_t mantissa = ((value->value.u32 & 0xFF000000) >> 24) | ((value->value.u32 & 0x00FF0000) >> 8) | ((value->value.u32 & 0x00007F00) << 8); - const int exponent = (int)(value->value.u32 & 0xFF) - 129; + const int32_t exponent = (int32_t)(value->value.u32 & 0xFF) - 129; const int sign = (value->value.u32 & 0x00008000); if (mantissa == 0 && exponent == -129) @@ -205,8 +218,8 @@ static void rc_transform_memref_mbf32(rc_typed_value_t* value) { static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { /* decodes a Microsoft Binary Format float */ /* Locomotive BASIC (CPC) uses MBF40, but in little endian format */ - const unsigned mantissa = value->value.u32 & 0x007FFFFF; - const int exponent = (int)(value->value.u32 >> 24) - 129; + const uint32_t mantissa = value->value.u32 & 0x007FFFFF; + const int32_t exponent = (int32_t)(value->value.u32 >> 24) - 129; const int sign = (value->value.u32 & 0x00800000); if (mantissa == 0 && exponent == -129) @@ -217,9 +230,9 @@ static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { value->type = RC_VALUE_TYPE_FLOAT; } -static const unsigned char rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; +static const uint8_t rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; -void rc_transform_memref_value(rc_typed_value_t* value, char size) { +void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size) { /* ASSERT: value->type == RC_VALUE_TYPE_UNSIGNED */ switch (size) { @@ -305,6 +318,10 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) { rc_transform_memref_float(value); break; + case RC_MEMSIZE_FLOAT_BE: + rc_transform_memref_float_be(value); + break; + case RC_MEMSIZE_MBF32: rc_transform_memref_mbf32(value); break; @@ -318,7 +335,7 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) { } } -static const unsigned rc_memref_masks[] = { +static const uint32_t rc_memref_masks[] = { 0x000000ff, /* RC_MEMSIZE_8_BITS */ 0x0000ffff, /* RC_MEMSIZE_16_BITS */ 0x00ffffff, /* RC_MEMSIZE_24_BITS */ @@ -340,10 +357,11 @@ static const unsigned rc_memref_masks[] = { 0xffffffff, /* RC_MEMSIZE_FLOAT */ 0xffffffff, /* RC_MEMSIZE_MBF32 */ 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT_BE */ 0xffffffff /* RC_MEMSIZE_VARIABLE */ }; -unsigned rc_memref_mask(char size) { +uint32_t rc_memref_mask(uint8_t size) { const size_t index = (size_t)size; if (index >= sizeof(rc_memref_masks) / sizeof(rc_memref_masks[0])) return 0xffffffff; @@ -354,7 +372,7 @@ unsigned rc_memref_mask(char size) { /* all sizes less than 8-bits (1 byte) are mapped to 8-bits. 24-bit is mapped to 32-bit * as we don't expect the client to understand a request for 3 bytes. all other reads are * mapped to the little-endian read of the same size. */ -static const char rc_memref_shared_sizes[] = { +static const uint8_t rc_memref_shared_sizes[] = { RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_8_BITS */ RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS */ @@ -376,10 +394,11 @@ static const char rc_memref_shared_sizes[] = { RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT_BE */ RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ }; -char rc_memref_shared_size(char size) { +uint8_t rc_memref_shared_size(uint8_t size) { const size_t index = (size_t)size; if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) return size; @@ -387,7 +406,7 @@ char rc_memref_shared_size(char size) { return rc_memref_shared_sizes[index]; } -static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud) { if (!peek) return 0; @@ -404,7 +423,7 @@ static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* default: { - unsigned value; + uint32_t value; const size_t index = (size_t)size; if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) return 0; @@ -418,7 +437,7 @@ static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* } } -void rc_update_memref_value(rc_memref_value_t* memref, unsigned new_value) { +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t new_value) { if (memref->value == new_value) { memref->changed = 0; } @@ -444,7 +463,7 @@ void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) *memrefs = 0; } -static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { +static uint32_t rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { switch (operand_type) { /* most common case explicitly first, even though it could be handled by default case. @@ -464,10 +483,10 @@ static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int o } } -unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state) { +uint32_t rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state) { /* if this is an indirect reference, handle the indirection. */ if (memref->value.is_indirect) { - const unsigned new_address = memref->address + eval_state->add_address; + const uint32_t new_address = memref->address + eval_state->add_address; rc_update_memref_value(&memref->value, rc_peek_value(new_address, memref->value.size, eval_state->peek, eval_state->peek_userdata)); } diff --git a/deps/rcheevos/src/rcheevos/operand.c b/deps/rcheevos/src/rcheevos/operand.c index f8abbbd7ba0f..d7e20908f8b6 100644 --- a/deps/rcheevos/src/rcheevos/operand.c +++ b/deps/rcheevos/src/rcheevos/operand.c @@ -68,10 +68,10 @@ static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_par return RC_OK; } -static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse, int is_indirect) { +static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect) { const char* aux = *memaddr; - unsigned address; - char size; + uint32_t address; + uint8_t size; int ret; switch (*aux) { @@ -115,7 +115,7 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ size = self->size; } - self->value.memref = rc_alloc_memref(parse, address, size, (char)is_indirect); + self->value.memref = rc_alloc_memref(parse, address, size, is_indirect); if (parse->offset < 0) return parse->offset; @@ -123,7 +123,7 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ return RC_OK; } -int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse) { +int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indirect, rc_parse_state_t* parse) { const char* aux = *memaddr; char* end; int ret; @@ -286,11 +286,11 @@ typedef struct { rc_luapeek_t; static int rc_luapeek(lua_State* L) { - unsigned address = (unsigned)luaL_checkinteger(L, 1); - unsigned num_bytes = (unsigned)luaL_checkinteger(L, 2); + uint32_t address = (uint32_t)luaL_checkinteger(L, 1); + uint32_t num_bytes = (uint32_t)luaL_checkinteger(L, 2); rc_luapeek_t* luapeek = (rc_luapeek_t*)lua_touserdata(L, 3); - unsigned value = luapeek->peek(address, num_bytes, luapeek->ud); + uint32_t value = luapeek->peek(address, num_bytes, luapeek->ud); lua_pushinteger(L, value); return 1; @@ -301,6 +301,7 @@ static int rc_luapeek(lua_State* L) { int rc_operand_is_float_memref(const rc_operand_t* self) { switch (self->size) { case RC_MEMSIZE_FLOAT: + case RC_MEMSIZE_FLOAT_BE: case RC_MEMSIZE_MBF32: case RC_MEMSIZE_MBF32_LE: return 1; @@ -329,7 +330,7 @@ int rc_operand_is_float(const rc_operand_t* self) { return rc_operand_is_float_memref(self); } -unsigned rc_transform_operand_value(unsigned value, const rc_operand_t* self) { +uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { switch (self->type) { case RC_OPERAND_BCD: @@ -450,10 +451,10 @@ void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_s if (lua_pcall(eval_state->L, 2, 1, 0) == LUA_OK) { if (lua_isboolean(eval_state->L, -1)) { - result->value.u32 = (unsigned)lua_toboolean(eval_state->L, -1); + result->value.u32 = (uint32_t)lua_toboolean(eval_state->L, -1); } else { - result->value.u32 = (unsigned)lua_tonumber(eval_state->L, -1); + result->value.u32 = (uint32_t)lua_tonumber(eval_state->L, -1); } } diff --git a/deps/rcheevos/src/rcheevos/rc_internal.h b/deps/rcheevos/src/rcheevos/rc_internal.h index 7f80d179ab8a..4a2f306a8977 100644 --- a/deps/rcheevos/src/rcheevos/rc_internal.h +++ b/deps/rcheevos/src/rcheevos/rc_internal.h @@ -1,5 +1,5 @@ -#ifndef INTERNAL_H -#define INTERNAL_H +#ifndef RC_INTERNAL_H +#define RC_INTERNAL_H #include "rc_runtime_types.h" @@ -36,10 +36,13 @@ RC_ALLOW_ALIGN(char) #define RC_ALLOC(t, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) #define RC_ALLOC_SCRATCH(t, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +/* force alignment to 4 bytes on 32-bit systems, or 8 bytes on 64-bit systems */ +#define RC_ALIGN(n) (((n) + (sizeof(void*)-1)) & ~(sizeof(void*)-1)) + typedef struct rc_scratch_buffer { struct rc_scratch_buffer* next; - int offset; - unsigned char buffer[512 - 16]; + int32_t offset; + uint8_t buffer[512 - 16]; } rc_scratch_buffer_t; @@ -74,8 +77,8 @@ enum { typedef struct { union { - unsigned u32; - int i32; + uint32_t u32; + int32_t i32; float f32; } value; @@ -87,24 +90,24 @@ rc_typed_value_t; typedef struct { rc_typed_value_t add_value;/* AddSource/SubSource */ - int add_hits; /* AddHits */ - unsigned add_address; /* AddAddress */ + int32_t add_hits; /* AddHits */ + uint32_t add_address; /* AddAddress */ rc_peek_t peek; void* peek_userdata; lua_State* L; rc_typed_value_t measured_value; /* Measured */ - char was_reset; /* ResetIf triggered */ - char has_hits; /* one of more hit counts is non-zero */ - char primed; /* true if all non-Trigger conditions are true */ - char measured_from_hits; /* true if the measured_value came from a condition's hit count */ - char was_cond_reset; /* ResetNextIf triggered */ + uint8_t was_reset; /* ResetIf triggered */ + uint8_t has_hits; /* one of more hit counts is non-zero */ + uint8_t primed; /* true if all non-Trigger conditions are true */ + uint8_t measured_from_hits; /* true if the measured_value came from a condition's hit count */ + uint8_t was_cond_reset; /* ResetNextIf triggered */ } rc_eval_state_t; typedef struct { - int offset; + int32_t offset; lua_State* L; int funcs_ndx; @@ -115,11 +118,11 @@ typedef struct { rc_memref_t** first_memref; rc_value_t** variables; - unsigned measured_target; + uint32_t measured_target; int lines_read; - char has_required_hits; - char measured_as_percent; + uint8_t has_required_hits; + uint8_t measured_as_percent; } rc_parse_state_t; @@ -128,18 +131,19 @@ void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables); void rc_destroy_parse_state(rc_parse_state_t* parse); -void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); -void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); -char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length); +void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); +void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length); -rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect); -int rc_parse_memref(const char** memaddr, char* size, unsigned* address); +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size, uint8_t is_indirect); +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address); void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud); -void rc_update_memref_value(rc_memref_value_t* memref, unsigned value); -unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state); -char rc_memref_shared_size(char size); -unsigned rc_memref_mask(char size); -void rc_transform_memref_value(rc_typed_value_t* value, char size); +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t value); +uint32_t rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state); +uint8_t rc_memref_shared_size(uint8_t size); +uint32_t rc_memref_mask(uint8_t size); +void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size); +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud); void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_trigger_state_active(int state); @@ -164,12 +168,12 @@ enum { RC_PROCESSING_COMPARE_ALWAYS_FALSE }; -rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect); +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect); int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state); int rc_condition_is_combining(const rc_condition_t* self); -int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse); +int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indirect, rc_parse_state_t* parse); void rc_evaluate_operand(rc_typed_value_t* value, rc_operand_t* self, rc_eval_state_t* eval_state); int rc_operand_is_float_memref(const rc_operand_t* self); int rc_operand_is_float(const rc_operand_t* self); @@ -177,7 +181,8 @@ int rc_operand_is_float(const rc_operand_t* self); void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L); void rc_reset_value(rc_value_t* self); -rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse); +int rc_value_from_hits(rc_value_t* self); +rc_value_t* rc_alloc_helper_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse); void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L); void rc_typed_value_convert(rc_typed_value_t* value, char new_type); @@ -188,7 +193,7 @@ void rc_typed_value_negate(rc_typed_value_t* value); int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper); void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); -int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, int format); +int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format); void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse); int rc_lboard_state_active(int state); @@ -199,4 +204,4 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, } #endif -#endif /* INTERNAL_H */ +#endif /* RC_INTERNAL_H */ diff --git a/deps/rcheevos/src/rcheevos/richpresence.c b/deps/rcheevos/src/rcheevos/richpresence.c index b7d883e81c3f..db58c76c0003 100644 --- a/deps/rcheevos/src/rcheevos/richpresence.c +++ b/deps/rcheevos/src/rcheevos/richpresence.c @@ -1,6 +1,6 @@ #include "rc_internal.h" -#include "rc_compat.h" +#include "../rc_compat.h" #include @@ -16,8 +16,8 @@ enum { static rc_memref_value_t* rc_alloc_helper_variable_memref_value(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { const char* end; rc_value_t* variable; - unsigned address; - char size; + uint32_t address; + uint8_t size; /* single memory reference lookups without a modifier flag can be handled without a variable */ end = memaddr; @@ -78,7 +78,7 @@ static const char* rc_parse_line(const char* line, const char** end, rc_parse_st typedef struct rc_richpresence_builtin_macro_t { const char* name; size_t name_len; - unsigned short display_type; + uint8_t display_type; } rc_richpresence_builtin_macro_t; static rc_richpresence_display_t* rc_parse_richpresence_display_internal(const char* line, const char* endline, rc_parse_state_t* parse, rc_richpresence_lookup_t* first_lookup) { @@ -280,7 +280,6 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo { rc_richpresence_lookup_item_t** items; rc_scratch_buffer_t* buffer; - const int alignment = sizeof(rc_richpresence_lookup_item_t*); int index; int size; @@ -293,7 +292,7 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo size = count * sizeof(rc_richpresence_lookup_item_t*); buffer = &parse->scratch.buffer; do { - const int aligned_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const int aligned_offset = RC_ALIGN(buffer->offset); const int remaining = sizeof(buffer->buffer) - aligned_offset; if (remaining >= size) { @@ -325,7 +324,7 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo } static void rc_insert_richpresence_lookup_item(rc_richpresence_lookup_t* lookup, - unsigned first, unsigned last, const char* label, int label_len, rc_parse_state_t* parse) + uint32_t first, uint32_t last, const char* label, size_t label_len, rc_parse_state_t* parse) { rc_richpresence_lookup_item_t** next; rc_richpresence_lookup_item_t* item; @@ -371,7 +370,7 @@ static const char* rc_parse_richpresence_lookup(rc_richpresence_lookup_t* lookup const char* endline; const char* label; char* endptr = 0; - unsigned first, last; + uint32_t first, last; int base; do @@ -526,7 +525,7 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, memcpy(format, line, chars); format[chars] = '\0'; - lookup->format = (unsigned short)rc_parse_format(format); + lookup->format = (uint8_t)rc_parse_format(format); } else { lookup->format = RC_FORMAT_VALUE; } @@ -664,7 +663,7 @@ void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, voi } } -static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part, char* buffer, unsigned buffersize) +static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part, char* buffer, size_t buffersize) { rc_richpresence_lookup_item_t* item; rc_typed_value_t value; @@ -808,7 +807,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part return (int)(ptr - buffer); } -int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { rc_richpresence_display_t* display; for (display = richpresence->first_display; display; display = display->next) { @@ -829,7 +828,7 @@ int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* bu return 0; } -int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { rc_update_richpresence(richpresence, peek, peek_ud, L); return rc_get_richpresence_display_string(richpresence, buffer, buffersize, peek, peek_ud, L); } diff --git a/deps/rcheevos/src/rcheevos/runtime.c b/deps/rcheevos/src/rcheevos/runtime.c index b5b55692706b..d663b7a9596d 100644 --- a/deps/rcheevos/src/rcheevos/runtime.c +++ b/deps/rcheevos/src/rcheevos/runtime.c @@ -1,7 +1,7 @@ #include "rc_runtime.h" #include "rc_internal.h" -#include "rc_compat.h" +#include "../rc_compat.h" #include "../rhash/md5.h" #include @@ -9,6 +9,17 @@ #define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 +rc_runtime_t* rc_runtime_alloc(void) { + rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); + + if (self) { + rc_runtime_init(self); + self->owns_self = 1; + } + + return self; +} + void rc_runtime_init(rc_runtime_t* self) { memset(self, 0, sizeof(rc_runtime_t)); self->next_memref = &self->memrefs; @@ -16,7 +27,7 @@ void rc_runtime_init(rc_runtime_t* self) { } void rc_runtime_destroy(rc_runtime_t* self) { - unsigned i; + uint32_t i; if (self->triggers) { for (i = 0; i < self->trigger_count; ++i) @@ -48,9 +59,13 @@ void rc_runtime_destroy(rc_runtime_t* self) { self->next_memref = 0; self->memrefs = 0; + + if (self->owns_self) { + free(self); + } } -static void rc_runtime_checksum(const char* memaddr, unsigned char* md5) { +void rc_runtime_checksum(const char* memaddr, uint8_t* md5) { md5_state_t state; md5_init(&state); md5_append(&state, (unsigned char*)memaddr, (int)strlen(memaddr)); @@ -81,7 +96,7 @@ static char rc_runtime_allocated_memrefs(rc_runtime_t* self) { return owns_memref; } -static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, unsigned index) { +static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, uint32_t index) { if (self->triggers[index].owns_memrefs) { /* if the trigger has one or more memrefs in its buffer, we can't free the buffer. * just null out the trigger so the runtime processor will skip it @@ -98,8 +113,8 @@ static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, unsigned } } -void rc_runtime_deactivate_achievement(rc_runtime_t* self, unsigned id) { - unsigned i; +void rc_runtime_deactivate_achievement(rc_runtime_t* self, uint32_t id) { + uint32_t i; for (i = 0; i < self->trigger_count; ++i) { if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) @@ -107,14 +122,14 @@ void rc_runtime_deactivate_achievement(rc_runtime_t* self, unsigned id) { } } -int rc_runtime_activate_achievement(rc_runtime_t* self, unsigned id, const char* memaddr, lua_State* L, int funcs_idx) { +int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { void* trigger_buffer; rc_trigger_t* trigger; rc_runtime_trigger_t* runtime_trigger; rc_parse_state_t parse; - unsigned char md5[16]; + uint8_t md5[16]; int size; - unsigned i; + uint32_t i; if (memaddr == NULL) return RC_INVALID_MEMORY_OPERAND; @@ -207,9 +222,9 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, unsigned id, const char* return RC_OK; } -rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, unsigned id) +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, uint32_t id) { - unsigned i; + uint32_t i; for (i = 0; i < self->trigger_count; ++i) { if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) @@ -219,7 +234,7 @@ rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, unsigned id) return NULL; } -int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id, unsigned* measured_value, unsigned* measured_target) +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target) { const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); if (!measured_value || !measured_target) @@ -242,10 +257,10 @@ int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id return 1; } -int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned id, char* buffer, size_t buffer_size) +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char* buffer, size_t buffer_size) { const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); - unsigned value; + uint32_t value; if (!buffer || !buffer_size) return 0; @@ -262,14 +277,14 @@ int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned value = trigger->measured_target; if (trigger->measured_as_percent) { - unsigned percent = (unsigned)(((unsigned long long)value * 100) / trigger->measured_target); + const uint32_t percent = (uint32_t)(((unsigned long long)value * 100) / trigger->measured_target); return snprintf(buffer, buffer_size, "%u%%", percent); } return snprintf(buffer, buffer_size, "%u/%u", value, trigger->measured_target); } -static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, unsigned index) { +static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, uint32_t index) { if (self->lboards[index].owns_memrefs) { /* if the lboard has one or more memrefs in its buffer, we can't free the buffer. * just null out the lboard so the runtime processor will skip it @@ -286,8 +301,8 @@ static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, unsigned i } } -void rc_runtime_deactivate_lboard(rc_runtime_t* self, unsigned id) { - unsigned i; +void rc_runtime_deactivate_lboard(rc_runtime_t* self, uint32_t id) { + uint32_t i; for (i = 0; i < self->lboard_count; ++i) { if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) @@ -295,14 +310,14 @@ void rc_runtime_deactivate_lboard(rc_runtime_t* self, unsigned id) { } } -int rc_runtime_activate_lboard(rc_runtime_t* self, unsigned id, const char* memaddr, lua_State* L, int funcs_idx) { +int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { void* lboard_buffer; - unsigned char md5[16]; + uint8_t md5[16]; rc_lboard_t* lboard; rc_parse_state_t parse; rc_runtime_lboard_t* runtime_lboard; int size; - unsigned i; + uint32_t i; if (memaddr == 0) return RC_INVALID_MEMORY_OPERAND; @@ -395,9 +410,9 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, unsigned id, const char* mema return RC_OK; } -rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, unsigned id) +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, uint32_t id) { - unsigned i; + uint32_t i; for (i = 0; i < self->lboard_count; ++i) { if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) @@ -407,7 +422,7 @@ rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, unsigned id) return NULL; } -int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format) +int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format) { return rc_format_value(buffer, size, value, format); } @@ -417,7 +432,7 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua rc_runtime_richpresence_t* previous; rc_runtime_richpresence_t** previous_ptr; rc_parse_state_t parse; - unsigned char md5[16]; + uint8_t md5[16]; int size; if (script == NULL) @@ -514,7 +529,7 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua return RC_OK; } -int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, unsigned buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L) { +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L) { if (self->richpresence && self->richpresence->richpresence) return rc_get_richpresence_display_string(self->richpresence->richpresence, buffer, buffersize, peek, peek_ud, L); @@ -534,7 +549,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha for (i = self->trigger_count - 1; i >= 0; --i) { rc_trigger_t* trigger = self->triggers[i].trigger; int old_state, new_state; - unsigned old_measured_value; + uint32_t old_measured_value; if (!trigger) continue; @@ -578,8 +593,8 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha if (trigger->measured_as_percent) { /* if reporting measured value as a percentage, only send the notification if the percentage changes */ - unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); - unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + const int32_t old_percent = (int32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const int32_t new_percent = (int32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); if (old_percent != new_percent) { runtime_event.value = new_percent; event_handler(&runtime_event); @@ -700,7 +715,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha void rc_runtime_reset(rc_runtime_t* self) { rc_value_t* variable; - unsigned i; + uint32_t i; for (i = 0; i < self->trigger_count; ++i) { if (self->triggers[i].trigger) @@ -712,13 +727,8 @@ void rc_runtime_reset(rc_runtime_t* self) { rc_reset_lboard(self->lboards[i].lboard); } - if (self->richpresence && self->richpresence->richpresence) { - rc_richpresence_display_t* display = self->richpresence->richpresence->first_display; - while (display != 0) { - rc_reset_trigger(&display->trigger); - display = display->next; - } - } + if (self->richpresence && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); for (variable = self->variables; variable; variable = variable->next) rc_reset_value(variable); @@ -739,7 +749,7 @@ static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memr return 0; } -static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { rc_condset_t* condset; if (!value) return 0; @@ -752,7 +762,7 @@ static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* return 0; } -static int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { rc_condset_t* condset; if (!trigger) return 0; @@ -769,7 +779,7 @@ static int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memr } static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref) { - unsigned i; + uint32_t i; /* disable any achievements dependent on the address */ for (i = 0; i < self->trigger_count; ++i) { @@ -804,7 +814,7 @@ static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref } } -void rc_runtime_invalidate_address(rc_runtime_t* self, unsigned address) { +void rc_runtime_invalidate_address(rc_runtime_t* self, uint32_t address) { rc_memref_t** last_memref = &self->memrefs; rc_memref_t* memref = self->memrefs; diff --git a/deps/rcheevos/src/rcheevos/runtime_progress.c b/deps/rcheevos/src/rcheevos/runtime_progress.c index cebe981c326d..d301377db8a7 100644 --- a/deps/rcheevos/src/rcheevos/runtime_progress.c +++ b/deps/rcheevos/src/rcheevos/runtime_progress.c @@ -1,6 +1,7 @@ #include "rc_runtime.h" #include "rc_internal.h" +#include "../rc_util.h" #include "../rhash/md5.h" #include @@ -17,12 +18,12 @@ #define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ typedef struct rc_runtime_progress_t { - rc_runtime_t* runtime; + const rc_runtime_t* runtime; - int offset; - unsigned char* buffer; + uint32_t offset; + uint8_t* buffer; - int chunk_size_offset; + uint32_t chunk_size_offset; lua_State* L; } rc_runtime_progress_t; @@ -39,7 +40,7 @@ typedef struct rc_runtime_progress_t { #define RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF 0x00100000 #define RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME 0x00200000 -static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, unsigned value) +static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, uint32_t value) { if (progress->buffer) { progress->buffer[progress->offset + 0] = value & 0xFF; value >>= 8; @@ -51,9 +52,9 @@ static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, unsi progress->offset += 4; } -static unsigned rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) +static uint32_t rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) { - unsigned value = progress->buffer[progress->offset + 0] | + uint32_t value = progress->buffer[progress->offset + 0] | (progress->buffer[progress->offset + 1] << 8) | (progress->buffer[progress->offset + 2] << 16) | (progress->buffer[progress->offset + 3] << 24); @@ -62,7 +63,7 @@ static unsigned rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) return value; } -static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, unsigned char* md5) +static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, uint8_t* md5) { if (progress->buffer) memcpy(&progress->buffer[progress->offset], md5, 16); @@ -70,7 +71,7 @@ static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, unsig progress->offset += 16; } -static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, unsigned char* md5) +static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, uint8_t* md5) { int result = 0; if (progress->buffer) @@ -81,18 +82,7 @@ static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, unsign return result; } -static unsigned rc_runtime_progress_djb2(const char* input) -{ - unsigned result = 5381; - char c; - - while ((c = *input++) != '\0') - result = ((result << 5) + result) + c; /* result = result * 33 + c */ - - return result; -} - -static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, unsigned chunk_id) +static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, uint32_t chunk_id) { rc_runtime_progress_write_uint(progress, chunk_id); @@ -103,14 +93,14 @@ static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, uns static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) { - unsigned length; - int offset; + uint32_t length; + uint32_t offset; progress->offset = (progress->offset + 3) & ~0x03; /* align to 4 byte boundary */ if (progress->buffer) { /* ignore chunk size field when calculating chunk size */ - length = (unsigned)(progress->offset - progress->chunk_size_offset - 4); + length = (uint32_t)(progress->offset - progress->chunk_size_offset - 4); /* temporarily update the write pointer to write the chunk size field */ offset = progress->offset; @@ -120,7 +110,7 @@ static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) } } -static void rc_runtime_progress_init(rc_runtime_progress_t* progress, rc_runtime_t* runtime, lua_State* L) +static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime, lua_State* L) { memset(progress, 0, sizeof(rc_runtime_progress_t)); progress->runtime = runtime; @@ -130,7 +120,7 @@ static void rc_runtime_progress_init(rc_runtime_progress_t* progress, rc_runtime static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) { rc_memref_t* memref = progress->runtime->memrefs; - unsigned int flags = 0; + uint32_t flags = 0; rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_MEMREFS); @@ -161,9 +151,9 @@ static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) static int rc_runtime_progress_read_memrefs(rc_runtime_progress_t* progress) { - unsigned entries; - unsigned address, flags, value, prior; - char size; + uint32_t entries; + uint32_t address, flags, value, prior; + uint8_t size; rc_memref_t* memref; rc_memref_t* first_unmatched_memref = progress->runtime->memrefs; @@ -218,7 +208,7 @@ static int rc_runtime_progress_is_indirect_memref(rc_operand_t* oper) static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) { rc_condition_t* cond; - unsigned flags; + uint32_t flags; rc_runtime_progress_write_uint(progress, condset->is_paused); @@ -262,7 +252,7 @@ static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) { rc_condition_t* cond; - unsigned flags; + uint32_t flags; condset->is_paused = (char)rc_runtime_progress_read_uint(progress); @@ -297,7 +287,7 @@ static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_ return RC_OK; } -static unsigned rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) +static uint32_t rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) { const rc_condition_t* condition; @@ -318,7 +308,7 @@ static unsigned rc_runtime_progress_should_serialize_variable_condset(const rc_c static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, const rc_value_t* variable) { - unsigned flags; + uint32_t flags; flags = rc_runtime_progress_should_serialize_variable_condset(variable->conditions); if (variable->value.changed) @@ -339,7 +329,7 @@ static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, c static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) { - unsigned count = 0; + uint32_t count = 0; const rc_value_t* variable; for (variable = progress->runtime->variables; variable; variable = variable->next) @@ -352,7 +342,7 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) for (variable = progress->runtime->variables; variable; variable = variable->next) { - unsigned djb2 = rc_runtime_progress_djb2(variable->name); + uint32_t djb2 = rc_djb2(variable->name); rc_runtime_progress_write_uint(progress, djb2); rc_runtime_progress_write_variable(progress, variable); @@ -364,7 +354,7 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) static int rc_runtime_progress_read_variable(rc_runtime_progress_t* progress, rc_value_t* variable) { - unsigned flags = rc_runtime_progress_read_uint(progress); + uint32_t flags = rc_runtime_progress_read_uint(progress); variable->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; variable->value.value = rc_runtime_progress_read_uint(progress); variable->value.prior = rc_runtime_progress_read_uint(progress); @@ -386,14 +376,14 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) struct rc_pending_value_t { rc_value_t* variable; - unsigned djb2; + uint32_t djb2; }; struct rc_pending_value_t local_pending_variables[32]; struct rc_pending_value_t* pending_variables; rc_value_t* variable; - unsigned count, serialized_count; + uint32_t count, serialized_count; int result; - unsigned i; + uint32_t i; serialized_count = rc_runtime_progress_read_uint(progress); if (serialized_count == 0) @@ -418,13 +408,13 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) count = 0; for (variable = progress->runtime->variables; variable; variable = variable->next) { pending_variables[count].variable = variable; - pending_variables[count].djb2 = rc_runtime_progress_djb2(variable->name); + pending_variables[count].djb2 = rc_djb2(variable->name); ++count; } result = RC_OK; for (; serialized_count > 0 && result == RC_OK; --serialized_count) { - unsigned djb2 = rc_runtime_progress_read_uint(progress); + uint32_t djb2 = rc_runtime_progress_read_uint(progress); for (i = 0; i < count; ++i) { if (pending_variables[i].djb2 == djb2) { variable = pending_variables[i].variable; @@ -502,7 +492,7 @@ static int rc_runtime_progress_read_trigger(rc_runtime_progress_t* progress, rc_ static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progress) { - unsigned i; + uint32_t i; int offset = 0; int result; @@ -543,8 +533,8 @@ static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progres static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) { - unsigned id = rc_runtime_progress_read_uint(progress); - unsigned i; + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; for (i = 0; i < progress->runtime->trigger_count; ++i) { rc_runtime_trigger_t* runtime_trigger = &progress->runtime->triggers[i]; @@ -564,8 +554,8 @@ static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) static int rc_runtime_progress_write_leaderboards(rc_runtime_progress_t* progress) { - unsigned i; - unsigned flags; + uint32_t i; + uint32_t flags; int offset = 0; int result; @@ -621,8 +611,8 @@ static int rc_runtime_progress_write_leaderboards(rc_runtime_progress_t* progres static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) { - unsigned id = rc_runtime_progress_read_uint(progress); - unsigned i; + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; int result; for (i = 0; i < progress->runtime->lboard_count; ++i) { @@ -632,7 +622,7 @@ static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) if (runtime_lboard->lboard->state == RC_TRIGGER_STATE_UNUPDATED) { /* only update state if definition hasn't changed (md5 matches) */ if (rc_runtime_progress_match_md5(progress, runtime_lboard->md5)) { - unsigned flags = rc_runtime_progress_read_uint(progress); + uint32_t flags = rc_runtime_progress_read_uint(progress); result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->start); if (result != RC_OK) @@ -712,7 +702,7 @@ static int rc_runtime_progress_read_rich_presence(rc_runtime_progress_t* progres static int rc_runtime_progress_serialize_internal(rc_runtime_progress_t* progress) { md5_state_t state; - unsigned char md5[16]; + uint8_t md5[16]; int result; rc_runtime_progress_write_uint(progress, RC_RUNTIME_MARKER); @@ -751,7 +741,7 @@ int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) rc_runtime_progress_t progress; int result; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + rc_runtime_progress_init(&progress, runtime, L); result = rc_runtime_progress_serialize_internal(&progress); if (result != RC_OK) @@ -764,26 +754,34 @@ int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua { rc_runtime_progress_t progress; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); - progress.buffer = (unsigned char*)buffer; + if (!buffer) + return RC_INVALID_STATE; + + rc_runtime_progress_init(&progress, runtime, L); + progress.buffer = (uint8_t*)buffer; return rc_runtime_progress_serialize_internal(&progress); } -int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* serialized, lua_State* L) +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L) { rc_runtime_progress_t progress; md5_state_t state; - unsigned char md5[16]; - unsigned chunk_id; - unsigned chunk_size; - unsigned next_chunk_offset; - unsigned i; + uint8_t md5[16]; + uint32_t chunk_id; + uint32_t chunk_size; + uint32_t next_chunk_offset; + uint32_t i; int seen_rich_presence = 0; int result = RC_OK; + if (!serialized) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + rc_runtime_progress_init(&progress, runtime, L); - progress.buffer = (unsigned char*)serialized; + progress.buffer = (uint8_t*)serialized; if (rc_runtime_progress_read_uint(&progress) != RC_RUNTIME_MARKER) { rc_runtime_reset(runtime); diff --git a/deps/rcheevos/src/rcheevos/trigger.c b/deps/rcheevos/src/rcheevos/trigger.c index 6061ab8ee194..71e186c01487 100644 --- a/deps/rcheevos/src/rcheevos/trigger.c +++ b/deps/rcheevos/src/rcheevos/trigger.c @@ -96,7 +96,7 @@ int rc_trigger_state_active(int state) } } -static int rc_condset_is_measured_from_hitcount(const rc_condset_t* condset, unsigned measured_value) +static int rc_condset_is_measured_from_hitcount(const rc_condset_t* condset, uint32_t measured_value) { const rc_condition_t* condition; for (condition = condset->conditions; condition; condition = condition->next) { @@ -280,6 +280,9 @@ int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) } void rc_reset_trigger(rc_trigger_t* self) { + if (!self) + return; + rc_reset_trigger_hitcounts(self); self->state = RC_TRIGGER_STATE_WAITING; diff --git a/deps/rcheevos/src/rcheevos/value.c b/deps/rcheevos/src/rcheevos/value.c index 9922231dc346..e7df82df908d 100644 --- a/deps/rcheevos/src/rcheevos/value.c +++ b/deps/rcheevos/src/rcheevos/value.c @@ -255,7 +255,7 @@ int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t return valid; } -int rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { +int32_t rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { rc_typed_value_t result; int valid = rc_evaluate_value_typed(self, &result, peek, ud, L); @@ -285,17 +285,31 @@ void rc_reset_value(rc_value_t* self) { self->value.changed = 0; } +int rc_value_from_hits(rc_value_t* self) +{ + rc_condset_t* condset = self->conditions; + for (; condset != NULL; condset = condset->next) { + rc_condition_t* condition = condset->conditions; + for (; condition != NULL; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) + return (condition->required_hits != 0); + } + } + + return 0; +} + void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { parse->variables = variables; *variables = 0; } -rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) +rc_value_t* rc_alloc_helper_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse) { rc_value_t** variables = parse->variables; rc_value_t* value; const char* name; - unsigned measured_target; + uint32_t measured_target; while ((value = *variables) != NULL) { if (strncmp(value->name, memaddr, memaddr_len) == 0 && value->name[memaddr_len] == 0) diff --git a/deps/rcheevos/src/rhash/cdreader.c b/deps/rcheevos/src/rhash/cdreader.c index c0f5c88a2bac..9ff9cb7396b3 100644 --- a/deps/rcheevos/src/rhash/cdreader.c +++ b/deps/rcheevos/src/rhash/cdreader.c @@ -1,6 +1,6 @@ #include "rc_hash.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include #include @@ -31,7 +31,7 @@ struct cdrom_t #endif }; -static int cdreader_get_sector(unsigned char header[16]) +static int cdreader_get_sector(uint8_t header[16]) { int minutes = (header[12] >> 4) * 10 + (header[12] & 0x0F); int seconds = (header[13] >> 4) * 10 + (header[13] & 0x0F); @@ -50,11 +50,11 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) * Then check for the presence of "CD001", which is gauranteed to be in either the * boot record or primary volume descriptor, one of which is always at sector 16. */ - const unsigned char sync_pattern[] = { + const uint8_t sync_pattern[] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 }; - unsigned char header[32]; + uint8_t header[32]; const int64_t toc_sector = 16 + cdrom->track_pregap_sectors; cdrom->sector_size = 0; diff --git a/deps/rcheevos/src/rhash/hash.c b/deps/rcheevos/src/rhash/hash.c index 72d7692b05d8..a953d525cb00 100644 --- a/deps/rcheevos/src/rhash/hash.c +++ b/deps/rcheevos/src/rhash/hash.c @@ -1,6 +1,6 @@ #include "rc_hash.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include "md5.h" @@ -225,11 +225,11 @@ static void rc_cd_close_track(void* track_handle) rc_hash_error("no hook registered for cdreader_close_track"); } -static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, unsigned* size) +static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uint32_t* size) { uint8_t buffer[2048], *tmp; int sector; - unsigned num_sectors = 0; + uint32_t num_sectors = 0; size_t filename_length; const char* slash; @@ -258,7 +258,7 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns } else { - unsigned logical_block_size; + uint32_t logical_block_size; /* find the cd information */ if (!rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 16, buffer, 256)) @@ -452,7 +452,7 @@ static int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_si return rc_hash_finalize(&md5, hash); } -static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector, const char* name, unsigned size, const char* description) +static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector, const char* name, uint32_t size, const char* description) { uint8_t buffer[2048]; size_t num_read; @@ -760,11 +760,11 @@ static int rc_hash_jaguar_cd(char hash[33], const char* path) void* track_handle; md5_state_t md5; int byteswapped = 0; - unsigned size = 0; - unsigned offset = 0; - unsigned sector = 0; - unsigned remaining; - unsigned i; + uint32_t size = 0; + uint32_t offset = 0; + uint32_t sector = 0; + uint32_t remaining; + uint32_t i; /* Jaguar CD header is in the first sector of the first data track OF THE SECOND SESSION. * The first track must be an audio track, but may be a warning message or actual game audio */ @@ -905,7 +905,7 @@ static int rc_hash_neogeo_cd(char hash[33], const char* path) char buffer[1024], *ptr; void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; md5_state_t md5; track_handle = rc_cd_open_track(path, 1); @@ -1091,7 +1091,7 @@ static int rc_hash_nintendo_ds(char hash[33], const char* path) { uint8_t header[512]; uint8_t* hash_buffer; - unsigned int hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; + uint32_t hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; size_t num_read; int64_t offset = 0; md5_state_t md5; @@ -1215,6 +1215,8 @@ static int rc_hash_gamecube(char hash[33], const char* path) uint32_t ix; file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); /* Verify Gamecube */ rc_file_seek(file_handle, 0x1c, SEEK_SET); @@ -1330,8 +1332,8 @@ static int rc_hash_pce_track(char hash[33], void* track_handle) { uint8_t buffer[2048]; md5_state_t md5; - int sector, num_sectors; - unsigned size; + uint32_t sector, num_sectors; + uint32_t size; /* the PC-Engine uses the second sector to specify boot information and program name. * the string "PC Engine CD-ROM SYSTEM" should exist at 32 bytes into the sector @@ -1516,7 +1518,7 @@ static int rc_hash_dreamcast(char hash[33], const char* path) uint8_t buffer[256] = ""; void* track_handle; char exe_file[32] = ""; - unsigned size; + uint32_t size; uint32_t sector; int result = 0; md5_state_t md5; @@ -1610,10 +1612,10 @@ static int rc_hash_dreamcast(char hash[33], const char* path) } static int rc_hash_find_playstation_executable(void* track_handle, const char* boot_key, const char* cdrom_prefix, - char exe_name[], unsigned exe_name_size, unsigned* exe_size) + char exe_name[], uint32_t exe_name_size, unsigned* exe_size) { uint8_t buffer[2048]; - unsigned size; + uint32_t size; char* ptr; char* start; const size_t boot_key_len = strlen(boot_key); @@ -1683,7 +1685,7 @@ static int rc_hash_psx(char hash[33], const char* path) char exe_name[64] = ""; void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; int result = 0; md5_state_t md5; @@ -1747,7 +1749,7 @@ static int rc_hash_ps2(char hash[33], const char* path) char exe_name[64] = ""; void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; int result = 0; md5_state_t md5; @@ -1795,7 +1797,7 @@ static int rc_hash_psp(char hash[33], const char* path) { void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; md5_state_t md5; track_handle = rc_cd_open_track(path, 1); @@ -1866,6 +1868,21 @@ static int rc_hash_sega_cd(char hash[33], const char* path) return rc_hash_buffer(hash, buffer, sizeof(buffer)); } +static int rc_hash_scv(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + /* https://gitlab.com/MaaaX-EmuSCV/libretro-emuscv/-/blob/master/readme.txt#L211 */ + if (memcmp(buffer, "EmuSCV", 6) == 0) + { + rc_hash_verbose("Ignoring SCV header"); + + buffer += 32; + buffer_size -= 32; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + static int rc_hash_snes(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it */ @@ -2026,6 +2043,9 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_PC_ENGINE: /* NOTE: does not support PCEngine CD */ return rc_hash_pce(hash, buffer, buffer_size); + case RC_CONSOLE_SUPER_CASSETTEVISION: + return rc_hash_scv(hash, buffer, buffer_size); + case RC_CONSOLE_SUPER_NINTENDO: return rc_hash_snes(hash, buffer, buffer_size); @@ -2322,6 +2342,7 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ATARI_LYNX: case RC_CONSOLE_NINTENDO: case RC_CONSOLE_PC_ENGINE: + case RC_CONSOLE_SUPER_CASSETTEVISION: case RC_CONSOLE_SUPER_NINTENDO: /* additional logic whole-file hash - buffer then call rc_hash_generate_from_buffer */ return rc_hash_buffered_file(hash, console_id, path); @@ -2463,7 +2484,7 @@ static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, c rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); } -void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size) +void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) { int need_path = !buffer; @@ -2540,7 +2561,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, - * Fairchild Channel F, Arcadia 2001, and Interton VC 4000. + * Fairchild Channel F, Arcadia 2001, Interton VC 4000, and Super Cassette Vision. * Since they all use the same hashing algorithm, only specify one of them */ iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; } @@ -2570,9 +2591,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; iterator->consoles[2] = RC_CONSOLE_DREAMCAST; iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; - iterator->consoles[5] = RC_CONSOLE_3DO; - iterator->consoles[6] = RC_CONSOLE_PCFX; + iterator->consoles[4] = RC_CONSOLE_PSP; + iterator->consoles[5] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[6] = RC_CONSOLE_3DO; + iterator->consoles[7] = RC_CONSOLE_PCFX; need_path = 1; } else if (rc_path_compare_extension(ext, "col")) @@ -2587,6 +2609,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { iterator->consoles[0] = RC_CONSOLE_FAIRCHILD_CHANNEL_F; } + else if (rc_path_compare_extension(ext, "cart")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_CASSETTEVISION; + } break; case 'd': @@ -2596,7 +2622,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "d64")) { - iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; } else if (rc_path_compare_extension(ext, "d88")) { @@ -2616,7 +2642,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "fd")) { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ } break; diff --git a/griffin/griffin.c b/griffin/griffin.c index 9a3115abc9d8..1c40fb54bdcb 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -186,6 +186,15 @@ ACHIEVEMENTS #include "../libretro-common/net/net_http.c" #endif +/* rcheevos doesn't actually spawn and manage threads, RC_NO_THREADS + * simply disables the mutexes that provide thread safety. */ +#if !defined(HAVE_THREADS) +#define RC_NO_THREADS 1 +#elif defined(GEKKO) || defined(_3DS) + /* Gekko (Wii) and 3DS use custom pthread wrappers (see rthreads.c) */ +#define RC_NO_THREADS 1 +#endif + #include "../libretro-common/formats/cdfs/cdfs.c" #include "../network/net_http_special.c" @@ -193,11 +202,15 @@ ACHIEVEMENTS #include "../cheevos/cheevos_client.c" #include "../cheevos/cheevos_menu.c" +#include "../deps/rcheevos/src/rc_client.c" +#include "../deps/rcheevos/src/rc_compat.c" +#include "../deps/rcheevos/src/rc_libretro.c" +#include "../deps/rcheevos/src/rc_util.c" #include "../deps/rcheevos/src/rapi/rc_api_common.c" +#include "../deps/rcheevos/src/rapi/rc_api_info.c" #include "../deps/rcheevos/src/rapi/rc_api_runtime.c" #include "../deps/rcheevos/src/rapi/rc_api_user.c" #include "../deps/rcheevos/src/rcheevos/alloc.c" -#include "../deps/rcheevos/src/rcheevos/compat.c" #include "../deps/rcheevos/src/rcheevos/condition.c" #include "../deps/rcheevos/src/rcheevos/condset.c" #include "../deps/rcheevos/src/rcheevos/consoleinfo.c" @@ -205,7 +218,6 @@ ACHIEVEMENTS #include "../deps/rcheevos/src/rcheevos/lboard.c" #include "../deps/rcheevos/src/rcheevos/memref.c" #include "../deps/rcheevos/src/rcheevos/operand.c" -#include "../deps/rcheevos/src/rcheevos/rc_libretro.c" #include "../deps/rcheevos/src/rcheevos/richpresence.c" #include "../deps/rcheevos/src/rcheevos/runtime.c" #include "../deps/rcheevos/src/rcheevos/runtime_progress.c"