From ef9cbf6be8a69e26bf2127c81fd548928a101ead Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 24 Feb 2024 11:56:53 +1000 Subject: [PATCH] 3rdparty/rcheevos: Bump to 3d01191 and move in-tree - aes and 3DS functions in hash.c removed, due to potential legal issues. - .github/test/validator directories removed, as they are unnecessary. --- .gitmodules | 3 - 3rdparty/rcheevos/.editorconfig | 15 + 3rdparty/rcheevos/.gitignore | 65 + 3rdparty/rcheevos/CHANGELOG.md | 339 + 3rdparty/rcheevos/CMakeLists.txt | 98 +- 3rdparty/rcheevos/LICENSE | 21 + 3rdparty/rcheevos/PCSX2-Modifications.txt | 2 + 3rdparty/rcheevos/Package.swift | 32 + 3rdparty/rcheevos/README.md | 81 + 3rdparty/rcheevos/include/rc_api_editor.h | 253 + 3rdparty/rcheevos/include/rc_api_info.h | 182 + 3rdparty/rcheevos/include/rc_api_request.h | 64 + 3rdparty/rcheevos/include/rc_api_runtime.h | 310 + 3rdparty/rcheevos/include/rc_api_user.h | 149 + 3rdparty/rcheevos/include/rc_client.h | 677 ++ .../include/rc_client_raintegration.h | 85 + 3rdparty/rcheevos/include/rc_consoles.h | 137 + 3rdparty/rcheevos/include/rc_error.h | 55 + 3rdparty/rcheevos/include/rc_export.h | 100 + 3rdparty/rcheevos/include/rc_hash.h | 130 + 3rdparty/rcheevos/include/rc_runtime.h | 152 + 3rdparty/rcheevos/include/rc_runtime_types.h | 429 ++ 3rdparty/rcheevos/include/rc_url.h | 36 + 3rdparty/rcheevos/include/rc_util.h | 51 + 3rdparty/rcheevos/include/rcheevos.h | 8 + 3rdparty/rcheevos/rcheevos | 1 - 3rdparty/rcheevos/rcheevos.vcxproj | 94 +- 3rdparty/rcheevos/rcheevos.vcxproj.filters | 94 +- 3rdparty/rcheevos/src/rapi/rc_api_common.c | 1222 ++++ 3rdparty/rcheevos/src/rapi/rc_api_common.h | 80 + 3rdparty/rcheevos/src/rapi/rc_api_editor.c | 529 ++ 3rdparty/rcheevos/src/rapi/rc_api_info.c | 373 ++ 3rdparty/rcheevos/src/rapi/rc_api_runtime.c | 648 ++ 3rdparty/rcheevos/src/rapi/rc_api_user.c | 254 + 3rdparty/rcheevos/src/rc_client.c | 5635 +++++++++++++++++ 3rdparty/rcheevos/src/rc_client_external.h | 132 + 3rdparty/rcheevos/src/rc_client_internal.h | 394 ++ .../rcheevos/src/rc_client_raintegration.c | 493 ++ .../src/rc_client_raintegration_internal.h | 52 + 3rdparty/rcheevos/src/rc_compat.c | 164 + 3rdparty/rcheevos/src/rc_compat.h | 97 + 3rdparty/rcheevos/src/rc_libretro.c | 831 +++ 3rdparty/rcheevos/src/rc_libretro.h | 93 + 3rdparty/rcheevos/src/rc_util.c | 188 + 3rdparty/rcheevos/src/rc_version.c | 11 + 3rdparty/rcheevos/src/rc_version.h | 32 + 3rdparty/rcheevos/src/rcheevos/alloc.c | 118 + 3rdparty/rcheevos/src/rcheevos/condition.c | 555 ++ 3rdparty/rcheevos/src/rcheevos/condset.c | 437 ++ 3rdparty/rcheevos/src/rcheevos/consoleinfo.c | 1094 ++++ 3rdparty/rcheevos/src/rcheevos/format.c | 280 + 3rdparty/rcheevos/src/rcheevos/lboard.c | 278 + 3rdparty/rcheevos/src/rcheevos/memref.c | 494 ++ 3rdparty/rcheevos/src/rcheevos/operand.c | 475 ++ 3rdparty/rcheevos/src/rcheevos/rc_internal.h | 197 + 3rdparty/rcheevos/src/rcheevos/rc_validate.c | 854 +++ 3rdparty/rcheevos/src/rcheevos/rc_validate.h | 22 + 3rdparty/rcheevos/src/rcheevos/richpresence.c | 829 +++ 3rdparty/rcheevos/src/rcheevos/runtime.c | 892 +++ .../rcheevos/src/rcheevos/runtime_progress.c | 881 +++ 3rdparty/rcheevos/src/rcheevos/trigger.c | 294 + 3rdparty/rcheevos/src/rcheevos/value.c | 719 +++ 3rdparty/rcheevos/src/rhash/cdreader.c | 879 +++ 3rdparty/rcheevos/src/rhash/hash.c | 3306 ++++++++++ 3rdparty/rcheevos/src/rhash/md5.c | 382 ++ 3rdparty/rcheevos/src/rhash/md5.h | 91 + 3rdparty/rcheevos/src/rurl/url.c | 402 ++ pcsx2/pcsx2.vcxproj | 2 +- 68 files changed, 28225 insertions(+), 147 deletions(-) create mode 100644 3rdparty/rcheevos/.editorconfig create mode 100644 3rdparty/rcheevos/.gitignore create mode 100644 3rdparty/rcheevos/CHANGELOG.md create mode 100644 3rdparty/rcheevos/LICENSE create mode 100644 3rdparty/rcheevos/PCSX2-Modifications.txt create mode 100644 3rdparty/rcheevos/Package.swift create mode 100644 3rdparty/rcheevos/README.md create mode 100644 3rdparty/rcheevos/include/rc_api_editor.h create mode 100644 3rdparty/rcheevos/include/rc_api_info.h create mode 100644 3rdparty/rcheevos/include/rc_api_request.h create mode 100644 3rdparty/rcheevos/include/rc_api_runtime.h create mode 100644 3rdparty/rcheevos/include/rc_api_user.h create mode 100644 3rdparty/rcheevos/include/rc_client.h create mode 100644 3rdparty/rcheevos/include/rc_client_raintegration.h create mode 100644 3rdparty/rcheevos/include/rc_consoles.h create mode 100644 3rdparty/rcheevos/include/rc_error.h create mode 100644 3rdparty/rcheevos/include/rc_export.h create mode 100644 3rdparty/rcheevos/include/rc_hash.h create mode 100644 3rdparty/rcheevos/include/rc_runtime.h create mode 100644 3rdparty/rcheevos/include/rc_runtime_types.h create mode 100644 3rdparty/rcheevos/include/rc_url.h create mode 100644 3rdparty/rcheevos/include/rc_util.h create mode 100644 3rdparty/rcheevos/include/rcheevos.h delete mode 160000 3rdparty/rcheevos/rcheevos create mode 100644 3rdparty/rcheevos/src/rapi/rc_api_common.c create mode 100644 3rdparty/rcheevos/src/rapi/rc_api_common.h create mode 100644 3rdparty/rcheevos/src/rapi/rc_api_editor.c create mode 100644 3rdparty/rcheevos/src/rapi/rc_api_info.c create mode 100644 3rdparty/rcheevos/src/rapi/rc_api_runtime.c create mode 100644 3rdparty/rcheevos/src/rapi/rc_api_user.c create mode 100644 3rdparty/rcheevos/src/rc_client.c create mode 100644 3rdparty/rcheevos/src/rc_client_external.h create mode 100644 3rdparty/rcheevos/src/rc_client_internal.h create mode 100644 3rdparty/rcheevos/src/rc_client_raintegration.c create mode 100644 3rdparty/rcheevos/src/rc_client_raintegration_internal.h create mode 100644 3rdparty/rcheevos/src/rc_compat.c create mode 100644 3rdparty/rcheevos/src/rc_compat.h create mode 100644 3rdparty/rcheevos/src/rc_libretro.c create mode 100644 3rdparty/rcheevos/src/rc_libretro.h create mode 100644 3rdparty/rcheevos/src/rc_util.c create mode 100644 3rdparty/rcheevos/src/rc_version.c create mode 100644 3rdparty/rcheevos/src/rc_version.h create mode 100644 3rdparty/rcheevos/src/rcheevos/alloc.c create mode 100644 3rdparty/rcheevos/src/rcheevos/condition.c create mode 100644 3rdparty/rcheevos/src/rcheevos/condset.c create mode 100644 3rdparty/rcheevos/src/rcheevos/consoleinfo.c create mode 100644 3rdparty/rcheevos/src/rcheevos/format.c create mode 100644 3rdparty/rcheevos/src/rcheevos/lboard.c create mode 100644 3rdparty/rcheevos/src/rcheevos/memref.c create mode 100644 3rdparty/rcheevos/src/rcheevos/operand.c create mode 100644 3rdparty/rcheevos/src/rcheevos/rc_internal.h create mode 100644 3rdparty/rcheevos/src/rcheevos/rc_validate.c create mode 100644 3rdparty/rcheevos/src/rcheevos/rc_validate.h create mode 100644 3rdparty/rcheevos/src/rcheevos/richpresence.c create mode 100644 3rdparty/rcheevos/src/rcheevos/runtime.c create mode 100644 3rdparty/rcheevos/src/rcheevos/runtime_progress.c create mode 100644 3rdparty/rcheevos/src/rcheevos/trigger.c create mode 100644 3rdparty/rcheevos/src/rcheevos/value.c create mode 100644 3rdparty/rcheevos/src/rhash/cdreader.c create mode 100644 3rdparty/rcheevos/src/rhash/hash.c create mode 100644 3rdparty/rcheevos/src/rhash/md5.c create mode 100644 3rdparty/rcheevos/src/rhash/md5.h create mode 100644 3rdparty/rcheevos/src/rurl/url.c diff --git a/.gitmodules b/.gitmodules index 04888bdf04..cb02164c45 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,9 +21,6 @@ [submodule "3rdparty/zstd/zstd"] path = 3rdparty/zstd/zstd url = https://github.com/facebook/zstd.git -[submodule "3rdparty/rcheevos/rcheevos"] - path = 3rdparty/rcheevos/rcheevos - url = https://github.com/RetroAchievements/rcheevos.git [submodule "3rdparty/libwebp/libwebp"] path = 3rdparty/libwebp/libwebp url = https://github.com/webmproject/libwebp diff --git a/3rdparty/rcheevos/.editorconfig b/3rdparty/rcheevos/.editorconfig new file mode 100644 index 0000000000..d1591493ba --- /dev/null +++ b/3rdparty/rcheevos/.editorconfig @@ -0,0 +1,15 @@ +# More info: http://EditorConfig.org +root = true + +# * here means any file type +[*] +end_of_line = crlf +insert_final_newline = true + +# latin1 is a type of ASCII, should work with mbcs +[*.{h,c,cpp}] +charset = latin1 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +curly_bracket_next_line = false diff --git a/3rdparty/rcheevos/.gitignore b/3rdparty/rcheevos/.gitignore new file mode 100644 index 0000000000..23ead4b3a7 --- /dev/null +++ b/3rdparty/rcheevos/.gitignore @@ -0,0 +1,65 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Visual Studio files +Debug/ +Release/ +*.user +.vs/ + +# Repository specific +test/test +test/galaga_nes.h +test/smw_snes.h +validator/validator +.vscode/* diff --git a/3rdparty/rcheevos/CHANGELOG.md b/3rdparty/rcheevos/CHANGELOG.md new file mode 100644 index 0000000000..2649f7c5f5 --- /dev/null +++ b/3rdparty/rcheevos/CHANGELOG.md @@ -0,0 +1,339 @@ +# v11.1.0 +* add rc_client_get_user_agent_clause to generate substring to include in client User-Agents +* add rc_client_can_pause function to control pause spam +* add achievement type and rarity to rc_api_fetch_game_data_response_t and rc_client_achievement_t +* add RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED for achievements that have been unlocked locally but not synced to the server +* add RC_CONSOLE_NEO_GEO_CD to supported consoles for chd file extension +* add hash logic for RC_CONSOLE_NINTENDO_3DS (note: added new file rhash/aes.c to support this) +* add hash logic for RC_CONSOLE_MS_DOS +* add game_hash and hardcore fields to rc_api_start_session_request_t and rc_api_ping_request_t +* add RC_FORMAT_FIXED1/2/3, RC_FORMAT_TENS, RC_FORMAT_HUNDREDS, RC_FORMAT_THOUSANDS, and RC_FORMAT_UNSIGNED_VALUE +* add RC_CONSOLE_STANDALONE +* add extern "C" and __cdecl attributes to public functions +* add __declspec(dllexport/dllimport) attributes to public functions via #define enablement +* add rc_version and rc_version_string functions for accessing version from external linkage +* add unicode path support to default filereader (Windows builds) +* add rc_mutex support for GEKKO (libogc) +* fix async_handle being returned when rc_client_begin_login is aborted synchronously +* fix logic error hashing CD files smaller than one sector +* fix read across region boundary in rc_libretro_memory_read +* fix RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW event not being raised if achievement is reset in the same frame that it's primed +* moved rc_util.h from src/ to include/ +* initial (incomplete) support for rc_client_external_t and rc_client_raintegration_t + +# 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 +* add console enum, hash method, and memory map for TI-83 +* add console enum, hash method, and memory map for Uzebox +* add constant for rcheevos version; include in start session server API call +* fix SubSource calculations using float values +* fix game identification for homebrew Jaguar CD games +* fix game identification for CD with many files at root directory +* address _CRT_SECURE_NO_WARNINGS warnings + +# v10.6.0 +* add RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED +* use optimized comparators for most common condition logic +* fix game identification of psx ISOs that have extra slashes in their boot path +* fix game identification of ndd files + +# v10.5.0 +* add RC_MEMSIZE_MBF32_LE +* add RC_OPERATOR_XOR +* add RC_CONSOLE_ATARI_JAGUAR_CD and hash/memory map for Atari Jaguar CD +* add RC_CONSOLE_ARCADIA_2001 and hash/memory map for Arcadia 2001 +* add RC_CONSOLE_INTERTON_VC_4000 and hash/memory map for Interton VC 4000 +* add RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER and hash/memory map for Elektor TV Games Computer +* split RC_CONSOLE_PC_ENGINE_CD off of RC_CONSOLE_PC_ENGINE +* add hash/memory map for RC_CONSOLE_NEO_GEO_CD +* add additional 256KB of RAM to memory map for RC_CONSOLE_SEGA_32X +* validation: don't report redundancy between trigger and non-trigger conditions +* validation: don't report range validation errors for float comparisons +* change default image host to media.retroachievements.org +* fix decoding of denormalized floats +* fix full line comments in the middle of Display: section causing RC_MISSING_DISPLAY_STRING + +# v10.4.0 +* add rc_libretro_hash_set_t with support for #SAVEDISK: m3u extension +* add rc_libretro_is_system_allowed for finer-grain control over core support +* fix measured value from hitcount not resetting while paused +* add RC_CONSOLE_WASM and hash/memory map for WASM-4 +* add scratchpad memory to RC_CONSOLE_PLAYSTATION_2 memory map +* add hash/memory map for RC_CONSOLE_FAIRCHILD_CHANNEL_F +* add hash/memory map for RC_CONSOLE_COMMODORE_64 +* add memory map for RC_CONSOLE_AMIGA + +# v10.3.3 +* add RC_CONSOLE_ARDUBOY and hash/memory map for Arduboy +* add display_name to rc_api_login_response_t +* detect logical conflicts and redundancies in validator +* fix tab sequences in JSON responses being turned into t +* fix overflow when float value has more than 9 digits after the decimal +* fix libretro memory mapping when disconnect mask breaks a region into multiple blocks +* fix non-virtualized file system call when reading some iso files + +# v10.3.2 +* fix RC_OPERAND_PRIOR for bit sizes other than RC_MEMSIZE_BIT_0 +* add memory map and hash for Amstrad CPC +* fix an issue where fetch_game_data and fetch_user_unlocks could return RC_MISSING_VALUE instead of acknowledging a server error + +# v10.3.1 +* allow empty description in rc_api_init_update_leaderboard_request +* fix buffered n64 hash when no filereader is registered +* add memory map and hash for Mega Duck + +# v10.3.0 +* support for floating point memory sizes and logic +* add built-in macros for rich presence: @Number, @Score, @Centisecs, @Seconds, @Minutes, @ASCIIChar, @UnicodeChar +* add rapi functions for fetch_code_notes, update_code_note, upload_achievement, update_leaderboard, fetch_badge_range, and add_game_hash +* add lower_is_better and hidden flags to leaderboards in rc_api_fetch_game_data_response_t +* add achievements_remaining to rc_api_award_achievement_response_t +* add console enums for PC6000, PICO, MEGADUCK and ZEEBO +* add memory map for Dreamcast +* capture leaderboard/rich presence state in rc_runtime_progress data +* support for hashing Dreamcast bin/cues +* support for hashing buffered NDS ROMs +* fix prior for sizes smaller than a byte sometimes returning current value + +# v10.2.0 + +* add RC_MEMSIZE_16_BITS_BE, RC_MEMSIZE_24_BITS_BE, and RC_MEMSIZE_32_BITS_BE +* add secondary flag for RC_CONDITION_MEASURED that tells the UI when to show progress as raw vs. as a percentage +* add rapi calls for fetch_leaderboard_info, fetch_achievement_info and fetch_game_list +* add hash support for RC_CONSOLE_PSP +* add RCHEEVOS_URL_SSL compile flag to use https in rurl functions +* add space to "PC Engine" label +* update RC_CONSOLE_INTELLIVISION memory map to acknowledge non-8-bit addresses +* standardize to z64 format when hashing RC_CONSOLE_N64 +* prevent generating hash for PSX disc when requesting RC_CONSOLE_PLAYSTATION2 +* fix wrong error message being returned when a leaderboard was only slightly malformed + +# v10.1.0 + +* add RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED +* add rc_runtime_validate_addresses +* add external memory to memory map for Magnavox Odyssey 2 +* fix memory map base address for NeoGeo Pocket +* fix bitcount always returning 0 when used in rich presence + +# v10.0.0 + +* add rapi sublibrary for communicating with server (eliminates need for client-side JSON parsing; client must still + provide HTTP functionality). rurl is now deprecated +* renamed 'rhash.h' to 'rc_hash.h' to eliminate conflict with system headers, renamed 'rconsoles.h' and 'rurl.h' for + consistency +* split non-runtime functions out of 'rcheevos.h' as they're not needed by most clients +* allow ranges in rich presence lookups +* add rc_richpresence_size_lines function to fetch line associated to error when processing rich presence script +* add rc_runtime_invalidate_address function to disable achievements when an unknown address is queried +* add RC_CONDITION_RESET_NEXT_IF +* add RC_CONDITION_SUB_HITS +* support MAXOF operator ($) for leaderboard values using trigger syntax +* allow RC_CONDITION_PAUSE_IF and RC_CONDITION_RESET_IF in leaderboard value expression +* changed track parameter of rc_hash_cdreader_open_track_handler to support three virtual tracks: + RC_HASH_CDTRACK_FIRST_DATA, RC_HASH_CDTRACK_LAST and RC_HASH_CDTRACK_LARGEST. +* changed offset parameter of rc_hash_filereader_seek_handler and return value of rc_hash_filereader_tell_handler + from size_t to int64_t to support files larger than 2GB when compiling in 32-bit mode. +* reset to default cd reader if NULL is passed to rc_hash_init_custom_cdreader +* add hash support for RC_CONSOLE_DREAMCAST, RC_CONSOLE_PLAYSTATION_2, RC_CONSOLE_SUPERVISION, and RC_CONSOLE_TIC80 +* ignore headers when generating hashs for RC_CONSOLE_PC_ENGINE and RC_CONSOLE_ATARI_7800 +* require unique identifier when hashing RC_CONSOLE_SEGA_CD and RC_CONSOLE_SATURN discs +* add expansion memory to RC_CONSOLE_SG1000 memory map +* rename RC_CONSOLE_MAGNAVOX_ODYSSEY -> RC_CONSOLE_MAGNAVOX_ODYSSEY2 +* rename RC_CONSOLE_AMIGA_ST -> RC_CONSOLE_ATARI_ST +* add RC_CONSOLE_SUPERVISION, RC_CONSOLE_SHARPX1, RC_CONSOLE_TIC80, RC_CONSOLE_THOMSONTO8 +* fix error identifying largest track when track has multiple bins +* fix memory corruption error when cue track has more than 6 INDEXs +* several improvements to data storage for conditions (rc_memref_t and rc_memref_value_t structures have been modified) + +# v9.2.0 + +* fix issue identifying some PC-FX titles where the boot code is not in the first data track +* add enums and labels for RC_CONSOLE_MAGNAVOX_ODYSSEY, RC_CONSOLE_SUPER_CASSETTEVISION, RC_CONSOLE_NEO_GEO_CD, + RC_CONSOLE_FAIRCHILD_CHANNEL_F, RC_CONSOLE_FM_TOWNS, RC_CONSOLE_ZX_SPECTRUM, RC_CONSOLE_GAME_AND_WATCH, + RC_CONSOLE_NOKIA_NGAGE, RC_CONSOLE_NINTENDO_3DS + +# v9.1.0 + +* add hash support and memory map for RC_CONSOLE_MSX +* add hash support and memory map for RC_CONSOLE_PCFX +* include parent directory when hashing non-arcade titles in arcade mode +* support absolute paths in m3u +* make cue scanning case-insensitive +* expand SRAM mapping for RC_CONSOLE_WONDERSWAN +* fix display of measured value when another group has an unmeasured hit count +* fix memory read error when hashing file with no extension +* fix possible divide by zero when using RC_CONDITION_ADD_SOURCE/RC_CONDITION_SUB_SOURCE +* fix classification of secondary RC_CONSOLE_SATURN memory region + +# v9.0.0 + +* new size: RC_MEMSIZE_BITCOUNT +* new flag: RC_CONDITION_OR_NEXT +* new flag: RC_CONDITION_TRIGGER +* new flag: RC_CONDITION_MEASURED_IF +* new operators: RC_OPERATOR_MULT / RC_OPERATOR_DIV +* is_bcd removed from memref - now part of RC_MEMSIZE +* add rc_runtime_t and associated functions +* add rc_hash_ functions +* add rc_error_str function +* add game_hash parameter to rc_url_award_cheevo +* remove hash parameter from rc_url_submit_lboard +* add rc_url_ping function +* add rc_console_ functions + +# v8.1.0 + +* new flag: RC_CONDITION_MEASURED +* new flag: RC_CONDITION_ADD_ADDRESS +* add rc_evaluate_trigger - extended version of rc_test_trigger with more granular return codes +* make rc_evaluate_value return a signed int (was unsigned int) +* new formats: RC_FORMAT_MINUTES and RC_FORMAT_SECONDS_AS_MINUTES +* removed " Points" text from RC_FORMAT_SCORE format +* removed RC_FORMAT_OTHER format. "OTHER" format now parses to RC_FORMAT_SCORE +* bugfix: AddHits will now honor AndNext on previous condition + +# v8.0.1 + +* bugfix: prevent null reference exception if rich presence contains condition without display string +* bugfix: 24-bit read from memory should only read 24-bits + +# v8.0.0 + +* support for prior operand type +* support for AndNext condition flag +* support for rich presence +* bugfix: update delta/prior memory values while group is paused +* bugfix: allow floating point number without leading 0 +* bugfix: support empty alt groups + +# v7.1.1 + +* Address signed/unsigned mismatch warnings + +# v7.1.0 + +* Added the RC_DISABLE_LUA macro to compile rcheevos without Lua support + +# v7.0.2 + +* Make sure the code is C89-compliant +* Use 32-bit types in Lua +* Only evaluate Lua operands when the Lua state is not `NULL` + +# v7.0.1 + +* Fix the alignment of memory allocations + +# v7.0.0 + +* Removed **rjson** + +# v6.5.0 + +* Added a schema for errors returned by the server + +# v6.4.0 + +* Added an enumeration with the console identifiers used in RetroAchievements + +# v6.3.1 + +* Pass the peek function and the user data to the Lua functions used in operands. + +# v6.3.0 + +* Added **rurl**, an API to build URLs to access RetroAchievements web services. + +# v6.2.0 + +* Added **rjson**, an API to easily decode RetroAchievements JSON files into C structures. + +# v6.1.0 + +* Added support for 24-bit operands with the `'W'` prefix (`RC_OPERAND_24_BITS`) + +# v6.0.2 + +* Only define RC_ALIGNMENT if it has not been already defined + +# v6.0.1 + +* Use `sizeof(void*)` as a better default for `RC_ALIGNMENT` + +# v6.0.0 + +* Simplified API: separate functions to get the buffer size and to parse `memaddr` into the provided buffer +* Fixed crash trying to call `rc_update_condition_pause` during a dry-run +* The callers are now responsible to pass down a scratch buffer to avoid accesses to out-of-scope memory + +# v5.0.0 + +* Pre-compute if a condition has a pause condition in its group +* Added a pre-computed flag that tells if the condition set has at least one pause condition +* Removed the link to the previous condition in a condition set chain + +# v4.0.0 + +* Fixed `ret` not being properly initialized in `rc_parse_trigger` +* Build the unit tests with optimizations and `-Wall` to help catch more issues +* Added `extern "C"` around the inclusion of the Lua headers so that **rcheevos** can be compiled cleanly as C++ +* Exposed `rc_parse_value` and `rc_evaluate_value` to be used with rich presence +* Removed the `reset` and `dirty` flags from the external API + +# v3.2.0 + +* Added the ability to reset triggers and leaderboards +* Add a function to parse a format string and return the format enum, and some unit tests for it + +# v3.1.0 + +* Added `rc_format_value` to the API + +# v3.0.1 + +* Fixed wrong 32-bit value on 64-bit platforms + +# v3.0.0 + +* Removed function rc_evaluate_value from the API + +# v2.0.0 + +* Removed leaderboard callbacks in favor of a simpler scheme + +# v1.1.2 + +* Fixed NULL pointer deference when there's an error during the parse + +# v1.1.1 + +* Removed unwanted garbage +* Should be v1.0.1 :/ + +# v1.0.0 + +* First version diff --git a/3rdparty/rcheevos/CMakeLists.txt b/3rdparty/rcheevos/CMakeLists.txt index 9209cbec15..5c3cb387ed 100644 --- a/3rdparty/rcheevos/CMakeLists.txt +++ b/3rdparty/rcheevos/CMakeLists.txt @@ -1,54 +1,54 @@ add_library(rcheevos - rcheevos/include/rcheevos.h - rcheevos/include/rc_api_editor.h - rcheevos/include/rc_api_info.h - rcheevos/include/rc_api_request.h - rcheevos/include/rc_api_runtime.h - rcheevos/include/rc_api_user.h - rcheevos/include/rc_client.h - rcheevos/include/rc_consoles.h - rcheevos/include/rc_error.h - rcheevos/include/rc_hash.h - rcheevos/include/rc_runtime.h - rcheevos/include/rc_runtime_types.h - rcheevos/include/rc_url.h - rcheevos/src/rapi/rc_api_common.c - rcheevos/src/rapi/rc_api_common.h - rcheevos/src/rapi/rc_api_editor.c - rcheevos/src/rapi/rc_api_info.c - rcheevos/src/rapi/rc_api_runtime.c - rcheevos/src/rapi/rc_api_user.c - rcheevos/src/rcheevos/alloc.c - rcheevos/src/rcheevos/condition.c - rcheevos/src/rcheevos/condset.c - rcheevos/src/rcheevos/consoleinfo.c - rcheevos/src/rcheevos/format.c - rcheevos/src/rcheevos/lboard.c - rcheevos/src/rcheevos/memref.c - rcheevos/src/rcheevos/operand.c - rcheevos/src/rcheevos/rc_internal.h - rcheevos/src/rcheevos/rc_validate.c - rcheevos/src/rcheevos/rc_validate.h - rcheevos/src/rcheevos/richpresence.c - rcheevos/src/rcheevos/runtime.c - rcheevos/src/rcheevos/runtime_progress.c - rcheevos/src/rcheevos/trigger.c - rcheevos/src/rcheevos/value.c - rcheevos/src/rc_client.c - rcheevos/src/rc_client_internal.h - rcheevos/src/rc_compat.c - rcheevos/src/rc_compat.h - rcheevos/src/rc_util.c - rcheevos/src/rc_util.h - rcheevos/src/rc_version.h - rcheevos/src/rhash/cdreader.c - rcheevos/src/rhash/hash.c - rcheevos/src/rhash/md5.c - rcheevos/src/rhash/md5.h - rcheevos/src/rurl/url.c + include/rcheevos.h + include/rc_api_editor.h + include/rc_api_info.h + include/rc_api_request.h + include/rc_api_runtime.h + include/rc_api_user.h + include/rc_client.h + include/rc_consoles.h + include/rc_error.h + include/rc_hash.h + include/rc_runtime.h + include/rc_runtime_types.h + include/rc_url.h + include/rc_util.h + src/rapi/rc_api_common.c + src/rapi/rc_api_common.h + src/rapi/rc_api_editor.c + src/rapi/rc_api_info.c + src/rapi/rc_api_runtime.c + src/rapi/rc_api_user.c + src/rcheevos/alloc.c + src/rcheevos/condition.c + src/rcheevos/condset.c + src/rcheevos/consoleinfo.c + src/rcheevos/format.c + src/rcheevos/lboard.c + src/rcheevos/memref.c + src/rcheevos/operand.c + src/rcheevos/rc_internal.h + src/rcheevos/rc_validate.c + src/rcheevos/rc_validate.h + src/rcheevos/richpresence.c + src/rcheevos/runtime.c + src/rcheevos/runtime_progress.c + src/rcheevos/trigger.c + src/rcheevos/value.c + src/rc_client.c + src/rc_client_internal.h + src/rc_compat.c + src/rc_compat.h + src/rc_util.c + src/rc_version.h + src/rhash/cdreader.c + src/rhash/hash.c + src/rhash/md5.c + src/rhash/md5.h + src/rurl/url.c ) -target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include") -target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include") +target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_compile_definitions(rcheevos PRIVATE "RC_DISABLE_LUA=1" "RCHEEVOS_URL_SSL") diff --git a/3rdparty/rcheevos/LICENSE b/3rdparty/rcheevos/LICENSE new file mode 100644 index 0000000000..5f1faf3dc4 --- /dev/null +++ b/3rdparty/rcheevos/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 RetroAchievements.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/rcheevos/PCSX2-Modifications.txt b/3rdparty/rcheevos/PCSX2-Modifications.txt new file mode 100644 index 0000000000..e7d541f730 --- /dev/null +++ b/3rdparty/rcheevos/PCSX2-Modifications.txt @@ -0,0 +1,2 @@ + - aes and 3DS functions in hash.c removed, due to potential legal issues. + - .github/test/validator directories removed, as they are unnecessary. diff --git a/3rdparty/rcheevos/Package.swift b/3rdparty/rcheevos/Package.swift new file mode 100644 index 0000000000..8205a679ec --- /dev/null +++ b/3rdparty/rcheevos/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "rcheevos", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "rcheevos", + targets: ["rcheevos"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "rcheevos", + dependencies: [], + path: ".", + exclude: ["src/rcheevos/rc_libretro.c"], + sources: ["include", "src/rcheevos", "src/rapi", "src/rhash"], + publicHeadersPath: "include", + cSettings: [ + .define("RC_DISABLE_LUA") + ]), + ] +) diff --git a/3rdparty/rcheevos/README.md b/3rdparty/rcheevos/README.md new file mode 100644 index 0000000000..b0b1a8c602 --- /dev/null +++ b/3rdparty/rcheevos/README.md @@ -0,0 +1,81 @@ +# **rcheevos** + +**rcheevos** is a set of C code, or a library if you will, that tries to make it easier for emulators to process [RetroAchievements](https://retroachievements.org) data, providing support for achievements and leaderboards for their players. + +Keep in mind that **rcheevos** does *not* provide HTTP network connections. Clients must get data from RetroAchievements, and pass the response down to **rcheevos** for processing. + +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. + +## Lua + +RetroAchievements previously considered the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. + +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. + +Lua functions used in trigger operands receive two parameters: `peek`, which is used to read from the emulated system's memory, and `userdata`, which must be passed to `peek`. `peek`'s signature is the same as its C counterpart: + +```lua +function peek(address, num_bytes, userdata) +``` + +## API + +An understanding about how achievements are developed may be useful, you can read more about it [here](http://docs.retroachievements.org/Developer-docs/). + +Most of the exposed APIs are documented [here](https://github.com/RetroAchievements/rcheevos/wiki) + +### Return values + +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 +const char* rc_error_str(int ret); +``` + +### Console identifiers + +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 + +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`. + +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. + +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 + +**rapi** builds URLs to access many RetroAchievements web services. Its purpose it to just to free the developer from having to URL-encode parameters and build correct URLs that are valid for the server. + +**rapi** does *not* make HTTP requests. + +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. + +An example can be found on the [rc_api_init_login_request](https://github.com/RetroAchievements/rcheevos/wiki/rc_api_init_login_request#example) page. + +### Functions + +Please see the [wiki](https://github.com/RetroAchievements/rcheevos/wiki) for details on the functions exposed for **rapi**. + +## Game Identification + +**rhash** provides logic for generating a RetroAchievements hash for a given game. There are two ways to use the API - you can pass the filename and let rhash open and process the file, or you can pass the buffered copy of the file to rhash if you've already loaded it into memory. + +These are in `rc_hash.h`. + +```c + 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/3rdparty/rcheevos/include/rc_api_editor.h b/3rdparty/rcheevos/include/rc_api_editor.h new file mode 100644 index 0000000000..2a6df339e3 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_api_editor.h @@ -0,0 +1,253 @@ +#ifndef RC_API_EDITOR_H +#define RC_API_EDITOR_H + +#include "rc_api_request.h" + +#include + +RC_BEGIN_C_DECLS + +/* --- Fetch Code Notes --- */ + +/** + * API parameters for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_request_t { + /* The unique identifier of the game */ + uint32_t game_id; +} +rc_api_fetch_code_notes_request_t; + +/* A code note definiton */ +typedef struct rc_api_code_note_t { + /* The address the note is associated to */ + uint32_t address; + /* The name of the use who last updated the note */ + const char* author; + /* The contents of the note */ + const char* note; +} rc_api_code_note_t; + +/** + * Response data for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_response_t { + /* An array of code notes for the game */ + rc_api_code_note_t* notes; + /* The number of items in the notes array */ + uint32_t num_notes; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_code_notes_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response); + +/* --- Update Code Note --- */ + +/** + * API parameters for an update code note request. + */ +typedef struct rc_api_update_code_note_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The address the note is associated to */ + uint32_t address; + /* The contents of the note (NULL or empty to delete a note) */ + const char* note; +} +rc_api_update_code_note_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_update_code_note_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_code_note_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response); + +/* --- Update Achievement --- */ + +/** + * API parameters for an update achievement request. + */ +typedef struct rc_api_update_achievement_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement (0 to create a new achievement) */ + uint32_t achievement_id; + /* The unique identifier of the game */ + uint32_t game_id; + /* The name of the achievement */ + const char* title; + /* The description of the achievement */ + const char* description; + /* The badge name for the achievement */ + const char* badge; + /* The serialized trigger for the achievement */ + const char* trigger; + /* The number of points the achievement is worth */ + uint32_t points; + /* The category of the achievement */ + uint32_t category; + /* The type of the achievement */ + uint32_t type; +} +rc_api_update_achievement_request_t; + +/** + * Response data for an update achievement request. + */ +typedef struct rc_api_update_achievement_response_t { + /* The unique identifier of the achievement */ + uint32_t achievement_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_achievement_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response); + +/* --- Update Leaderboard --- */ + +/** + * API parameters for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard (0 to create a new leaderboard) */ + uint32_t leaderboard_id; + /* The unique identifier of the game */ + uint32_t game_id; + /* The name of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The start trigger for the leaderboard */ + const char* start_trigger; + /* The submit trigger for the leaderboard */ + const char* submit_trigger; + /* The cancel trigger for the leaderboard */ + const char* cancel_trigger; + /* The value definition for the leaderboard */ + const char* value_definition; + /* The format of leaderboard values */ + const char* format; + /* Whether or not lower scores are better for the leaderboard */ + uint32_t lower_is_better; +} +rc_api_update_leaderboard_request_t; + +/** + * Response data for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_response_t { + /* The unique identifier of the leaderboard */ + uint32_t leaderboard_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_leaderboard_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response); + +/* --- Fetch Badge Range --- */ + +/** + * API parameters for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_request_t { + /* Unused */ + uint32_t unused; +} +rc_api_fetch_badge_range_request_t; + +/** + * Response data for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_response_t { + /* The numeric identifier of the first valid badge ID */ + uint32_t first_badge_id; + /* The numeric identifier of the first unassigned badge ID */ + uint32_t next_badge_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_badge_range_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response); + +/* --- Add Game Hash --- */ + +/** + * API parameters for an add game hash request. + */ +typedef struct rc_api_add_game_hash_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game (0 to create a new game entry) */ + uint32_t game_id; + /* The unique identifier of the console for the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The hash being added */ + const char* hash; + /* A description of the hash being added (usually the normalized ROM name) */ + const char* hash_description; +} +rc_api_add_game_hash_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_add_game_hash_response_t { + /* The unique identifier of the game */ + uint32_t game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_add_game_hash_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_EDITOR_H */ diff --git a/3rdparty/rcheevos/include/rc_api_info.h b/3rdparty/rcheevos/include/rc_api_info.h new file mode 100644 index 0000000000..93b652bf7a --- /dev/null +++ b/3rdparty/rcheevos/include/rc_api_info.h @@ -0,0 +1,182 @@ +#ifndef RC_API_INFO_H +#define RC_API_INFO_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/* --- Fetch Achievement Info --- */ + +/** + * API parameters for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + uint32_t achievement_id; + /* The 1-based index of the first entry to retrieve */ + uint32_t first_entry; + /* The number of entries to retrieve */ + uint32_t count; + /* Non-zero to only return unlocks earned by the user's friends */ + uint32_t friends_only; +} +rc_api_fetch_achievement_info_request_t; + +/* An achievement awarded entry */ +typedef struct rc_api_achievement_awarded_entry_t { + /* The user associated to the entry */ + const char* username; + /* When the achievement was awarded */ + time_t awarded; +} +rc_api_achievement_awarded_entry_t; + +/** + * Response data for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_response_t { + /* The unique identifier of the achievement */ + uint32_t id; + /* The unique identifier of the game to which the leaderboard is associated */ + uint32_t game_id; + /* The number of times the achievement has been awarded */ + uint32_t num_awarded; + /* The number of players that have earned at least one achievement for the game */ + 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 */ + uint32_t num_recently_awarded; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_achievement_info_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); + +/* --- Fetch Leaderboard Info --- */ + +/** + * API parameters for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_request_t { + /* The unique identifier of the leaderboard */ + uint32_t leaderboard_id; + /* The number of entries to retrieve */ + uint32_t count; + /* The 1-based index of the first entry to retrieve */ + uint32_t first_entry; + /* The username of the player around whom the entries should be returned */ + const char* username; +} +rc_api_fetch_leaderboard_info_request_t; + +/* A leaderboard info entry */ +typedef struct rc_api_lboard_info_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + uint32_t rank; + /* The index of the entry */ + uint32_t index; + /* The value of the entry */ + int32_t score; + /* When the entry was submitted */ + time_t submitted; +} +rc_api_lboard_info_entry_t; + +/** + * Response data for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_response_t { + /* The unique identifier of the leaderboard */ + 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 */ + uint32_t lower_is_better; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* 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 */ + uint32_t game_id; + /* The author of the leaderboard */ + const char* author; + /* When the leaderboard was first uploaded to the server */ + time_t created; + /* When the leaderboard was last modified on the server */ + time_t updated; + + /* An array of requested entries */ + rc_api_lboard_info_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_leaderboard_info_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); + +/* --- Fetch Games List --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_request_t { + /* The unique identifier of the console to query */ + 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 */ + uint32_t id; + /* The name of the game */ + const char* name; +} +rc_api_game_list_entry_t; + +/** + * Response data for a fetch games list request. + */ +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 */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_games_list_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_API_INFO_H */ diff --git a/3rdparty/rcheevos/include/rc_api_request.h b/3rdparty/rcheevos/include/rc_api_request.h new file mode 100644 index 0000000000..dd72fb56de --- /dev/null +++ b/3rdparty/rcheevos/include/rc_api_request.h @@ -0,0 +1,64 @@ +#ifndef RC_API_REQUEST_H +#define RC_API_REQUEST_H + +#include "rc_error.h" +#include "rc_util.h" + +#include + +RC_BEGIN_C_DECLS + +/** + * A constructed request to send to the retroachievements server. + */ +typedef struct rc_api_request_t { + /* The URL to send the request to (contains protocol, host, path, and query args) */ + 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_buffer_t buffer; +} +rc_api_request_t; + +/** + * Common attributes for all server responses. + */ +typedef struct rc_api_response_t { + /* Server-provided success indicator (non-zero on success, zero on failure) */ + 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_buffer_t buffer; +} +rc_api_response_t; + +RC_EXPORT void RC_CCONV rc_api_destroy_request(rc_api_request_t* request); + +RC_EXPORT void RC_CCONV rc_api_set_host(const char* hostname); +RC_EXPORT void RC_CCONV 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 +}; + +RC_END_C_DECLS + +#endif /* RC_API_REQUEST_H */ diff --git a/3rdparty/rcheevos/include/rc_api_runtime.h b/3rdparty/rcheevos/include/rc_api_runtime.h new file mode 100644 index 0000000000..5b5552ca54 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_api_runtime.h @@ -0,0 +1,310 @@ +#ifndef RC_API_RUNTIME_H +#define RC_API_RUNTIME_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/* --- Fetch Image --- */ + +/** + * API parameters for a fetch image request. + * NOTE: fetch image server response is the raw image data. There is no rc_api_process_fetch_image_response function. + */ +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 */ + uint32_t image_type; +} +rc_api_fetch_image_request_t; + +#define RC_IMAGE_TYPE_GAME 1 +#define RC_IMAGE_TYPE_ACHIEVEMENT 2 +#define RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED 3 +#define RC_IMAGE_TYPE_USER 4 + +RC_EXPORT int RC_CCONV rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params); + +/* --- Resolve Hash --- */ + +/** + * API parameters for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_request_t { + /* Unused - hash lookup does not require credentials */ + const char* username; + /* Unused - hash lookup does not require credentials */ + const char* api_token; + /* The generated hash of the game to be identified */ + const char* game_hash; +} +rc_api_resolve_hash_request_t; + +/** + * Response data for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_response_t { + /* The unique identifier of the game, 0 if no match was found */ + uint32_t game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_resolve_hash_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); + +/* --- Fetch Game Data --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + 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 */ + uint32_t id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* 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 */ + uint8_t lower_is_better; + /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ + uint8_t hidden; +} +rc_api_leaderboard_definition_t; + +/* An achievement definition */ +typedef struct rc_api_achievement_definition_t { + /* The unique identifier of the achievement */ + uint32_t id; + /* The number of points the achievement is worth */ + uint32_t points; + /* The achievement category (core, unofficial) */ + uint32_t category; + /* The title of the achievement */ + const char* title; + /* The dscription of the achievement */ + const char* description; + /* The definition of the achievement to be passed to rc_runtime_activate_achievement */ + const char* definition; + /* The author of the achievment */ + const char* author; + /* The image name for the achievement badge */ + const char* badge_name; + /* When the achievement was first uploaded to the server */ + time_t created; + /* When the achievement was last modified on the server */ + time_t updated; + /* The achievement type (win/progression/missable) */ + uint32_t type; + /* The approximate rarity of the achievement (X% of users have earned the achievement) */ + float rarity; + /* The approximate rarity of the achievement in hardcore (X% of users have earned the achievement in hardcore) */ + float rarity_hardcore; +} +rc_api_achievement_definition_t; + +#define RC_ACHIEVEMENT_CATEGORY_CORE 3 +#define RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL 5 + +#define RC_ACHIEVEMENT_TYPE_STANDARD 0 +#define RC_ACHIEVEMENT_TYPE_MISSABLE 1 +#define RC_ACHIEVEMENT_TYPE_PROGRESSION 2 +#define RC_ACHIEVEMENT_TYPE_WIN 3 + +/** + * Response data for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_response_t { + /* The unique identifier of the game */ + uint32_t id; + /* The console associated to the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + + /* An array of achievements for the game */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + 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 */ + uint32_t num_leaderboards; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_data_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); + +/* --- Ping --- */ + +/** + * API parameters for a ping request. + */ +typedef struct rc_api_ping_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* (optional) The current rich presence evaluation for the user */ + const char* rich_presence; + /* (recommended) The hash associated to the game being played */ + const char* game_hash; + /* Non-zero if hardcore is currently enabled (ignored if game_hash is null) */ + uint32_t hardcore; +} +rc_api_ping_request_t; + +/** + * Response data for a ping request. + */ +typedef struct rc_api_ping_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_ping_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_ping_response(rc_api_ping_response_t* response); + +/* --- Award Achievement --- */ + +/** + * API parameters for an award achievement request. + */ +typedef struct rc_api_award_achievement_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + uint32_t achievement_id; + /* Non-zero if the achievement was earned in hardcore */ + uint32_t hardcore; + /* The hash associated to the game being played */ + const char* game_hash; +} +rc_api_award_achievement_request_t; + +/** + * Response data for an award achievement request. + */ +typedef struct rc_api_award_achievement_response_t { + /* The unique identifier of the achievement that was awarded */ + uint32_t awarded_achievement_id; + /* The updated 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) */ + uint32_t achievements_remaining; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_award_achievement_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); + +/* --- Submit Leaderboard Entry --- */ + +/** + * API parameters for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard */ + uint32_t leaderboard_id; + /* The value being submitted */ + int32_t score; + /* The hash associated to the game being played */ + const char* game_hash; +} +rc_api_submit_lboard_entry_request_t; + +/* A leaderboard entry */ +typedef struct rc_api_lboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + uint32_t rank; + /* The value of the entry */ + int32_t score; +} +rc_api_lboard_entry_t; + +/** + * Response data for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_response_t { + /* The value that was submitted */ + int32_t submitted_score; + /* The player's best submitted value */ + int32_t best_score; + /* 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_api_lboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + uint32_t num_top_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_submit_lboard_entry_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_API_RUNTIME_H */ diff --git a/3rdparty/rcheevos/include/rc_api_user.h b/3rdparty/rcheevos/include/rc_api_user.h new file mode 100644 index 0000000000..c06cec4450 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_api_user.h @@ -0,0 +1,149 @@ +#ifndef RC_API_USER_H +#define RC_API_USER_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/* --- Login --- */ + +/** + * API parameters for a login request. + * If both password and api_token are provided, api_token will be ignored. + */ +typedef struct rc_api_login_request_t { + /* The username of the player being logged in */ + const char* username; + /* The API token from a previous login */ + const char* api_token; + /* The player's password */ + const char* password; +} +rc_api_login_request_t; + +/** + * Response data for a login request. + */ +typedef struct rc_api_login_response_t { + /* The case-corrected username of the player */ + const char* username; + /* The API token to use for all future requests */ + const char* api_token; + /* The current score of the player */ + 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 */ + uint32_t num_unread_messages; + /* The preferred name to display for the player */ + const char* display_name; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_login_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_login_response(rc_api_login_response_t* response); + +/* --- Start Session --- */ + +/** + * API parameters for a start session request. + */ +typedef struct rc_api_start_session_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* (recommended) The hash associated to the game being played */ + const char* game_hash; + /* Non-zero if hardcore is currently enabled (ignored if game_hash is null) */ + uint32_t hardcore; +} +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; +} +rc_api_start_session_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); + +/* --- Fetch User Unlocks --- */ + +/** + * API parameters for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* Non-zero to fetch hardcore unlocks, 0 to fetch non-hardcore unlocks */ + uint32_t hardcore; +} +rc_api_fetch_user_unlocks_request_t; + +/** + * Response data for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_response_t { + /* An array of achievement IDs previously unlocked by the user */ + uint32_t* achievement_ids; + /* The number of items in the achievement_ids array */ + uint32_t num_achievement_ids; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_user_unlocks_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_API_H */ diff --git a/3rdparty/rcheevos/include/rc_client.h b/3rdparty/rcheevos/include/rc_client.h new file mode 100644 index 0000000000..48bef373ee --- /dev/null +++ b/3rdparty/rcheevos/include/rc_client.h @@ -0,0 +1,677 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +RC_BEGIN_C_DECLS + +/* 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_CCONV *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_CCONV *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_CCONV *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_CCONV *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_CCONV *rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +RC_EXPORT rc_client_t* RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +RC_EXPORT int RC_CCONV rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +RC_EXPORT void RC_CCONV rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +RC_EXPORT void* RC_CCONV rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +RC_EXPORT void RC_CCONV rc_client_set_host(const rc_client_t* client, const char* hostname); + +typedef uint64_t rc_clock_t; +typedef rc_clock_t (RC_CCONV *rc_get_time_millisecs_func_t)(const rc_client_t* client); + +/** + * Specifies a function that returns a value that increases once per millisecond. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/** + * Gets a clause that can be added to the User-Agent to identify the version of rcheevos being used. + */ +RC_EXPORT size_t RC_CCONV rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +RC_EXPORT void RC_CCONV 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_EXPORT rc_client_async_handle_t* RC_CCONV 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_EXPORT rc_client_async_handle_t* RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT const rc_client_user_t* RC_CCONV rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV 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_EXPORT rc_client_async_handle_t* RC_CCONV 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_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Unloads the current game. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT const rc_client_game_t* RC_CCONV rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV 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_EXPORT rc_client_async_handle_t* RC_CCONV 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; + +RC_EXPORT const rc_client_subset_t* RC_CCONV 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_TYPE_STANDARD = 0, + RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE = 1, + RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION = 2, + RC_CLIENT_ACHIEVEMENT_TYPE_WIN = 3 +}; + +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, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED = 8, + NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 9 +}; + +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; + /* minimum version: 11.1 */ + float rarity; + float rarity_hardcore; + uint8_t type; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +RC_EXPORT const rc_client_achievement_t* RC_CCONV rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV 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_EXPORT rc_client_achievement_list_t* RC_CCONV rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_get_achievement_list. + */ +RC_EXPORT void RC_CCONV 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(). + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT const rc_client_leaderboard_t* RC_CCONV 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_EXPORT rc_client_leaderboard_list_t* RC_CCONV rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_get_leaderboard_list. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +/** + * Returns non-zero if the current game has any leaderboards. + */ +RC_EXPORT int RC_CCONV 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_CCONV *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_EXPORT rc_client_async_handle_t* RC_CCONV 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_EXPORT rc_client_async_handle_t* RC_CCONV 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. + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT int RC_CCONV rc_client_has_rich_presence(rc_client_t* client); + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +RC_EXPORT size_t RC_CCONV 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_CCONV *rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +RC_EXPORT void RC_CCONV rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT int RC_CCONV rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +RC_EXPORT void RC_CCONV 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. + */ +RC_EXPORT void RC_CCONV rc_client_idle(rc_client_t* client); + +/** + * Determines if a sufficient amount of frames have been processed since the last call to rc_client_can_pause. + * Should not be called unless the client is trying to pause. + * If false is returned, and frames_remaining is not NULL, frames_remaining will be set to the number of frames + * still required before pause is allowed, which can be converted to a time in seconds for displaying to the user. + */ +RC_EXPORT int RC_CCONV rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +RC_EXPORT void RC_CCONV rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +RC_EXPORT size_t RC_CCONV rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +RC_EXPORT int RC_CCONV 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. + */ +RC_EXPORT int RC_CCONV rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +RC_END_C_DECLS + +#endif /* RC_RUNTIME_H */ diff --git a/3rdparty/rcheevos/include/rc_client_raintegration.h b/3rdparty/rcheevos/include/rc_client_raintegration.h new file mode 100644 index 0000000000..b2e77cdec0 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_client_raintegration.h @@ -0,0 +1,85 @@ +#ifndef RC_CLIENT_RAINTEGRATION_H +#define RC_CLIENT_RAINTEGRATION_H + +#ifndef _WIN32 + #undef RC_CLIENT_SUPPORTS_RAINTEGRATION /* Windows required for RAIntegration */ +#endif + +#include + +#include "rc_export.h" + +RC_BEGIN_C_DECLS + +typedef struct rc_client_t rc_client_t; /* forward reference; in rc_client.h */ + +/* types needed to implement raintegration */ + +typedef struct rc_client_raintegration_menu_item_t { + const char* label; + uint32_t id; + uint8_t checked; + uint8_t enabled; +} rc_client_raintegration_menu_item_t; + +typedef struct rc_client_raintegration_menu_t { + rc_client_raintegration_menu_item_t* items; + uint32_t num_items; +} rc_client_raintegration_menu_t; + +enum { + RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE = 0, + RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED = 1, /* [menu_item] checked changed */ + RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED = 2, /* hardcore was enabled or disabled */ + RC_CLIENT_RAINTEGRATION_EVENT_PAUSE = 3 /* emulated system should be paused */ +}; + +typedef struct rc_client_raintegration_event_t { + uint32_t type; + + const rc_client_raintegration_menu_item_t* menu_item; +} rc_client_raintegration_event_t; + +typedef void (RC_CCONV *rc_client_raintegration_event_handler_t)(const rc_client_raintegration_event_t* event, + rc_client_t* client); + +typedef void (RC_CCONV *rc_client_raintegration_write_memory_func_t)(uint32_t address, uint8_t* buffer, + uint32_t num_bytes, rc_client_t* client); + +/* types needed to integrate raintegration */ + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +#ifndef RC_CLIENT_SUPPORTS_EXTERNAL + #define RC_CLIENT_SUPPORTS_EXTERNAL /* external rc_client required for RAIntegration */ +#endif + +#include /* HWND */ + +#include "rc_client.h" + +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_raintegration(rc_client_t* client, + const wchar_t* search_directory, HWND main_window_handle, + const char* client_name, const char* client_version, + rc_client_callback_t callback, void* callback_userdata); + +RC_EXPORT void RC_CCONV rc_client_unload_raintegration(rc_client_t* client); + +RC_EXPORT void RC_CCONV rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle); + +RC_EXPORT const rc_client_raintegration_menu_t* RC_CCONV rc_client_raintegration_get_menu(const rc_client_t* client); + +RC_EXPORT void RC_CCONV rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu); +RC_EXPORT void RC_CCONV rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menu_item); +RC_EXPORT int RC_CCONV rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t nMenuItemId); + +RC_EXPORT void RC_CCONV rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler); + +RC_EXPORT void RC_CCONV rc_client_raintegration_set_event_handler(rc_client_t* client, + rc_client_raintegration_event_handler_t handler); + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ + +RC_END_C_DECLS + +#endif /* RC_CLIENT_RAINTEGRATION_H */ diff --git a/3rdparty/rcheevos/include/rc_consoles.h b/3rdparty/rcheevos/include/rc_consoles.h new file mode 100644 index 0000000000..269879202b --- /dev/null +++ b/3rdparty/rcheevos/include/rc_consoles.h @@ -0,0 +1,137 @@ +#ifndef RC_CONSOLES_H +#define RC_CONSOLES_H + +#include "rc_export.h" + +#include + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Console identifiers | +\*****************************************************************************/ + +enum { + RC_CONSOLE_UNKNOWN = 0, + 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, + RC_CONSOLE_PC6000 = 67, + RC_CONSOLE_PICO = 68, + RC_CONSOLE_MEGADUCK = 69, + RC_CONSOLE_ZEEBO = 70, + RC_CONSOLE_ARDUBOY = 71, + RC_CONSOLE_WASM4 = 72, + RC_CONSOLE_ARCADIA_2001 = 73, + RC_CONSOLE_INTERTON_VC_4000 = 74, + RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER = 75, + RC_CONSOLE_PC_ENGINE_CD = 76, + RC_CONSOLE_ATARI_JAGUAR_CD = 77, + RC_CONSOLE_NINTENDO_DSI = 78, + RC_CONSOLE_TI83 = 79, + RC_CONSOLE_UZEBOX = 80, + + RC_CONSOLE_HUBS = 100, + RC_CONSOLE_EVENTS = 101, + RC_CONSOLE_STANDALONE = 102 +}; + +RC_EXPORT const char* RC_CCONV rc_console_name(uint32_t console_id); + +/*****************************************************************************\ +| Memory mapping | +\*****************************************************************************/ + +enum { + RC_MEMORY_TYPE_SYSTEM_RAM, /* normal system memory */ + RC_MEMORY_TYPE_SAVE_RAM, /* memory that persists between sessions */ + RC_MEMORY_TYPE_VIDEO_RAM, /* memory reserved for graphical processing */ + RC_MEMORY_TYPE_READONLY, /* memory that maps to read only data */ + RC_MEMORY_TYPE_HARDWARE_CONTROLLER, /* memory for interacting with system components */ + RC_MEMORY_TYPE_VIRTUAL_RAM, /* secondary address space that maps to real memory in system RAM */ + RC_MEMORY_TYPE_UNUSED /* these addresses don't really exist */ +}; + +typedef struct rc_memory_region_t { + 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; + uint32_t num_regions; +} +rc_memory_regions_t; + +RC_EXPORT const rc_memory_regions_t* RC_CCONV rc_console_memory_regions(uint32_t console_id); + +RC_END_C_DECLS + +#endif /* RC_CONSOLES_H */ diff --git a/3rdparty/rcheevos/include/rc_error.h b/3rdparty/rcheevos/include/rc_error.h new file mode 100644 index 0000000000..d0aca2fd70 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_error.h @@ -0,0 +1,55 @@ +#ifndef RC_ERROR_H +#define RC_ERROR_H + +#include "rc_export.h" + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Return values | +\*****************************************************************************/ + +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, + 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 +}; + +RC_EXPORT const char* RC_CCONV rc_error_str(int ret); + +RC_END_C_DECLS + +#endif /* RC_ERROR_H */ diff --git a/3rdparty/rcheevos/include/rc_export.h b/3rdparty/rcheevos/include/rc_export.h new file mode 100644 index 0000000000..da111056d3 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_export.h @@ -0,0 +1,100 @@ +#ifndef RC_EXPORT_H +#define RC_EXPORT_H + +/* These macros control how callbacks and public functions are defined */ + +/* RC_SHARED should be defined when building rcheevos as a shared library (e.g. dll/dylib/so). External code should not define this macro. */ +/* RC_STATIC should be defined when building rcheevos as a static library. External code should also define this macro. */ +/* RC_IMPORT should be defined for external code using rcheevos as a shared library. */ + +/* For compatibility, if none of these three macros are defined, then the build is assumed to be RC_STATIC */ + +#if !defined(RC_SHARED) && !defined(RC_STATIC) && !defined(RC_IMPORT) + #define RC_STATIC +#endif + +#if (defined(RC_SHARED) && defined(RC_STATIC)) || (defined(RC_SHARED) && defined(RC_IMPORT)) || (defined(RC_STATIC) && defined(RC_IMPORT)) + #error RC_SHARED, RC_STATIC, and RC_IMPORT are mutually exclusive +#endif + +/* RC_BEGIN_C_DECLS and RC_END_C_DECLS should be used for all headers, to enforce C linkage and the C calling convention */ +/* RC_BEGIN_C_DECLS should be placed after #include's and before header declarations */ +/* RC_END_C_DECLS should be placed after header declarations */ + +/* example usage + * + * #ifndef RC_HEADER_H + * #define RC_HEADER_H + * + * #include + * + * RC_BEGIN_C_DECLS + * + * uint8_t rc_function(void); + * + * RC_END_C_DECLS + * + * #endif + */ + +#ifdef __cplusplus + #define RC_BEGIN_C_DECLS extern "C" { + #define RC_END_C_DECLS } +#else + #define RC_BEGIN_C_DECLS + #define RC_END_C_DECLS +#endif + +/* RC_CCONV should be used for public functions and callbacks, to enforce the cdecl calling convention, if applicable */ +/* RC_CCONV should be placed after the return type, and between the ( and * for callbacks */ + +/* example usage */ +/* void RC_CCONV rc_function(void) */ +/* void (RC_CCONV *rc_callback)(void) */ + +#if defined(_WIN32) + /* Windows compilers will ignore __cdecl when not applicable */ + #define RC_CCONV __cdecl +#elif defined(__GNUC__) && defined(__i386__) + /* GNU C compilers will warn if cdecl is defined on an unsupported platform */ + #define RC_CCONV __attribute__((cdecl)) +#else + #define RC_CCONV +#endif + +/* RC_EXPORT should be used for public functions */ +/* RC_EXPORT will provide necessary hints for shared library usage, if applicable */ +/* RC_EXPORT should be placed before the return type */ + +/* example usage */ +/* RC_EXPORT void rc_function(void) */ + +#ifdef RC_SHARED + #if defined(_WIN32) + #define RC_EXPORT __declspec(dllexport) + #elif defined(__GNUC__) && __GNUC__ >= 4 + #define RC_EXPORT __attribute__((visibility("default"))) + #else + #define RC_EXPORT + #endif +#endif + +#ifdef RC_IMPORT + #if defined(_WIN32) + #define RC_EXPORT __declspec(dllimport) + #elif defined(__GNUC__) && __GNUC__ >= 4 + #define RC_EXPORT __attribute__((visibility("default"))) + #else + #define RC_EXPORT + #endif +#endif + +#ifdef RC_STATIC + #if defined(__GNUC__) && __GNUC__ >= 4 + #define RC_EXPORT __attribute__((visibility("default"))) + #else + #define RC_EXPORT + #endif +#endif + +#endif /* RC_EXPORT_H */ diff --git a/3rdparty/rcheevos/include/rc_hash.h b/3rdparty/rcheevos/include/rc_hash.h new file mode 100644 index 0000000000..a762615cbb --- /dev/null +++ b/3rdparty/rcheevos/include/rc_hash.h @@ -0,0 +1,130 @@ +#ifndef RC_HASH_H +#define RC_HASH_H + +#include +#include +#include + +#include "rc_consoles.h" + +RC_BEGIN_C_DECLS + + /* ===================================================== */ + + /* generates a hash from a block of memory. + * returns non-zero on success, or zero on failure. + */ + RC_EXPORT int RC_CCONV rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size); + + /* generates a hash from a file. + * returns non-zero on success, or zero on failure. + */ + RC_EXPORT int RC_CCONV rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path); + + /* ===================================================== */ + + /* data for rc_hash_iterate + */ + typedef struct rc_hash_iterator + { + 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) + */ + RC_EXPORT void RC_CCONV 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 + */ + RC_EXPORT void RC_CCONV rc_hash_destroy_iterator(struct rc_hash_iterator* iterator); + + /* generates the next hash for the data in the rc_hash_iterator. + * returns non-zero if a hash was generated, or zero if no more hashes can be generated for the data. + */ + RC_EXPORT int RC_CCONV rc_hash_iterate(char hash[33], struct rc_hash_iterator* iterator); + + /* ===================================================== */ + + /* specifies a function to call when an error occurs to display the error message */ + typedef void (RC_CCONV *rc_hash_message_callback)(const char*); + RC_EXPORT void RC_CCONV rc_hash_init_error_message_callback(rc_hash_message_callback callback); + + /* specifies a function to call for verbose logging */ + RC_EXPORT void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback); + + /* ===================================================== */ + + /* opens a file */ + typedef void* (RC_CCONV *rc_hash_filereader_open_file_handler)(const char* path_utf8); + + /* moves the file pointer - standard fseek parameters */ + typedef void (RC_CCONV *rc_hash_filereader_seek_handler)(void* file_handle, int64_t offset, int origin); + + /* locates the file pointer */ + typedef int64_t (RC_CCONV *rc_hash_filereader_tell_handler)(void* file_handle); + + /* reads the specified number of bytes from the file starting at the read pointer. + * returns the number of bytes actually read. + */ + typedef size_t (RC_CCONV *rc_hash_filereader_read_handler)(void* file_handle, void* buffer, size_t requested_bytes); + + /* closes the file */ + typedef void (RC_CCONV *rc_hash_filereader_close_file_handler)(void* file_handle); + + struct rc_hash_filereader + { + rc_hash_filereader_open_file_handler open; + rc_hash_filereader_seek_handler seek; + rc_hash_filereader_tell_handler tell; + rc_hash_filereader_read_handler read; + rc_hash_filereader_close_file_handler close; + }; + + RC_EXPORT void RC_CCONV rc_hash_init_custom_filereader(struct rc_hash_filereader* reader); + + /* ===================================================== */ + + #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. 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_CCONV *rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); + + /* attempts to read the specified number of bytes from the file starting at the specified absolute sector. + * returns the number of bytes actually read. + */ + typedef size_t (RC_CCONV *rc_hash_cdreader_read_sector_handler)(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes); + + /* closes the track handle */ + typedef void (RC_CCONV *rc_hash_cdreader_close_track_handler)(void* track_handle); + + /* gets the absolute sector index for the first sector of a track */ + typedef uint32_t(RC_CCONV *rc_hash_cdreader_first_track_sector_handler)(void* track_handle); + + struct rc_hash_cdreader + { + rc_hash_cdreader_open_track_handler open_track; + rc_hash_cdreader_read_sector_handler read_sector; + rc_hash_cdreader_close_track_handler close_track; + rc_hash_cdreader_first_track_sector_handler first_track_sector; + }; + + RC_EXPORT void RC_CCONV rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader); + RC_EXPORT void RC_CCONV rc_hash_init_default_cdreader(void); + RC_EXPORT void RC_CCONV rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader); + + /* ===================================================== */ + +RC_END_C_DECLS + +#endif /* RC_HASH_H */ diff --git a/3rdparty/rcheevos/include/rc_runtime.h b/3rdparty/rcheevos/include/rc_runtime.h new file mode 100644 index 0000000000..c5780c47a2 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_runtime.h @@ -0,0 +1,152 @@ +#ifndef RC_RUNTIME_H +#define RC_RUNTIME_H + +#include "rc_error.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Forward Declarations (defined in rc_runtime_types.h) | +\*****************************************************************************/ + +#ifndef RC_RUNTIME_TYPES_H /* prevents pedantic redefinition error */ + +typedef struct lua_State lua_State; + +typedef struct rc_trigger_t rc_trigger_t; +typedef struct rc_lboard_t rc_lboard_t; +typedef struct rc_richpresence_t rc_richpresence_t; +typedef struct rc_memref_t rc_memref_t; +typedef struct rc_value_t rc_value_t; + +#endif + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address. If + * num_bytes is greater than 1, the value is read in little-endian from + * memory. + */ +typedef uint32_t(RC_CCONV *rc_runtime_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +typedef struct rc_runtime_trigger_t { + uint32_t id; + rc_trigger_t* trigger; + void* buffer; + rc_memref_t* invalid_memref; + uint8_t md5[16]; + int32_t serialized_size; + uint8_t owns_memrefs; +} +rc_runtime_trigger_t; + +typedef struct rc_runtime_lboard_t { + uint32_t id; + int32_t value; + rc_lboard_t* lboard; + void* buffer; + rc_memref_t* invalid_memref; + uint8_t md5[16]; + uint32_t serialized_size; + uint8_t owns_memrefs; +} +rc_runtime_lboard_t; + +typedef struct rc_runtime_richpresence_t { + rc_richpresence_t* richpresence; + void* buffer; + struct rc_runtime_richpresence_t* previous; + uint8_t md5[16]; + uint8_t owns_memrefs; +} +rc_runtime_richpresence_t; + +typedef struct rc_runtime_t { + rc_runtime_trigger_t* triggers; + uint32_t trigger_count; + uint32_t trigger_capacity; + + rc_runtime_lboard_t* lboards; + uint32_t lboard_count; + uint32_t lboard_capacity; + + rc_runtime_richpresence_t* richpresence; + + rc_memref_t* memrefs; + rc_memref_t** next_memref; + + rc_value_t* variables; + rc_value_t** next_variable; + + uint8_t owns_self; +} +rc_runtime_t; + +RC_EXPORT rc_runtime_t* RC_CCONV rc_runtime_alloc(void); +RC_EXPORT void RC_CCONV rc_runtime_init(rc_runtime_t* runtime); +RC_EXPORT void RC_CCONV rc_runtime_destroy(rc_runtime_t* runtime); + +RC_EXPORT int RC_CCONV rc_runtime_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +RC_EXPORT void RC_CCONV rc_runtime_deactivate_achievement(rc_runtime_t* runtime, uint32_t id); +RC_EXPORT rc_trigger_t* RC_CCONV rc_runtime_get_achievement(const rc_runtime_t* runtime, uint32_t id); +RC_EXPORT int RC_CCONV rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target); +RC_EXPORT int RC_CCONV rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char *buffer, size_t buffer_size); + +RC_EXPORT int RC_CCONV rc_runtime_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +RC_EXPORT void RC_CCONV rc_runtime_deactivate_lboard(rc_runtime_t* runtime, uint32_t id); +RC_EXPORT rc_lboard_t* RC_CCONV rc_runtime_get_lboard(const rc_runtime_t* runtime, uint32_t id); +RC_EXPORT int RC_CCONV rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format); + + +RC_EXPORT int RC_CCONV rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); +RC_EXPORT int RC_CCONV 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 */ + RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, + RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, + RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, + RC_RUNTIME_EVENT_LBOARD_STARTED, + RC_RUNTIME_EVENT_LBOARD_CANCELED, + RC_RUNTIME_EVENT_LBOARD_UPDATED, + RC_RUNTIME_EVENT_LBOARD_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, + RC_RUNTIME_EVENT_LBOARD_DISABLED, + RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED +}; + +typedef struct rc_runtime_event_t { + uint32_t id; + int32_t value; + uint8_t type; +} +rc_runtime_event_t; + +typedef void (RC_CCONV *rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); + +RC_EXPORT void RC_CCONV 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); +RC_EXPORT void RC_CCONV rc_runtime_reset(rc_runtime_t* runtime); + +typedef int (RC_CCONV *rc_runtime_validate_address_t)(uint32_t address); +RC_EXPORT void RC_CCONV rc_runtime_validate_addresses(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); +RC_EXPORT void RC_CCONV rc_runtime_invalidate_address(rc_runtime_t* runtime, uint32_t address); + +RC_EXPORT int RC_CCONV rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L); +RC_EXPORT int RC_CCONV rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L); +RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L); + +RC_END_C_DECLS + +#endif /* RC_RUNTIME_H */ diff --git a/3rdparty/rcheevos/include/rc_runtime_types.h b/3rdparty/rcheevos/include/rc_runtime_types.h new file mode 100644 index 0000000000..d8a7db65d0 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_runtime_types.h @@ -0,0 +1,429 @@ +#ifndef RC_RUNTIME_TYPES_H +#define RC_RUNTIME_TYPES_H + +#include "rc_error.h" + +#include +#include + +RC_BEGIN_C_DECLS + +#ifndef RC_RUNTIME_H /* prevents pedantic redefiniton error */ + +typedef struct lua_State lua_State; + +typedef struct rc_trigger_t rc_trigger_t; +typedef struct rc_lboard_t rc_lboard_t; +typedef struct rc_richpresence_t rc_richpresence_t; +typedef struct rc_memref_t rc_memref_t; +typedef struct rc_value_t rc_value_t; + +#endif + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address. If + * num_bytes is greater than 1, the value is read in little-endian from + * memory. + */ +typedef uint32_t(RC_CCONV *rc_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); + +/*****************************************************************************\ +| Memory References | +\*****************************************************************************/ + +/* Sizes. */ +enum { + RC_MEMSIZE_8_BITS, + RC_MEMSIZE_16_BITS, + RC_MEMSIZE_24_BITS, + RC_MEMSIZE_32_BITS, + RC_MEMSIZE_LOW, + RC_MEMSIZE_HIGH, + RC_MEMSIZE_BIT_0, + RC_MEMSIZE_BIT_1, + RC_MEMSIZE_BIT_2, + RC_MEMSIZE_BIT_3, + RC_MEMSIZE_BIT_4, + RC_MEMSIZE_BIT_5, + RC_MEMSIZE_BIT_6, + RC_MEMSIZE_BIT_7, + RC_MEMSIZE_BITCOUNT, + RC_MEMSIZE_16_BITS_BE, + RC_MEMSIZE_24_BITS_BE, + RC_MEMSIZE_32_BITS_BE, + 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. */ + uint32_t value; + /* The last differing value of this memory reference. */ + uint32_t prior; + + /* The size of the value. */ + uint8_t size; + /* True if the value changed this frame. */ + uint8_t changed; + /* The value type of the value (for variables) */ + 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 */ + uint8_t is_indirect; +} +rc_memref_value_t; + +struct rc_memref_t { + /* The current value at the specified memory address. */ + rc_memref_value_t value; + + /* The memory address of this variable. */ + uint32_t address; + + /* The next memory reference in the chain. */ + rc_memref_t* next; +}; + +/*****************************************************************************\ +| Operands | +\*****************************************************************************/ + +/* types */ +enum { + RC_OPERAND_ADDRESS, /* The value of a live address in RAM. */ + RC_OPERAND_DELTA, /* The value last known at this address. */ + RC_OPERAND_CONST, /* A 32-bit unsigned integer. */ + RC_OPERAND_FP, /* A floating point value. */ + RC_OPERAND_LUA, /* A Lua function that provides the value. */ + RC_OPERAND_PRIOR, /* The last differing value at this address. */ + RC_OPERAND_BCD, /* The BCD-decoded value of a live address in RAM. */ + RC_OPERAND_INVERTED /* The twos-complement value of a live address in RAM. */ +}; + +typedef struct rc_operand_t { + union { + /* A value read from memory. */ + rc_memref_t* memref; + + /* An integer value. */ + uint32_t num; + + /* A floating point value. */ + double dbl; + + /* A reference to the Lua function that provides the value. */ + int luafunc; + } value; + + /* specifies which member of the value union is being used */ + uint8_t type; + + /* the actual RC_MEMSIZE of the operand - memref.size may differ */ + uint8_t size; +} +rc_operand_t; + +RC_EXPORT int RC_CCONV rc_operand_is_memref(const rc_operand_t* operand); + +/*****************************************************************************\ +| Conditions | +\*****************************************************************************/ + +/* types */ +enum { + /* NOTE: this enum is ordered to optimize the switch statements in rc_test_condset_internal. the values may change between releases */ + + /* non-combining conditions (third switch) */ + RC_CONDITION_STANDARD, /* this should always be 0 */ + RC_CONDITION_PAUSE_IF, + RC_CONDITION_RESET_IF, + RC_CONDITION_MEASURED_IF, + RC_CONDITION_TRIGGER, + RC_CONDITION_MEASURED, /* measured also appears in the first switch, so place it at the border between them */ + + /* modifiers (first switch) */ + RC_CONDITION_ADD_SOURCE, /* everything from this point on affects the condition after it */ + RC_CONDITION_SUB_SOURCE, + RC_CONDITION_ADD_ADDRESS, + + /* logic flags (second switch) */ + RC_CONDITION_ADD_HITS, + RC_CONDITION_SUB_HITS, + RC_CONDITION_RESET_NEXT_IF, + RC_CONDITION_AND_NEXT, + RC_CONDITION_OR_NEXT +}; + +/* operators */ +enum { + RC_OPERATOR_EQ, + RC_OPERATOR_LT, + RC_OPERATOR_LE, + RC_OPERATOR_GT, + RC_OPERATOR_GE, + RC_OPERATOR_NE, + RC_OPERATOR_NONE, + RC_OPERATOR_MULT, + RC_OPERATOR_DIV, + RC_OPERATOR_AND, + RC_OPERATOR_XOR +}; + +typedef struct rc_condition_t rc_condition_t; + +struct rc_condition_t { + /* The condition's operands. */ + rc_operand_t operand1; + rc_operand_t operand2; + + /* Required hits to fire this condition. */ + uint32_t required_hits; + /* Number of hits so far. */ + uint32_t current_hits; + + /* The next condition in the chain. */ + rc_condition_t* next; + + /* The type of the condition. (RC_CONDITION_*) */ + uint8_t type; + + /* 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. (bool) */ + uint8_t pause; + + /* Whether or not the condition evaluated true on the last check. (bool) */ + uint8_t is_true; + + /* Unique identifier of optimized comparator to use. (RC_PROCESSING_COMPARE_*) */ + uint8_t optimized_comparator; +}; + +/*****************************************************************************\ +| Condition sets | +\*****************************************************************************/ + +typedef struct rc_condset_t rc_condset_t; + +struct rc_condset_t { + /* The next condition set in the chain. */ + rc_condset_t* next; + + /* The list of conditions in this condition set. */ + rc_condition_t* conditions; + + /* True if any condition in the set is a pause condition. */ + uint8_t has_pause; + + /* True if the set is currently paused. */ + uint8_t is_paused; + + /* True if the set has indirect memory references. */ + uint8_t has_indirect_memrefs; +}; + +/*****************************************************************************\ +| Trigger | +\*****************************************************************************/ + +enum { + RC_TRIGGER_STATE_INACTIVE, /* achievement is not being processed */ + RC_TRIGGER_STATE_WAITING, /* achievement cannot trigger until it has been false for at least one frame */ + RC_TRIGGER_STATE_ACTIVE, /* achievement is active and may trigger */ + RC_TRIGGER_STATE_PAUSED, /* achievement is currently paused and will not trigger */ + RC_TRIGGER_STATE_RESET, /* achievement hit counts were reset */ + RC_TRIGGER_STATE_TRIGGERED, /* achievement has triggered */ + RC_TRIGGER_STATE_PRIMED, /* all non-Trigger conditions are true */ + RC_TRIGGER_STATE_DISABLED /* achievement cannot be processed at this time */ +}; + +struct rc_trigger_t { + /* The main condition set. */ + rc_condset_t* requirement; + + /* The list of sub condition sets in this test. */ + rc_condset_t* alternative; + + /* The memory references required by the trigger. */ + rc_memref_t* memrefs; + + /* The current state of the MEASURED condition. */ + uint32_t measured_value; + + /* The target state of the MEASURED condition */ + uint32_t measured_target; + + /* The current state of the trigger */ + uint8_t state; + + /* True if at least one condition has a non-zero hit count */ + uint8_t has_hits; + + /* True if at least one condition has a non-zero required hit count */ + uint8_t has_required_hits; + + /* True if the measured value should be displayed as a percentage */ + uint8_t measured_as_percent; +}; + +RC_EXPORT int RC_CCONV rc_trigger_size(const char* memaddr); +RC_EXPORT rc_trigger_t* RC_CCONV rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +RC_EXPORT int RC_CCONV rc_evaluate_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); +RC_EXPORT int RC_CCONV rc_test_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); +RC_EXPORT void RC_CCONV rc_reset_trigger(rc_trigger_t* self); + +/*****************************************************************************\ +| Values | +\*****************************************************************************/ + +struct rc_value_t { + /* The current value of the variable. */ + rc_memref_value_t value; + + /* The list of conditions to evaluate. */ + rc_condset_t* conditions; + + /* The memory references required by the variable. */ + rc_memref_t* memrefs; + + /* The name of the variable. */ + const char* name; + + /* The next variable in the chain. */ + rc_value_t* next; +}; + +RC_EXPORT int RC_CCONV rc_value_size(const char* memaddr); +RC_EXPORT rc_value_t* RC_CCONV rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +RC_EXPORT int32_t RC_CCONV rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +/* Return values for rc_evaluate_lboard. */ +enum { + RC_LBOARD_STATE_INACTIVE, /* leaderboard is not being processed */ + RC_LBOARD_STATE_WAITING, /* leaderboard cannot activate until the start condition has been false for at least one frame */ + RC_LBOARD_STATE_ACTIVE, /* leaderboard is active and may start */ + RC_LBOARD_STATE_STARTED, /* leaderboard attempt in progress */ + RC_LBOARD_STATE_CANCELED, /* leaderboard attempt canceled */ + RC_LBOARD_STATE_TRIGGERED, /* leaderboard attempt complete, value should be submitted */ + RC_LBOARD_STATE_DISABLED /* leaderboard cannot be processed at this time */ +}; + +struct rc_lboard_t { + rc_trigger_t start; + rc_trigger_t submit; + rc_trigger_t cancel; + rc_value_t value; + rc_value_t* progress; + rc_memref_t* memrefs; + + uint8_t state; +}; + +RC_EXPORT int RC_CCONV rc_lboard_size(const char* memaddr); +RC_EXPORT rc_lboard_t* RC_CCONV rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +RC_EXPORT int RC_CCONV rc_evaluate_lboard(rc_lboard_t* lboard, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L); +RC_EXPORT void RC_CCONV rc_reset_lboard(rc_lboard_t* lboard); + +/*****************************************************************************\ +| Value formatting | +\*****************************************************************************/ + +/* Supported formats. */ +enum { + RC_FORMAT_FRAMES, + RC_FORMAT_SECONDS, + RC_FORMAT_CENTISECS, + RC_FORMAT_SCORE, + RC_FORMAT_VALUE, + RC_FORMAT_MINUTES, + RC_FORMAT_SECONDS_AS_MINUTES, + RC_FORMAT_FLOAT1, + RC_FORMAT_FLOAT2, + RC_FORMAT_FLOAT3, + RC_FORMAT_FLOAT4, + RC_FORMAT_FLOAT5, + RC_FORMAT_FLOAT6, + RC_FORMAT_FIXED1, + RC_FORMAT_FIXED2, + RC_FORMAT_FIXED3, + RC_FORMAT_TENS, + RC_FORMAT_HUNDREDS, + RC_FORMAT_THOUSANDS, + RC_FORMAT_UNSIGNED_VALUE +}; + +RC_EXPORT int RC_CCONV rc_parse_format(const char* format_str); +RC_EXPORT int RC_CCONV rc_format_value(char* buffer, int size, int32_t value, int format); + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +typedef struct rc_richpresence_lookup_item_t rc_richpresence_lookup_item_t; + +struct rc_richpresence_lookup_item_t { + uint32_t first; + uint32_t last; + rc_richpresence_lookup_item_t* left; + rc_richpresence_lookup_item_t* right; + const char* label; +}; + +typedef struct rc_richpresence_lookup_t rc_richpresence_lookup_t; + +struct rc_richpresence_lookup_t { + rc_richpresence_lookup_item_t* root; + rc_richpresence_lookup_t* next; + const char* name; + const char* default_label; + uint8_t format; +}; + +typedef struct rc_richpresence_display_part_t rc_richpresence_display_part_t; + +struct rc_richpresence_display_part_t { + rc_richpresence_display_part_t* next; + const char* text; + rc_richpresence_lookup_t* lookup; + rc_memref_value_t *value; + uint8_t display_type; +}; + +typedef struct rc_richpresence_display_t rc_richpresence_display_t; + +struct rc_richpresence_display_t { + rc_trigger_t trigger; + rc_richpresence_display_t* next; + rc_richpresence_display_part_t* display; +}; + +struct rc_richpresence_t { + rc_richpresence_display_t* first_display; + rc_richpresence_lookup_t* first_lookup; + rc_memref_t* memrefs; + rc_value_t* variables; +}; + +RC_EXPORT int RC_CCONV rc_richpresence_size(const char* script); +RC_EXPORT int RC_CCONV rc_richpresence_size_lines(const char* script, int* lines_read); +RC_EXPORT rc_richpresence_t* RC_CCONV rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx); +RC_EXPORT int RC_CCONV rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +RC_EXPORT void RC_CCONV rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L); +RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_reset_richpresence(rc_richpresence_t* self); + +RC_END_C_DECLS + +#endif /* RC_RUNTIME_TYPES_H */ diff --git a/3rdparty/rcheevos/include/rc_url.h b/3rdparty/rcheevos/include/rc_url.h new file mode 100644 index 0000000000..e3f06d698d --- /dev/null +++ b/3rdparty/rcheevos/include/rc_url.h @@ -0,0 +1,36 @@ +#ifndef RC_URL_H +#define RC_URL_H + +#include "rc_export.h" + +#include + +RC_BEGIN_C_DECLS + +RC_EXPORT int RC_CCONV rc_url_award_cheevo(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned cheevo_id, int hardcore, const char* game_hash); + +RC_EXPORT int RC_CCONV rc_url_submit_lboard(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned lboard_id, int value); + +RC_EXPORT int RC_CCONV rc_url_get_lboard_entries(char* buffer, size_t size, unsigned lboard_id, unsigned first_index, unsigned count); +RC_EXPORT int RC_CCONV rc_url_get_lboard_entries_near_user(char* buffer, size_t size, unsigned lboard_id, const char* user_name, unsigned count); + +RC_EXPORT int RC_CCONV rc_url_get_gameid(char* buffer, size_t size, const char* hash); + +RC_EXPORT int RC_CCONV rc_url_get_patch(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid); + +RC_EXPORT int RC_CCONV rc_url_get_badge_image(char* buffer, size_t size, const char* badge_name); + +RC_EXPORT int RC_CCONV rc_url_login_with_password(char* buffer, size_t size, const char* user_name, const char* password); + +RC_EXPORT int RC_CCONV rc_url_login_with_token(char* buffer, size_t size, const char* user_name, const char* login_token); + +RC_EXPORT int RC_CCONV rc_url_get_unlock_list(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid, int hardcore); + +RC_EXPORT int RC_CCONV rc_url_post_playing(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid); + +RC_EXPORT int RC_CCONV rc_url_ping(char* url_buffer, size_t url_buffer_size, char* post_buffer, size_t post_buffer_size, + const char* user_name, const char* login_token, unsigned gameid, const char* rich_presence); + +RC_END_C_DECLS + +#endif /* RC_URL_H */ diff --git a/3rdparty/rcheevos/include/rc_util.h b/3rdparty/rcheevos/include/rc_util.h new file mode 100644 index 0000000000..18d27e0fc7 --- /dev/null +++ b/3rdparty/rcheevos/include/rc_util.h @@ -0,0 +1,51 @@ +#ifndef RC_UTIL_H +#define RC_UTIL_H + +#include "rc_export.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/** + * 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]); + +RC_END_C_DECLS + +#endif /* RC_UTIL_H */ diff --git a/3rdparty/rcheevos/include/rcheevos.h b/3rdparty/rcheevos/include/rcheevos.h new file mode 100644 index 0000000000..0d96002148 --- /dev/null +++ b/3rdparty/rcheevos/include/rcheevos.h @@ -0,0 +1,8 @@ +#ifndef RCHEEVOS_H +#define RCHEEVOS_H + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "rc_consoles.h" + +#endif /* RCHEEVOS_H */ diff --git a/3rdparty/rcheevos/rcheevos b/3rdparty/rcheevos/rcheevos deleted file mode 160000 index 3cadf84c30..0000000000 --- a/3rdparty/rcheevos/rcheevos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3cadf84c30bbc050c0fec79d26e1c8ff504bda42 diff --git a/3rdparty/rcheevos/rcheevos.vcxproj b/3rdparty/rcheevos/rcheevos.vcxproj index 925225ce4c..e6361379e5 100644 --- a/3rdparty/rcheevos/rcheevos.vcxproj +++ b/3rdparty/rcheevos/rcheevos.vcxproj @@ -30,59 +30,59 @@ AllRules.ruleset - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + _CRT_NONSTDC_NO_WARNINGS;RC_DISABLE_LUA;RCHEEVOS_URL_SSL;%(PreprocessorDefinitions) - $(ProjectDir)rcheevos\include;%(AdditionalIncludeDirectories) + $(ProjectDir)include;%(AdditionalIncludeDirectories) TurnOffAllWarnings diff --git a/3rdparty/rcheevos/rcheevos.vcxproj.filters b/3rdparty/rcheevos/rcheevos.vcxproj.filters index 7a8c1f3552..511b9e59ba 100644 --- a/3rdparty/rcheevos/rcheevos.vcxproj.filters +++ b/3rdparty/rcheevos/rcheevos.vcxproj.filters @@ -18,130 +18,132 @@ - + rapi - + rapi - + rapi - + rapi - + rapi - + rhash - + rhash - + rurl - - - - + + + + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rcheevos - + rapi - + rhash - + include - + include - + include - + include - + include - + include - + include - + include - + include - + include - + include - + include - + include - - - - - + + + + include + + + rcheevos - + rcheevos diff --git a/3rdparty/rcheevos/src/rapi/rc_api_common.c b/3rdparty/rcheevos/src/rapi/rc_api_common.c new file mode 100644 index 0000000000..5835b28014 --- /dev/null +++ b/3rdparty/rcheevos/src/rapi/rc_api_common.c @@ -0,0 +1,1222 @@ +#include "rc_api_common.h" +#include "rc_api_request.h" +#include "rc_api_runtime.h" + +#include "../rc_compat.h" + +#include +#include +#include +#include + +#define RETROACHIEVEMENTS_HOST "https://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org" +#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org" +static char* g_host = NULL; +static char* g_imagehost = NULL; + +/* --- rc_json --- */ + +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* 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; + } + + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + int result; + + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; + + field->value_start = iterator->json; + + switch (*iterator->json) + { + case '"': /* quoted string */ + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; + break; + + case '-': + case '+': /* signed number */ + ++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 */ + 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(iterator, field); + if (result != RC_OK) + return result; + + break; + + case '{': /* object */ + 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)*iterator->json)) + return RC_INVALID_JSON; + + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; + break; + } + + field->value_end = iterator->json; + return RC_OK; +} + +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_field_t unused_field; + int result; + + if (!rc_json_match_char(iterator, '[')) + return RC_INVALID_JSON; + + field->array_size = 0; + + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; + + do + { + rc_json_skip_whitespace(iterator); + + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; + + ++field->array_size; + + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); + + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; + + return RC_OK; +} + +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, '"')) + return RC_INVALID_JSON; + + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) + return RC_INVALID_JSON; + ++iterator->json; + } + + if (iterator->json == iterator->end) + return RC_INVALID_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; + + rc_json_skip_whitespace(iterator); + + if (rc_json_parse_field(iterator, field) < 0) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); + + return RC_OK; +} + +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen) { + size_t i; + uint32_t num_fields = 0; + rc_json_field_t field; + int result; + + if (fields_seen) + *fields_seen = 0; + + for (i = 0; i < field_count; ++i) + fields[i].value_start = fields[i].value_end = NULL; + + if (!rc_json_match_char(iterator, '{')) + return RC_INVALID_JSON; + + if (rc_json_match_char(iterator, '}')) /* empty object */ + return RC_OK; + + do + { + 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 && 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; + + } while (rc_json_match_char(iterator, ',')); + + if (!rc_json_match_char(iterator, '}')) + return RC_INVALID_JSON; + + if (fields_seen) + *fields_seen = num_fields; + + return RC_OK; +} + +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; + + 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); +} + +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; + if (strcmp(fields[0].name, "Success") != 0) + return RC_INVALID_STATE; + if (strcmp(fields[1].name, "Error") != 0) + return RC_INVALID_STATE; +#endif + + response->error_message = NULL; + + if (!server_response) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + 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; + } + + 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; + + 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; + } + + 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) { + const char* not_found = " not found in response"; + const size_t not_found_len = strlen(not_found); + const size_t field_len = strlen(field->name); + + uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1); + if (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_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write); + } + + response->succeeded = 0; + return 0; +} + +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) { + rc_json_iterator_t iterator; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!field->value_start) + return rc_json_missing_field(response, field); + + 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_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) + return 0; + + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); + + return 1; +} + +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; + uint32_t* entry; + + 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 = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t)); + 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)) + return RC_MISSING_VALUE; + + ++entry; + } + } + else { + *entries = NULL; + } + + return RC_OK; +} + +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, 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, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!field->value_start || *field->value_start != '[') { + *num_entries = 0; + return 0; + } + + 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_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) + return 0; + + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); + + return 1; +} + +static uint32_t rc_json_decode_hex4(const char* input) { + char hex[5]; + + memcpy(hex, input, 4); + hex[4] = '\0'; + + return (uint32_t)strtoul(hex, NULL, 16); +} + +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; + } + + if (ucs32_char < 0x0800) { + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xC0 | (ucs32_char & 0x1F); + return 2; + } + + if (ucs32_char < 0x010000) { + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xE0 | (ucs32_char & 0x0F); + return 3; + } + + if (ucs32_char < 0x200000) { + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF0 | (ucs32_char & 0x07); + return 4; + } + + if (ucs32_char < 0x04000000) { + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF8 | (ucs32_char & 0x03); + return 5; + } + + dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xFC | (ucs32_char & 0x01); + return 6; +} + +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; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = NULL; + return 0; + } + + if (len == 4 && memcmp(field->value_start, "null", 4) == 0) { + *out = NULL; + return 1; + } + + if (*src == '\"') { + ++src; + + if (*src == '\"') { + /* simple optimization for empty string - don't allocate space */ + *out = ""; + return 1; + } + + *out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ + + do { + if (*src == '\\') { + ++src; + if (*src == 'n') { + /* newline */ + ++src; + *dst++ = '\n'; + continue; + } + + if (*src == 'r') { + /* carriage return */ + ++src; + *dst++ = '\r'; + continue; + } + + if (*src == 'u') { + /* unicode character */ + 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 uint32_t surrogate = rc_json_decode_hex4(src + 2); + src += 6; + + if (surrogate >= 0xDC00 && surrogate < 0xE000) { + /* found a surrogate tail, merge them */ + ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000; + } + } + + if (!(ucs32_char & 0xFFFF0000)) { + /* invalid surrogate pair, fallback to replacement char */ + ucs32_char = 0xFFFD; + } + } + + dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char); + continue; + } + + if (*src == 't') { + /* tab */ + ++src; + *dst++ = '\t'; + continue; + } + + /* just an escaped character, fallthrough to normal copy */ + } + + *dst++ = *src++; + } while (*src != '\"'); + + } else { + *out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */ + memcpy(dst, src, len); + dst += len; + } + + *dst++ = '\0'; + rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst); + return 1; +} + +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) { + if (!rc_json_get_string(out, &response->buffer, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_string(out, &response->buffer, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + int32_t value = 0; + int negative = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + /* assert: string contains only numerals and an optional sign per rc_json_parse_field */ + if (*src == '-') { + negative = 1; + ++src; + } else if (*src == '+') { + ++src; + } else if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + if (negative) + *out = -value; + else + *out = value; + + return 1; +} + +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(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(uint32_t* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + uint32_t value = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + /* assert: string contains only numerals per rc_json_parse_field */ + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + *out = value; + return 1; +} + +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(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; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name) { + int32_t whole, fraction, fraction_denominator; + const char* decimal = field->value_start; + + if (!decimal) { + *out = 0.0f; + return 0; + } + + if (!rc_json_get_num(&whole, field, field_name)) + return 0; + + while (decimal < field->value_end && *decimal != '.') + ++decimal; + + fraction = 0; + fraction_denominator = 1; + if (decimal) { + ++decimal; + while (decimal < field->value_end && *decimal >= '0' && *decimal <= '9') { + fraction *= 10; + fraction += *decimal - '0'; + fraction_denominator *= 10; + ++decimal; + } + } + + if (whole < 0) + fraction = -fraction; + + *out = (float)whole + ((float)fraction / (float)fraction_denominator); + return 1; +} + +void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value) { + if (!rc_json_get_float(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_float(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) { + struct tm tm; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (*field->value_start == '\"') { + memset(&tm, 0, sizeof(tm)); + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { + tm.tm_mon--; /* 0-based */ + tm.tm_year -= 1900; /* 1900 based */ + + /* mktime converts a struct tm to a time_t using the local timezone. + * the input string is UTC. since timegm is not universally cross-platform, + * figure out the offset between UTC and local time by applying the + * timezone conversion twice and manually removing the difference */ + { + time_t local_timet = mktime(&tm); + time_t skewed_timet, tz_offset; + struct tm gmt_tm; + gmtime_s(&gmt_tm, &local_timet); + skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */ + tz_offset = skewed_timet - local_timet; + *out = local_timet - tz_offset; + } + + return 1; + } + } + + *out = 0; + return 0; +} + +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_datetime(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (src) { + const size_t len = field->value_end - field->value_start; + if (len == 4 && strncasecmp(src, "true", 4) == 0) { + *out = 1; + return 1; + } else if (len == 5 && strncasecmp(src, "false", 5) == 0) { + *out = 0; + return 1; + } else if (len == 1) { + *out = (*src != '0'); + return 1; + } + } + + *out = 0; + return 0; +} + +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_bool(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_bool(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +/* --- rc_api_request --- */ + +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_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 = (char*)rc_buffer_reserve(buffer, estimated_size); + + used_buffer = &buffer->chunk; + while (used_buffer && used_buffer->write != (uint8_t*)builder->write) + used_buffer = used_buffer->next; + + builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size; +} + +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { + rc_url_builder_append(builder, "", 1); + + if (builder->result != RC_OK) + return NULL; + + rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write); + return builder->start; +} + +static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) { + if (builder->result == RC_OK) { + size_t remaining = builder->end - builder->write; + 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_buffer_chunk_t); + char* new_start; + size_t new_size = (current_size < 256) ? 256 : current_size * 2; + do { + remaining = new_size - used; + if (remaining >= amount) + break; + + new_size *= 2; + } while (1); + + /* 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 = (char*)rc_buffer_reserve(builder->buffer, new_size); + if (!new_start) { + builder->result = RC_OUT_OF_MEMORY; + return RC_OUT_OF_MEMORY; + } + + if (new_start != builder->start) { + memcpy(new_start, builder->start, used); + builder->start = new_start; + builder->write = new_start + used; + } + + builder->end = builder->start + new_size; + } + } + + return builder->result; +} + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) { + static const char hex[] = "0123456789abcdef"; + const char* start = str; + size_t len = 0; + for (;;) { + const char c = *str++; + switch (c) { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + case '-': case '_': case '.': case '~': + len++; + continue; + + case '\0': + if (len) + rc_url_builder_append(builder, start, len); + + return; + + default: + if (rc_url_builder_reserve(builder, len + 3) != RC_OK) + return; + + if (len) { + memcpy(builder->write, start, len); + builder->write += len; + } + + if (c == ' ') { + *builder->write++ = '+'; + } else { + *builder->write++ = '%'; + *builder->write++ = hex[((unsigned char)c) >> 4]; + *builder->write++ = hex[c & 0x0F]; + } + break; + } + + start = str; + len = 0; + } +} + +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) { + if (rc_url_builder_reserve(builder, len) == RC_OK) { + memcpy(builder->write, data, len); + builder->write += len; + } +} + +static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) { + size_t param_len = strlen(param); + + if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) { + if (builder->write > builder->start) { + if (builder->write[-1] != '?') + *builder->write++ = '&'; + } + + memcpy(builder->write, param, param_len); + builder->write += param_len; + *builder->write++ = '='; + } + + return builder->result; +} + +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); + rc_url_builder_append(builder, num, chars); + } +} + +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); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) { + rc_url_builder_append_param_equals(builder, param); + rc_url_builder_append_encoded_str(builder, value); +} + +void rc_api_url_build_dorequest_url(rc_api_request_t* request) { + #define DOREQUEST_ENDPOINT "/dorequest.php" + rc_buffer_init(&request->buffer); + + if (!g_host) { + request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; + } + else { + 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; + 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_buffer_consume(&request->buffer, url, url + url_len); + + request->url = (char*)url; + } + #undef DOREQUEST_ENDPOINT +} + +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) { + if (!username || !*username || !api_token || !*api_token) { + builder->result = RC_INVALID_STATE; + return 0; + } + + rc_url_builder_append_str_param(builder, "r", api); + rc_url_builder_append_str_param(builder, "u", username); + rc_url_builder_append_str_param(builder, "t", api_token); + + return (builder->result == RC_OK); +} + +/* --- Set Host --- */ + +static void rc_api_update_host(char** host, const char* hostname) { + if (*host != NULL) + free(*host); + + if (hostname != NULL) { + if (strstr(hostname, "://")) { + *host = strdup(hostname); + } + else { + const size_t hostname_len = strlen(hostname); + if (hostname_len == 0) { + *host = NULL; + } + else { + char* newhost = (char*)malloc(hostname_len + 7 + 1); + if (newhost) { + memcpy(newhost, "http://", 7); + memcpy(&newhost[7], hostname, hostname_len + 1); + *host = newhost; + } + else { + *host = NULL; + } + } + } + } + else { + *host = NULL; + } +} + +void rc_api_set_host(const char* hostname) { + rc_api_update_host(&g_host, hostname); + + if (!hostname) { + /* also clear out the image hostname */ + rc_api_set_image_host(NULL); + } + else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if just pointing at the non-HTTPS host, explicitly use the default image host + * so it doesn't try to use the web host directly */ + rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL); + } +} + +void rc_api_set_image_host(const char* hostname) { + rc_api_update_host(&g_imagehost, hostname); +} + +/* --- Fetch Image --- */ + +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_buffer_init(&request->buffer); + rc_url_builder_init(&builder, &request->buffer, 64); + + if (g_imagehost) { + rc_url_builder_append(&builder, g_imagehost, strlen(g_imagehost)); + } + else if (g_host) { + rc_url_builder_append(&builder, g_host, strlen(g_host)); + } + else { + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); + } + + switch (api_params->image_type) + { + case RC_IMAGE_TYPE_GAME: + rc_url_builder_append(&builder, "/Images/", 8); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, "_lock.png", 9); + break; + + case RC_IMAGE_TYPE_USER: + rc_url_builder_append(&builder, "/UserPic/", 9); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + default: + return RC_INVALID_STATE; + } + + request->url = rc_url_builder_finalize(&builder); + request->post_data = NULL; + + return builder.result; +} diff --git a/3rdparty/rcheevos/src/rapi/rc_api_common.h b/3rdparty/rcheevos/src/rapi/rc_api_common.h new file mode 100644 index 0000000000..7311cfff55 --- /dev/null +++ b/3rdparty/rcheevos/src/rapi/rc_api_common.h @@ -0,0 +1,80 @@ +#ifndef RC_API_COMMON_H +#define RC_API_COMMON_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +#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_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_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) {NULL,NULL,n,sizeof(n)-1,0} + +typedef struct rc_json_field_t { + const char* value_start; + const char* value_end; + const char* name; + size_t name_len; + uint32_t array_size; +} +rc_json_field_t; + +typedef struct rc_json_iterator_t { + const char* json; + const char* end; +} +rc_json_iterator_t; + +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_float(float* 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(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_float(float* out, const rc_json_field_t* field, const char* field_name, float 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, 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(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_float(float* 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(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, 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); + +RC_END_C_DECLS + +#endif /* RC_API_COMMON_H */ diff --git a/3rdparty/rcheevos/src/rapi/rc_api_editor.c b/3rdparty/rcheevos/src/rapi/rc_api_editor.c new file mode 100644 index 0000000000..8cf6369f11 --- /dev/null +++ b/3rdparty/rcheevos/src/rapi/rc_api_editor.c @@ -0,0 +1,529 @@ +#include "rc_api_editor.h" +#include "rc_api_common.h" +#include "rc_api_runtime.h" + +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +/* --- Fetch Code Notes --- */ + +int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "codenotes2"); + 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_code_notes_response(rc_api_fetch_code_notes_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_code_notes_server_response(response, &response_obj); +} + +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; + rc_api_code_note_t* note; + const char* address_str; + const char* last_author = ""; + size_t last_author_len = 0; + size_t len; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("CodeNotes") + }; + + rc_json_field_t note_fields[] = { + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Note") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_array(&response->num_notes, &array_field, &response->response, &fields[2], "CodeNotes")) + return RC_MISSING_VALUE; + + if (response->num_notes) { + response->notes = (rc_api_code_note_t*)rc_buffer_alloc(&response->response.buffer, response->num_notes * sizeof(rc_api_code_note_t)); + if (!response->notes) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + note = response->notes; + while (rc_json_get_array_entry_object(note_fields, sizeof(note_fields) / sizeof(note_fields[0]), &iterator)) { + /* an empty note represents a record that was deleted on the server */ + /* a note set to '' also represents a deleted note (remnant of a bug) */ + /* NOTE: len will include the quotes */ + if (note_fields[2].value_start) { + len = note_fields[2].value_end - note_fields[2].value_start; + if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { + --response->num_notes; + continue; + } + } + + if (!rc_json_get_required_string(&address_str, &response->response, ¬e_fields[0], "Address")) + return RC_MISSING_VALUE; + note->address = (unsigned)strtol(address_str, NULL, 16); + if (!rc_json_get_required_string(¬e->note, &response->response, ¬e_fields[2], "Note")) + return RC_MISSING_VALUE; + + len = note_fields[1].value_end - note_fields[1].value_start; + if (len == last_author_len && memcmp(note_fields[1].value_start, last_author, len) == 0) { + note->author = last_author; + } + else { + if (!rc_json_get_required_string(¬e->author, &response->response, ¬e_fields[1], "User")) + return RC_MISSING_VALUE; + + last_author = note->author; + last_author_len = len; + } + + ++note; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Code Note --- */ + +int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitcodenote", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "m", api_params->address); + + if (api_params->note && *api_params->note) + rc_url_builder_append_str_param(&builder, "n", api_params->note); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_code_note_response(rc_api_update_code_note_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_update_code_note_server_response(response, &response_obj); +} + +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_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") + /* unused fields + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("Note") + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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; + + return RC_OK; +} + +void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Achievement --- */ + +static const char* rc_type_string(uint32_t type) { + switch (type) { + case RC_ACHIEVEMENT_TYPE_MISSABLE: return "missable"; + case RC_ACHIEVEMENT_TYPE_PROGRESSION: return "progression"; + case RC_ACHIEVEMENT_TYPE_WIN: return "win_condition"; + default: return ""; + } +} + +int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0 || api_params->category == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description || !*api_params->description) + return RC_INVALID_STATE; + if (!api_params->trigger || !*api_params->trigger) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadachievement", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->achievement_id) + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "m", api_params->trigger); + rc_url_builder_append_unum_param(&builder, "z", api_params->points); + rc_url_builder_append_unum_param(&builder, "f", api_params->category); + if (api_params->badge) + rc_url_builder_append_str_param(&builder, "b", api_params->badge); + rc_url_builder_append_str_param(&builder, "x", rc_type_string(api_params->type)); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->trigger, (int)strlen(api_params->trigger)); + snprintf(buffer, sizeof(buffer), "%u", api_params->points); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + snprintf(buffer, sizeof(buffer), "%u", api_params->points * 3); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, hash); + rc_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_achievement_response(rc_api_update_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_update_achievement_server_response(response, &response_obj); +} + +int rc_api_process_update_achievement_server_response(rc_api_update_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("AchievementID") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_unum(&response->achievement_id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Leaderboard --- */ + +int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description) + return RC_INVALID_STATE; + if (!api_params->start_trigger || !*api_params->start_trigger) + return RC_INVALID_STATE; + if (!api_params->submit_trigger || !*api_params->submit_trigger) + return RC_INVALID_STATE; + if (!api_params->cancel_trigger || !*api_params->cancel_trigger) + return RC_INVALID_STATE; + if (!api_params->value_definition || !*api_params->value_definition) + return RC_INVALID_STATE; + if (!api_params->format || !*api_params->format) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadleaderboard", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->leaderboard_id) + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "s", api_params->start_trigger); + rc_url_builder_append_str_param(&builder, "b", api_params->submit_trigger); + rc_url_builder_append_str_param(&builder, "c", api_params->cancel_trigger); + rc_url_builder_append_str_param(&builder, "l", api_params->value_definition); + rc_url_builder_append_num_param(&builder, "w", api_params->lower_is_better); + rc_url_builder_append_str_param(&builder, "f", api_params->format); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->start_trigger, (int)strlen(api_params->start_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->submit_trigger, (int)strlen(api_params->submit_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->cancel_trigger, (int)strlen(api_params->cancel_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->value_definition, (int)strlen(api_params->value_definition)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + md5_append(&md5, (md5_byte_t*)api_params->format, (int)strlen(api_params->format)); + md5_finish(&md5, hash); + rc_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_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_update_leaderboard_server_response(response, &response_obj); +} + +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_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("LeaderboardID") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_unum(&response->leaderboard_id, &response->response, &fields[2], "LeaderboardID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Badge Range --- */ + +int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "badgeiter"); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + (void)api_params; + + return builder.result; +} + +int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_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_badge_range_server_response(response, &response_obj); +} + +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_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("FirstBadge"), + RC_JSON_NEW_FIELD("NextBadge") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_unum(&response->first_badge_id, &response->response, &fields[2], "FirstBadge")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->next_badge_id, &response->response, &fields[3], "NextBadge")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Add Game Hash --- */ + +int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + if (!api_params->hash || !*api_params->hash) + return RC_INVALID_STATE; + if (api_params->game_id == 0 && (!api_params->title || !*api_params->title)) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitgametitle", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + rc_url_builder_append_str_param(&builder, "m", api_params->hash); + if (api_params->title) + rc_url_builder_append_str_param(&builder, "i", api_params->title); + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + if (api_params->hash_description && *api_params->hash_description) + rc_url_builder_append_str_param(&builder, "d", api_params->hash_description); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_add_game_hash_response(rc_api_add_game_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_add_game_hash_server_response(response, &response_obj); +} + +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_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("Response") + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("GameID") + /* unused fields + RC_JSON_NEW_FIELD("MD5"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("Success") + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[0], "GameID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} diff --git a/3rdparty/rcheevos/src/rapi/rc_api_info.c b/3rdparty/rcheevos/src/rapi/rc_api_info.c new file mode 100644 index 0000000000..2b9f882625 --- /dev/null +++ b/3rdparty/rcheevos/src/rapi/rc_api_info.c @@ -0,0 +1,373 @@ +#include "rc_api_info.h" +#include "rc_api_common.h" + +#include "rc_runtime_types.h" + +#include +#include + +/* --- Fetch Achievement Info --- */ + +int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementwondata", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + + if (api_params->friends_only) + rc_url_builder_append_unum_param(&builder, "f", 1); + if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + 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 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("AchievementID"), + RC_JSON_NEW_FIELD("Response") + /* unused fields + RC_JSON_NEW_FIELD("Offset"), + RC_JSON_NEW_FIELD("Count"), + RC_JSON_NEW_FIELD("FriendsOnly") + * unused fields */ + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("NumEarned"), + RC_JSON_NEW_FIELD("TotalPlayers"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("RecentWinner") /* array */ + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("DateAwarded") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[3], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->num_awarded, &response->response, &response_fields[0], "NumEarned")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->num_players, &response->response, &response_fields[1], "TotalPlayers")) + return RC_MISSING_VALUE; + 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, &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_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")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[1], "DateAwarded")) + return RC_MISSING_VALUE; + entry->awarded = (time_t)timet; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Leaderboard Info --- */ + +int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "lbinfo"); + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + + if (api_params->username) + rc_url_builder_append_str_param(&builder, "u", api_params->username); + else if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + + 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 array_field; + rc_json_iterator_t iterator; + uint32_t timet; + int result; + size_t len; + char format[16]; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardData") + }; + + rc_json_field_t leaderboarddata_fields[] = { + RC_JSON_NEW_FIELD("LBID"), + RC_JSON_NEW_FIELD("LBFormat"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("LBTitle"), + RC_JSON_NEW_FIELD("LBDesc"), + RC_JSON_NEW_FIELD("LBMem"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("LBAuthor"), + RC_JSON_NEW_FIELD("LBCreated"), + RC_JSON_NEW_FIELD("LBUpdated"), + RC_JSON_NEW_FIELD("Entries") /* array */ + /* unused fields + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ConsoleName"), + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("GameIcon") + * unused fields */ + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Index"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("DateSubmitted") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_object(leaderboarddata_fields, sizeof(leaderboarddata_fields) / sizeof(leaderboarddata_fields[0]), &response->response, &fields[2], "LeaderboardData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &leaderboarddata_fields[0], "LBID")) + return RC_MISSING_VALUE; + 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; + if (!rc_json_get_required_string(&response->description, &response->response, &leaderboarddata_fields[4], "LBDesc")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->definition, &response->response, &leaderboarddata_fields[5], "LBMem")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &leaderboarddata_fields[6], "GameID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->author, &response->response, &leaderboarddata_fields[7], "LBAuthor")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->created, &response->response, &leaderboarddata_fields[8], "LBCreated")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->updated, &response->response, &leaderboarddata_fields[9], "LBUpdated")) + return RC_MISSING_VALUE; + + if (!leaderboarddata_fields[1].value_end) + return RC_MISSING_VALUE; + len = leaderboarddata_fields[1].value_end - leaderboarddata_fields[1].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboarddata_fields[1].value_start + 1, len); + format[len] = '\0'; + response->format = rc_parse_format(format); + } + else { + response->format = RC_FORMAT_VALUE; + } + + 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_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")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->index, &response->response, &entry_fields[2], "Index")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[3], "Score")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[4], "DateSubmitted")) + return RC_MISSING_VALUE; + entry->submitted = (time_t)timet; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Games List --- */ + +int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameslist"); + 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_iterator_t iterator; + rc_json_field_t field; + int result; + char* end; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + 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_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, &field)) { + entry->id = strtol(field.name, &end, 10); + + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) + return RC_MISSING_VALUE; + + ++entry; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} diff --git a/3rdparty/rcheevos/src/rapi/rc_api_runtime.c b/3rdparty/rcheevos/src/rapi/rc_api_runtime.c new file mode 100644 index 0000000000..4f3bc5b57c --- /dev/null +++ b/3rdparty/rcheevos/src/rapi/rc_api_runtime.c @@ -0,0 +1,648 @@ +#include "rc_api_runtime.h" +#include "rc_api_common.h" + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include +#include + +/* --- Resolve Hash --- */ + +int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (!api_params->game_hash || !*api_params->game_hash) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + 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"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("GameID") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID"); + return RC_OK; +} + +void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Game Data --- */ + +int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + 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); +} + +static int rc_parse_achievement_type(const char* type) +{ + if (strcmp(type, "missable") == 0) + return RC_ACHIEVEMENT_TYPE_MISSABLE; + + if (strcmp(type, "win_condition") == 0) + return RC_ACHIEVEMENT_TYPE_WIN; + + if (strcmp(type, "progression") == 0) + return RC_ACHIEVEMENT_TYPE_PROGRESSION; + + return RC_ACHIEVEMENT_TYPE_STANDARD; +} + +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 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; + uint32_t timet; + int result; + char format[16]; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("PatchData") /* nested object */ + }; + + rc_json_field_t patchdata_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ + }; + + rc_json_field_t achievement_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Flags"), + RC_JSON_NEW_FIELD("Points"), + RC_JSON_NEW_FIELD("MemAddr"), + RC_JSON_NEW_FIELD("Author"), + RC_JSON_NEW_FIELD("BadgeName"), + RC_JSON_NEW_FIELD("Created"), + RC_JSON_NEW_FIELD("Modified"), + RC_JSON_NEW_FIELD("Type"), + RC_JSON_NEW_FIELD("Rarity"), + RC_JSON_NEW_FIELD("RarityHardcore") + }; + + rc_json_field_t leaderboard_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Mem"), + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("Hidden") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[2], "PatchData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID")) + return RC_MISSING_VALUE; + + /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ + if (patchdata_fields[3].value_end) { + str = patchdata_fields[3].value_end - 5; + if (memcmp(str, ".png\"", 5) == 0) { + patchdata_fields[3].value_end -= 5; + + while (str > patchdata_fields[3].value_start && str[-1] != '/') + --str; + + patchdata_fields[3].value_start = str; + } + } + rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", ""); + + /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards. + determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) + and add space for the structures. */ + len = patchdata_fields[4].value_end - patchdata_fields[4].value_start; /* rich presence */ + + len += (patchdata_fields[5].value_end - patchdata_fields[5].value_start) - /* achievements */ + patchdata_fields[5].array_size * (130 - sizeof(rc_api_achievement_definition_t)); + + 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_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, &array_field, &response->response, &patchdata_fields[5], "Achievements")) + return RC_MISSING_VALUE; + + if (response->num_achievements) { + 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")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName")) + return RC_MISSING_VALUE; + + len = achievement_fields[6].value_end - achievement_fields[6].value_start; + if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { + achievement->author = last_author; + } + else { + if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author")) + return RC_MISSING_VALUE; + + last_author = achievement->author; + last_author_field = achievement_fields[6].value_start; + last_author_len = len; + } + + if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created")) + return RC_MISSING_VALUE; + achievement->created = (time_t)timet; + if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified")) + return RC_MISSING_VALUE; + achievement->updated = (time_t)timet; + + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; + if (achievement_fields[10].value_end) { + len = achievement_fields[10].value_end - achievement_fields[10].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, achievement_fields[10].value_start + 1, len); + format[len] = '\0'; + achievement->type = rc_parse_achievement_type(format); + } + } + + /* legacy support : if title contains[m], change type to missable and remove[m] from title */ + if (memcmp(achievement->title, "[m]", 3) == 0) { + len = 3; + while (achievement->title[len] == ' ') + ++len; + achievement->title += len; + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + } + else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) { + len = strlen(achievement->title) - 3; + while (achievement->title[len - 1] == ' ') + --len; + ((char*)achievement->title)[len] = '\0'; + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + } + + rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0); + rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0); + + ++achievement; + } + } + + 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_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")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description")) + 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(&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; + len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboard_fields[4].value_start + 1, len); + format[len] = '\0'; + leaderboard->format = rc_parse_format(format); + } + else { + leaderboard->format = RC_FORMAT_VALUE; + } + + ++leaderboard; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Ping --- */ + +int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + if (api_params->rich_presence && *api_params->rich_presence) + rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); + + if (api_params->game_hash && *api_params->game_hash) { + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore); + rc_url_builder_append_str_param(&builder, "x", 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_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_buffer_init(&response->response.buffer); + + 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_buffer_destroy(&response->response.buffer); +} + +/* --- Award Achievement --- */ + +int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t digest[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 96); + if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); + if (api_params->game_hash && *api_params->game_hash) + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + /* Evaluate the signature. */ + md5_init(&md5); + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + 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_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_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!response->response.succeeded) { + if (response->response.error_message && + memcmp(response->response.error_message, "User already has", 16) == 0) { + /* not really an error, the achievement is unlocked, just not by the current call. + * hardcore: User already has hardcore and regular achievements awarded. + * non-hardcore: User already has this achievement awarded. + */ + response->response.succeeded = 1; + } else { + return result; + } + } + + rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); + 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_buffer_destroy(&response->response.buffer); +} + +/* --- Submit Leaderboard Entry --- */ + +int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t digest[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 96); + if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_num_param(&builder, "s", api_params->score); + + if (api_params->game_hash && *api_params->game_hash) + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + /* Evaluate the signature. */ + md5_init(&md5); + snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + snprintf(buffer, sizeof(buffer), "%d", api_params->score); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, 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 array_field; + rc_json_iterator_t iterator; + const char* str; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") /* nested object */ + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("BestScore"), + RC_JSON_NEW_FIELD("RankInfo"), /* nested object */ + RC_JSON_NEW_FIELD("TopEntries") /* array */ + /* unused fields + RC_JSON_NEW_FIELD("LBData"), / * array * / + RC_JSON_NEW_FIELD("ScoreFormatted"), + RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * / + * unused fields */ + }; + + /* unused fields + rc_json_field_t lbdata_fields[] = { + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LeaderboardID"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("LowerIsBetter") + }; + * unused fields */ + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Score") + /* unused fields + RC_JSON_NEW_FIELD("DateSubmitted") + * unused fields */ + }; + + rc_json_field_t rank_info_fields[] = { + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("NumEntries") + /* unused fields + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("UserRank") + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries")) + return RC_MISSING_VALUE; + response->num_entries = (unsigned)atoi(str); + + 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_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")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} diff --git a/3rdparty/rcheevos/src/rapi/rc_api_user.c b/3rdparty/rcheevos/src/rapi/rc_api_user.c new file mode 100644 index 0000000000..0349f4a584 --- /dev/null +++ b/3rdparty/rcheevos/src/rapi/rc_api_user.c @@ -0,0 +1,254 @@ +#include "rc_api_user.h" +#include "rc_api_common.h" + +#include "../rc_version.h" + +#include + +/* --- Login --- */ + +int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (!api_params->username || !*api_params->username) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + 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]) + rc_url_builder_append_str_param(&builder, "p", api_params->password); + else if (api_params->api_token && api_params->api_token[0]) + rc_url_builder_append_str_param(&builder, "t", api_params->api_token); + else + 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_buffer_init(&response->response.buffer); + + 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[3], "User")) + return RC_MISSING_VALUE; + 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[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[8], "DisplayName", response->username); + + return RC_OK; +} + +void rc_api_destroy_login_response(rc_api_login_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Start Session --- */ + +int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + 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); + + if (api_params->game_hash && *api_params->game_hash) { + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore); + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + } + + 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("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_buffer_init(&response->response.buffer); + + 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, &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, &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_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch User Unlocks --- */ + +int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "unlocks", api_params->username, api_params->api_token)) { + 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"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("UserUnlocks") + /* unused fields + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("HardcoreMode") + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + 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; + + result = rc_json_get_required_unum_array(&response->achievement_ids, &response->num_achievement_ids, &response->response, &fields[2], "UserUnlocks"); + return result; +} + +void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} diff --git a/3rdparty/rcheevos/src/rc_client.c b/3rdparty/rcheevos/src/rc_client.c new file mode 100644 index 0000000000..6790e1768d --- /dev/null +++ b/3rdparty/rcheevos/src/rc_client.c @@ -0,0 +1,5635 @@ +#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 "rc_version.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 */ + +#define RC_MINIMUM_UNPAUSED_FRAMES 20 +#define RC_PAUSE_DECAY_MULTIPLIER 4 + +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 int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id); +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) +{ + (void)event; + (void)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->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + + 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); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->destroy) + client->state.external_client->destroy(); +#endif + + 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->enable_logging) + client->state.external_client->enable_logging(client, level, callback); +#endif +} + +/* ===== Common ===== */ + +static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec now; + (void)client; + + 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; + + (void)client; + + /* 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(); + + (void)client; + + 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_get_time_millisecs) + client->state.external_client->set_get_time_millisecs(client, handler); +#endif +} + +int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted; + + rc_mutex_lock(&client->state.mutex); + aborted = async_handle->aborted; + rc_mutex_unlock(&client->state.mutex); + + return aborted; +} + +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) { +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->abort_async) { + client->state.external_client->abort_async(async_handle); + return; + } +#endif + + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = RC_CLIENT_ASYNC_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } +} + +static int rc_client_async_handle_valid(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int valid = 0; + size_t i; + + /* there is a small window of opportunity where the client could have been destroyed before calling + * this function, but this function assumes the possibility that the handle has been destroyed, so + * we can't check it for RC_CLIENT_ASYNC_DESTROYED before attempting to scan the client data */ + 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) { + valid = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return valid; +} + +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; + } + + (void)http_status_code; + + 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 525: /* 525 SSL Handshake Failed */ + /* web server worker connection pool is exhausted */ + 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); + + /* if the user state has changed, the async operation completed synchronously */ + rc_mutex_lock(&client->state.mutex); + if (client->state.user != RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + callback_data = NULL; + rc_mutex_unlock(&client->state.mutex); + + return callback_data ? &callback_data->async_handle : NULL; +} + +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; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_login_with_password) + return client->state.external_client->begin_login_with_password(client, username, password, callback, callback_userdata); +#endif + + 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; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_login_with_token) + return client->state.external_client->begin_login_with_token(client, username, token, callback, callback_userdata); +#endif + + 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->logout) { + client->state.external_client->logout(); + return; + } +#endif + + 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) +{ + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_user_info) + return client->state.external_client->get_user_info(); +#endif + + return (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) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_user_game_summary) { + client->state.external_client->get_user_game_summary(summary); + return; + } +#endif + + if (!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 (!leaderboard->lboard) + 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; + + 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; + const uint8_t leaderboards_allowed = + client->state.hardcore || client->state.allow_leaderboards_in_softcore; + + 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 (leaderboards_allowed) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (leaderboards_allowed) + ++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; + start_session_params.game_hash = load_state->hash->hash; + start_session_params.hardcore = client->state.hardcore; + + 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; + achievement->public_.rarity = read->rarity; + achievement->public_.rarity_hardcore = read->rarity_hardcore; + achievement->public_.type = read->type; /* assert: mapping is 1:1 */ + + 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: + case RC_FORMAT_FIXED1: + case RC_FORMAT_FIXED2: + case RC_FORMAT_FIXED3: + case RC_FORMAT_TENS: + case RC_FORMAT_HUNDREDS: + case RC_FORMAT_THOUSANDS: + case RC_FORMAT_UNSIGNED_VALUE: + 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; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_load_game) + return client->state.external_client->begin_load_game(client, hash, callback, callback_userdata); +#endif + + 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; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_identify_and_load_game) + return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata); +#endif + + 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->unload_game) { + client->state.external_client->unload_game(); + return; + } +#endif + + 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; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata); +#endif + + 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_client_async_handle_t* async_handle; + 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; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + /* if handle is no longer valid, the async operation completed synchronously */ + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; + } +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_game_info) + return client->state.external_client->get_game_info(); +#endif + + return 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 ===== */ + +rc_client_async_handle_t* 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 NULL; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_load_subset) + return client->state.external_client->begin_load_subset(client, subset_id, callback, callback_userdata); +#endif + + if (!client->game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return NULL; + } + + 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 NULL; + } + + 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); + + return (client->state.load == load_state) ? &load_state->async_handle : NULL; +} + +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) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_subset_info) + return client->state.external_client->get_subset_info(subset_id); +#endif + + if (!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 */ + if (achievement->public_.unlock_time >= recent_unlock_time) { + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + } else { + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + if (client->state.disconnect && rc_client_is_award_achievement_pending(client, achievement->public_.id)) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED; + } + } + 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; + } + } + + 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"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: return "Unlocks Not Synced to Server"; + 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: + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: + 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_info_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS]; + 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, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED, + }; + 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) + return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t)); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->create_achievement_list) + return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping); +#endif + + if (!client->game) + return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_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_info_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + bucket_ptr = list->public_.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->destroy_func = NULL; + list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets); + return &list->public_; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + rc_client_achievement_list_info_t* info = (rc_client_achievement_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + free(list); +} + +int rc_client_has_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->has_achievements) + return client->state.external_client->has_achievements(); +#endif + + if (!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) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_achievement_info) + return client->state.external_client->get_achievement_info(id); +#endif + + if (!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 int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id) +{ + /* assume lock already held */ + rc_client_scheduled_callback_data_t* scheduled_callback = client->state.scheduled_callbacks; + for (; scheduled_callback; scheduled_callback = scheduled_callback->next) + { + if (scheduled_callback->callback == rc_client_award_achievement_retry) + { + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)scheduled_callback->data; + if (ach_data->id == achievement_id) + return 1; + } + } + + return 0; +} + +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; + + (void)client; + (void)now; + + 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) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_leaderboard_info) + return client->state.external_client->get_leaderboard_info(id); +#endif + + if (!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_info_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) + return calloc(1, sizeof(rc_client_leaderboard_list_t)); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->create_leaderboard_list) + return (rc_client_leaderboard_list_t*)client->state.external_client->create_leaderboard_list(grouping); +#endif + + if (!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_info_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + bucket_ptr = list->public_.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->destroy_func = NULL; + list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets); + return &list->public_; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + rc_client_leaderboard_list_info_t* info = (rc_client_leaderboard_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + free(list); +} + +int rc_client_has_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->has_leaderboards) + return client->state.external_client->has_leaderboards(); +#endif + + if (!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; + + (void)client; + (void)now; + + 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->state.hardcore) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission not allowed in softcore", leaderboard->public_.id); + return; + } + + 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_info_t* info; + const size_t list_size = sizeof(*info) + 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; + + info = (rc_client_leaderboard_entry_list_info_t*)malloc(needed_size); + if (!info) { + 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_list_t* list = &info->public_; + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)info + sizeof(*info)); + 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; + info->destroy_func = NULL; + 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_client_async_handle_t* async_handle; + 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; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries) + return client->state.external_client->begin_fetch_leaderboard_entries(client, leaderboard_id, first_entry, count, callback, callback_userdata); +#endif + + 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries_around_user) + return client->state.external_client->begin_fetch_leaderboard_entries_around_user(client, leaderboard_id, count, callback, callback_userdata); +#endif + + 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) +{ + rc_client_leaderboard_entry_list_info_t* info = (rc_client_leaderboard_entry_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + 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; + api_params.game_hash = client->game->public_.hash; + api_params.hardcore = client->state.hardcore; + + 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) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->has_rich_presence) + return client->state.external_client->has_rich_presence(); +#endif + + if (!client->game || !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 || !buffer) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_rich_presence_message) + return client->state.external_client->get_rich_presence_message(buffer, buffer_size); +#endif + + if (!client->game) + 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); + /* snprintf will return the amount of space needed, we want to return the number of chars written */ + if ((size_t)result >= buffer_size) + return (buffer_size - 1); + } + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_event_handler) + client->state.external_client->set_event_handler(client, handler); +#endif + + 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) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_read_memory) + client->state.external_client->set_read_memory(client, handler); +#endif + + 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) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->is_processing_required) + return client->state.external_client->is_processing_required(); +#endif + + if (!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); + + /* trigger->state doesn't actually change to RESET - RESET just serves as a notification. + * we don't care about that particular notification, so look at the actual state. */ + if (new_state == RC_TRIGGER_STATE_RESET) + new_state = trigger->state; + + /* 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)); + + (void)callback_data; + (void)now; + + 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->do_frame) { + client->state.external_client->do_frame(); + return; + } +#endif + + 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 || client->state.allow_leaderboards_in_softcore) { + 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); + } + + /* we've processed a frame. if there's a pause delay in effect, process it */ + if (client->state.unpaused_frame_decay > 0) { + client->state.unpaused_frame_decay--; + + if (client->state.unpaused_frame_decay == 0 && + client->state.required_unpaused_frames > RC_MINIMUM_UNPAUSED_FRAMES) { + /* the full decay has elapsed and a penalty still exists. + * lower the penalty and reset the decay counter */ + client->state.required_unpaused_frames >>= 1; + + if (client->state.required_unpaused_frames <= RC_MINIMUM_UNPAUSED_FRAMES) + client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + + client->state.unpaused_frame_decay = + client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1) - 1; + } + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->idle) { + client->state.external_client->idle(); + return; + } +#endif + + 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) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->reset) { + client->state.external_client->reset(); + return; + } +#endif + + if (!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); +} + +int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining) +{ +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->can_pause) + return client->state.external_client->can_pause(frames_remaining); +#endif + + if (frames_remaining) + *frames_remaining = 0; + + /* pause is always allowed in softcore */ + if (!rc_client_get_hardcore_enabled(client)) + return 1; + + /* a full decay means we haven't processed any frames since the last time this was called. */ + if (client->state.unpaused_frame_decay == client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER) + return 1; + + /* if less than RC_MINIMUM_UNPAUSED_FRAMES have been processed, don't allow the pause */ + if (client->state.unpaused_frame_decay > client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1)) { + if (frames_remaining) { + *frames_remaining = client->state.unpaused_frame_decay - + client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1); + } + return 0; + } + + /* we're going to allow the emulator to pause. calculate how many frames are needed before the next + * pause will be allowed. */ + + if (client->state.unpaused_frame_decay > 0) { + /* The user has paused within the decay window. Require a longer + * run of unpaused frames before allowing the next pause */ + if (client->state.required_unpaused_frames < 5 * 60) /* don't make delay longer then 5 seconds */ + client->state.required_unpaused_frames += RC_MINIMUM_UNPAUSED_FRAMES; + } + + /* require multiple unpaused_frames windows to decay the penalty */ + client->state.unpaused_frame_decay = client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER; + + return 1; +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->progress_size) + return client->state.external_client->progress_size(); +#endif + + if (!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) + return RC_NO_GAME_LOADED; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->serialize_progress) + return client->state.external_client->serialize_progress(buffer); +#endif + + if (!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) + return RC_NO_GAME_LOADED; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->deserialize_progress) + return client->state.external_client->deserialize_progress(serialized); +#endif + + if (!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); + + if (!client->state.allow_leaderboards_in_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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_hardcore_enabled) { + client->state.external_client->set_hardcore_enabled(enabled); + return; + } +#endif + + 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) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_hardcore_enabled) + return client->state.external_client->get_hardcore_enabled(); +#endif + + return client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_unofficial_enabled) { + client->state.external_client->set_unofficial_enabled(enabled); + return; + } +#endif + + 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) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_unofficial_enabled) + return client->state.external_client->get_unofficial_enabled(); +#endif + + return client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_encore_mode_enabled) { + client->state.external_client->set_encore_mode_enabled(enabled); + return; + } +#endif + + 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) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_encore_mode_enabled) + return client->state.external_client->get_encore_mode_enabled(); +#endif + + return client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_spectator_mode_enabled) { + client->state.external_client->set_spectator_mode_enabled(enabled); + return; + } +#endif + + 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) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_spectator_mode_enabled) + return client->state.external_client->get_spectator_mode_enabled(); +#endif + + return (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); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client && client->state.external_client && client->state.external_client->set_host) + client->state.external_client->set_host(hostname); +#endif +} + +size_t rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size) +{ + size_t result; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client && client->state.external_client && client->state.external_client->get_user_agent_clause) { + result = client->state.external_client->get_user_agent_clause(buffer, buffer_size); + if (result > 0) { + result += snprintf(buffer + result, buffer_size - result, " rc_client/" RCHEEVOS_VERSION_STRING); + buffer[buffer_size - 1] = '\0'; + return result; + } + } +#else + (void)client; +#endif + + result = snprintf(buffer, buffer_size, "rcheevos/" RCHEEVOS_VERSION_STRING); + + /* some implementations of snprintf will fill the buffer without null terminating. + * make sure the buffer is null terminated */ + buffer[buffer_size - 1] = '\0'; + return result; +} diff --git a/3rdparty/rcheevos/src/rc_client_external.h b/3rdparty/rcheevos/src/rc_client_external.h new file mode 100644 index 0000000000..a519e428e4 --- /dev/null +++ b/3rdparty/rcheevos/src/rc_client_external.h @@ -0,0 +1,132 @@ +#ifndef RC_CLIENT_EXTERNAL_H +#define RC_CLIENT_EXTERNAL_H + +#include "rc_client.h" + +RC_BEGIN_C_DECLS + +/* NOTE: any function that is passed a callback also needs to be passed a client instance to pass + * to the callback, and the external interface has to capture both. */ + +typedef void (RC_CCONV *rc_client_external_enable_logging_func_t)(rc_client_t* client, int level, rc_client_message_callback_t callback); +typedef void (RC_CCONV *rc_client_external_set_event_handler_func_t)(rc_client_t* client, rc_client_event_handler_t handler); +typedef void (RC_CCONV *rc_client_external_set_read_memory_func_t)(rc_client_t* client, rc_client_read_memory_func_t handler); +typedef void (RC_CCONV *rc_client_external_set_get_time_millisecs_func_t)(rc_client_t* client, rc_get_time_millisecs_func_t handler); +typedef int (RC_CCONV *rc_client_external_can_pause_func_t)(uint32_t* frames_remaining); + +typedef void (RC_CCONV *rc_client_external_set_int_func_t)(int value); +typedef int (RC_CCONV *rc_client_external_get_int_func_t)(void); +typedef void (RC_CCONV *rc_client_external_set_string_func_t)(const char* value); +typedef size_t (RC_CCONV *rc_client_external_copy_string_func_t)(char buffer[], size_t buffer_size); +typedef void (RC_CCONV *rc_client_external_action_func_t)(void); + +typedef void (RC_CCONV *rc_client_external_async_handle_func_t)(rc_client_async_handle_t* handle); + +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_login_func_t)(rc_client_t* client, + const char* username, const char* pass_token, rc_client_callback_t callback, void* callback_userdata); +typedef const rc_client_user_t* (RC_CCONV *rc_client_external_get_user_info_func_t)(void); + +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_identify_and_load_game_func_t)( + 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); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_game_func_t)(rc_client_t* client, + const char* hash, rc_client_callback_t callback, void* callback_userdata); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_subset_t)(rc_client_t* client, + uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); +typedef const rc_client_game_t* (RC_CCONV *rc_client_external_get_game_info_func_t)(void); +typedef const rc_client_subset_t* (RC_CCONV *rc_client_external_get_subset_info_func_t)(uint32_t subset_id); +typedef void (RC_CCONV *rc_client_external_get_user_game_summary_func_t)(rc_client_user_game_summary_t* summary); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_change_media_func_t)(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +/* NOTE: rc_client_external_create_achievement_list_func_t returns an internal wrapper structure which contains the public list + * and a destructor function. */ +struct rc_client_achievement_list_info_t; +typedef struct rc_client_achievement_list_info_t* (RC_CCONV *rc_client_external_create_achievement_list_func_t)(int category, int grouping); +typedef const rc_client_achievement_t* (RC_CCONV *rc_client_external_get_achievement_info_func_t)(uint32_t id); + +/* NOTE: rc_client_external_create_leaderboard_list_func_t returns an internal wrapper structure which contains the public list + * and a destructor function. */ +struct rc_client_leaderboard_list_info_t; +typedef struct rc_client_leaderboard_list_info_t* (RC_CCONV *rc_client_external_create_leaderboard_list_func_t)(int grouping); +typedef const rc_client_leaderboard_t* (RC_CCONV *rc_client_external_get_leaderboard_info_func_t)(uint32_t id); + +/* NOTE: rc_client_external_begin_fetch_leaderboard_entries_func_t and rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t + * pass an internal wrapper structure around the list, which contains the public list and a destructor function. */ +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_fetch_leaderboard_entries_func_t)(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); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t)(rc_client_t* client, + uint32_t leaderboard_id, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + + +typedef size_t (RC_CCONV *rc_client_external_progress_size_func_t)(void); +typedef int (RC_CCONV *rc_client_external_serialize_progress_func_t)(uint8_t* buffer); +typedef int (RC_CCONV *rc_client_external_deserialize_progress_func_t)(const uint8_t* buffer); + +typedef struct rc_client_external_t +{ + rc_client_external_action_func_t destroy; + + rc_client_external_enable_logging_func_t enable_logging; + rc_client_external_set_event_handler_func_t set_event_handler; + rc_client_external_set_read_memory_func_t set_read_memory; + rc_client_external_set_get_time_millisecs_func_t set_get_time_millisecs; + rc_client_external_set_string_func_t set_host; + rc_client_external_copy_string_func_t get_user_agent_clause; + + rc_client_external_set_int_func_t set_hardcore_enabled; + rc_client_external_get_int_func_t get_hardcore_enabled; + rc_client_external_set_int_func_t set_unofficial_enabled; + rc_client_external_get_int_func_t get_unofficial_enabled; + rc_client_external_set_int_func_t set_encore_mode_enabled; + rc_client_external_get_int_func_t get_encore_mode_enabled; + rc_client_external_set_int_func_t set_spectator_mode_enabled; + rc_client_external_get_int_func_t get_spectator_mode_enabled; + + rc_client_external_async_handle_func_t abort_async; + + rc_client_external_begin_login_func_t begin_login_with_password; + rc_client_external_begin_login_func_t begin_login_with_token; + rc_client_external_action_func_t logout; + rc_client_external_get_user_info_func_t get_user_info; + + rc_client_external_begin_identify_and_load_game_func_t begin_identify_and_load_game; + rc_client_external_begin_load_game_func_t begin_load_game; + rc_client_external_get_game_info_func_t get_game_info; + rc_client_external_begin_load_subset_t begin_load_subset; + rc_client_external_get_subset_info_func_t get_subset_info; + rc_client_external_action_func_t unload_game; + rc_client_external_get_user_game_summary_func_t get_user_game_summary; + rc_client_external_begin_change_media_func_t begin_change_media; + + rc_client_external_create_achievement_list_func_t create_achievement_list; + rc_client_external_get_int_func_t has_achievements; + rc_client_external_get_achievement_info_func_t get_achievement_info; + + rc_client_external_create_leaderboard_list_func_t create_leaderboard_list; + rc_client_external_get_int_func_t has_leaderboards; + rc_client_external_get_leaderboard_info_func_t get_leaderboard_info; + rc_client_external_begin_fetch_leaderboard_entries_func_t begin_fetch_leaderboard_entries; + rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t begin_fetch_leaderboard_entries_around_user; + + rc_client_external_copy_string_func_t get_rich_presence_message; + rc_client_external_get_int_func_t has_rich_presence; + + rc_client_external_action_func_t do_frame; + rc_client_external_action_func_t idle; + rc_client_external_get_int_func_t is_processing_required; + rc_client_external_can_pause_func_t can_pause; + rc_client_external_action_func_t reset; + + rc_client_external_progress_size_func_t progress_size; + rc_client_external_serialize_progress_func_t serialize_progress; + rc_client_external_deserialize_progress_func_t deserialize_progress; + +} rc_client_external_t; + +#define RC_CLIENT_EXTERNAL_VERSION 1 + +RC_END_C_DECLS + +#endif /* RC_CLIENT_EXTERNAL_H */ diff --git a/3rdparty/rcheevos/src/rc_client_internal.h b/3rdparty/rcheevos/src/rc_client_internal.h new file mode 100644 index 0000000000..5e57091516 --- /dev/null +++ b/3rdparty/rcheevos/src/rc_client_internal.h @@ -0,0 +1,394 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#include "rc_client.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + #include "rc_client_raintegration_internal.h" +#endif +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + #include "rc_client_external.h" +#endif + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +struct rc_api_fetch_game_data_response_t; +typedef void (RC_CCONV *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_CCONV *rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); +typedef int (RC_CCONV *rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); +typedef int (RC_CCONV *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_CCONV *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); + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| 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; + +struct rc_client_achievement_list_info_t; +typedef void (RC_CCONV *rc_client_destroy_achievement_list_func_t)(struct rc_client_achievement_list_info_t* list); + +typedef struct rc_client_achievement_list_info_t { + rc_client_achievement_list_t public_; + rc_client_destroy_achievement_list_func_t destroy_func; +} rc_client_achievement_list_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; + +struct rc_client_leaderboard_list_info_t; +typedef void (RC_CCONV *rc_client_destroy_leaderboard_list_func_t)(struct rc_client_leaderboard_list_info_t* list); + +typedef struct rc_client_leaderboard_list_info_t { + rc_client_leaderboard_list_t public_; + rc_client_destroy_leaderboard_list_func_t destroy_func; +} rc_client_leaderboard_list_info_t; + +struct rc_client_leaderboard_entry_list_info_t; +typedef void (RC_CCONV *rc_client_destroy_leaderboard_entry_list_func_t)(struct rc_client_leaderboard_entry_list_info_t* list); + +typedef struct rc_client_leaderboard_entry_list_info_t { + rc_client_leaderboard_entry_list_t public_; + rc_client_destroy_leaderboard_entry_list_func_t destroy_func; +} rc_client_leaderboard_entry_list_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; + +rc_client_async_handle_t* 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; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + rc_client_external_t* external_client; +#endif +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + rc_client_raintegration_t* raintegration; +#endif + + uint16_t unpaused_frame_decay; + uint16_t required_unpaused_frames; + + 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; + uint8_t allow_leaderboards_in_softcore; + + 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); + +RC_END_C_DECLS + +#endif /* RC_CLIENT_INTERNAL_H */ diff --git a/3rdparty/rcheevos/src/rc_client_raintegration.c b/3rdparty/rcheevos/src/rc_client_raintegration.c new file mode 100644 index 0000000000..efea7e449d --- /dev/null +++ b/3rdparty/rcheevos/src/rc_client_raintegration.c @@ -0,0 +1,493 @@ +#include "rc_client_raintegration_internal.h" + +#include "rc_client_internal.h" + +#include "rapi/rc_api_common.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +static void rc_client_raintegration_load_dll(rc_client_t* client, + const wchar_t* search_directory, rc_client_callback_t callback, void* callback_userdata) +{ + wchar_t sPath[_MAX_PATH]; + const int nPathSize = sizeof(sPath) / sizeof(sPath[0]); + rc_client_raintegration_t* raintegration; + int sPathIndex = 0; + DWORD dwAttrib; + HINSTANCE hDLL; + + if (search_directory) { + sPathIndex = swprintf_s(sPath, nPathSize, L"%s\\", search_directory); + if (sPathIndex > nPathSize - 22) { + callback(RC_INVALID_STATE, "search_directory too long", client, callback_userdata); + return; + } + } + +#if defined(_M_X64) || defined(__amd64__) + wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration-x64.dll"); + dwAttrib = GetFileAttributesW(sPath); + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); + dwAttrib = GetFileAttributesW(sPath); + } +#else + wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); + dwAttrib = GetFileAttributesW(sPath); +#endif + + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + callback(RC_MISSING_VALUE, "RA_Integration.dll not found in search directory", client, callback_userdata); + return; + } + + hDLL = LoadLibraryW(sPath); + if (hDLL == NULL) { + char error_message[512]; + const DWORD last_error = GetLastError(); + int offset = snprintf(error_message, sizeof(error_message), "Failed to load RA_Integration.dll (%u)", last_error); + + if (last_error != 0) { + LPSTR messageBuffer = NULL; + const DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); + + snprintf(&error_message[offset], sizeof(error_message) - offset, ": %.*s", size, messageBuffer); + + LocalFree(messageBuffer); + } + + callback(RC_ABORTED, error_message, client, callback_userdata); + return; + } + + raintegration = (rc_client_raintegration_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_raintegration_t)); + memset(raintegration, 0, sizeof(*raintegration)); + raintegration->hDLL = hDLL; + + raintegration->get_version = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_IntegrationVersion"); + raintegration->get_host_url = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_HostUrl"); + raintegration->init_client = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitClient"); + raintegration->init_client_offline = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitOffline"); + raintegration->shutdown = (rc_client_raintegration_action_func_t)GetProcAddress(hDLL, "_RA_Shutdown"); + + raintegration->update_main_window_handle = (rc_client_raintegration_hwnd_action_func_t)GetProcAddress(hDLL, "_RA_UpdateHWnd"); + + raintegration->get_external_client = (rc_client_raintegration_get_external_client_func_t)GetProcAddress(hDLL, "_Rcheevos_GetExternalClient"); + raintegration->get_menu = (rc_client_raintegration_get_menu_func_t)GetProcAddress(hDLL, "_Rcheevos_RAIntegrationGetMenu"); + raintegration->activate_menu_item = (rc_client_raintegration_activate_menuitem_func_t)GetProcAddress(hDLL, "_Rcheevos_ActivateRAIntegrationMenuItem"); + raintegration->set_write_memory_function = (rc_client_raintegration_set_write_memory_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationWriteMemoryFunction"); + raintegration->set_event_handler = (rc_client_raintegration_set_event_handler_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationEventHandler"); + + if (!raintegration->get_version || + !raintegration->init_client || + !raintegration->get_external_client) { + FreeLibrary(hDLL); + + callback(RC_ABORTED, "One or more required exports was not found in RA_Integration.dll", client, callback_userdata); + } + else { + rc_mutex_lock(&client->state.mutex); + client->state.raintegration = raintegration; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "RA_Integration.dll %s loaded", client->state.raintegration->get_version()); + } +} + +typedef struct rc_client_version_validation_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + HWND main_window_handle; + char* client_name; + char* client_version; + rc_client_async_handle_t async_handle; +} rc_client_version_validation_callback_data_t; + +int rc_client_version_less(const char* left, const char* right) +{ + do { + int left_len = 0; + int right_len = 0; + while (*left && *left == '0') + ++left; + while (left[left_len] && left[left_len] != '.') + ++left_len; + while (*right && *right == '0') + ++right; + while (right[right_len] && right[right_len] != '.') + ++right_len; + + if (left_len != right_len) + return (left_len < right_len); + + while (left_len--) { + if (*left != *right) + return (*left < *right); + ++left; + ++right; + } + + if (*left == '.') + ++left; + if (*right == '.') + ++right; + } while (*left || *right); + + return 0; +} + +static void rc_client_init_raintegration(rc_client_t* client, + rc_client_version_validation_callback_data_t* version_validation_callback_data) +{ + rc_client_raintegration_init_client_func_t init_func = client->state.raintegration->init_client; + + if (client->state.raintegration->get_host_url) { + const char* host_url = client->state.raintegration->get_host_url(); + if (host_url) { + if (strcmp(host_url, "OFFLINE") != 0) { + rc_client_set_host(client, host_url); + } + else if (client->state.raintegration->init_client_offline) { + init_func = client->state.raintegration->init_client_offline; + RC_CLIENT_LOG_INFO(client, "Initializing in offline mode"); + } + } + } + + if (!init_func || !init_func(version_validation_callback_data->main_window_handle, + version_validation_callback_data->client_name, + version_validation_callback_data->client_version)) { + const char* error_message = "RA_Integration initialization failed"; + + rc_client_unload_raintegration(client); + + RC_CLIENT_LOG_ERR(client, error_message); + version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); + } + else { + rc_client_external_t* external_client = (rc_client_external_t*) + rc_buffer_alloc(&client->state.buffer, sizeof(*external_client)); + memset(external_client, 0, sizeof(*external_client)); + + if (!client->state.raintegration->get_external_client(external_client, RC_CLIENT_EXTERNAL_VERSION)) { + const char* error_message = "RA_Integration external client export failed"; + + rc_client_unload_raintegration(client); + + RC_CLIENT_LOG_ERR(client, error_message); + version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); + } + else { + /* copy state to the external client */ + if (external_client->enable_logging) + external_client->enable_logging(client, client->state.log_level, client->callbacks.log_call); + + if (external_client->set_event_handler) + external_client->set_event_handler(client, client->callbacks.event_handler); + if (external_client->set_read_memory) + external_client->set_read_memory(client, client->callbacks.read_memory); + + if (external_client->set_hardcore_enabled) + external_client->set_hardcore_enabled(rc_client_get_hardcore_enabled(client)); + if (external_client->set_unofficial_enabled) + external_client->set_unofficial_enabled(rc_client_get_unofficial_enabled(client)); + if (external_client->set_encore_mode_enabled) + external_client->set_encore_mode_enabled(rc_client_get_encore_mode_enabled(client)); + if (external_client->set_spectator_mode_enabled) + external_client->set_spectator_mode_enabled(rc_client_get_spectator_mode_enabled(client)); + + /* attach the external client and call the callback */ + client->state.external_client = external_client; + + client->state.raintegration->bIsInited = 1; + + version_validation_callback_data->callback(RC_OK, NULL, + client, version_validation_callback_data->callback_userdata); + } + } +} + +static void rc_client_version_validation_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_version_validation_callback_data_t* version_validation_callback_data = + (rc_client_version_validation_callback_data_t*)callback_data; + rc_client_t* client = version_validation_callback_data->client; + + if (rc_client_async_handle_aborted(client, &version_validation_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Version validation aborted"); + } + else { + rc_api_response_t response; + int result; + const char* current_version; + const char* minimum_version = ""; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("MinimumVersion"), + }; + + memset(&response, 0, sizeof(response)); + rc_buffer_init(&response.buffer); + + result = rc_json_parse_server_response(&response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result == RC_OK) { + if (!rc_json_get_required_string(&minimum_version, &response, &fields[2], "MinimumVersion")) + result = RC_MISSING_VALUE; + } + + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to fetch latest integration version: %.*s", server_response->body_length, server_response->body); + + rc_client_unload_raintegration(client); + + version_validation_callback_data->callback(result, rc_error_str(result), + client, version_validation_callback_data->callback_userdata); + } + else { + current_version = client->state.raintegration->get_version(); + + if (rc_client_version_less(current_version, minimum_version)) { + char error_message[256]; + + rc_client_unload_raintegration(client); + + snprintf(error_message, sizeof(error_message), + "RA_Integration version %s is lower than minimum version %s", current_version, minimum_version); + RC_CLIENT_LOG_WARN(client, error_message); + version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Validated RA_Integration version %s (minimum %s)", current_version, minimum_version); + + rc_client_init_raintegration(client, version_validation_callback_data); + } + } + + rc_buffer_destroy(&response.buffer); + } + + free(version_validation_callback_data->client_name); + free(version_validation_callback_data->client_version); + free(version_validation_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client, + const wchar_t* search_directory, HWND main_window_handle, + const char* client_name, const char* client_version, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_version_validation_callback_data_t* callback_data; + rc_api_url_builder_t builder; + rc_api_request_t request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!client_name) { + callback(RC_INVALID_STATE, "client_name is required", client, callback_userdata); + return NULL; + } + + if (!client_version) { + callback(RC_INVALID_STATE, "client_version is required", client, callback_userdata); + return NULL; + } + + if (client->state.user != RC_CLIENT_USER_STATE_NONE) { + callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata); + return NULL; + } + + if (!client->state.raintegration) { + if (!main_window_handle) { + callback(RC_INVALID_STATE, "main_window_handle is required", client, callback_userdata); + return NULL; + } + + rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata); + if (!client->state.raintegration) + return NULL; + } + + if (client->state.raintegration->get_host_url) { + const char* host_url = client->state.raintegration->get_host_url(); + if (host_url && strcmp(host_url, "https://retroachievements.org") != 0 && + strcmp(host_url, "OFFLINE") != 0) { + /* if the DLL specifies a custom host, use it */ + rc_client_set_host(client, host_url); + } + } + + memset(&request, 0, sizeof(request)); + rc_api_url_build_dorequest_url(&request); + rc_url_builder_init(&builder, &request.buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "latestintegration"); + request.post_data = rc_url_builder_finalize(&builder); + + callback_data = 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; + callback_data->client_name = strdup(client_name); + callback_data->client_version = strdup(client_version); + callback_data->main_window_handle = main_window_handle; + + client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client); + return &callback_data->async_handle; +} + +void rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle) +{ + if (client && client->state.raintegration && + client->state.raintegration->bIsInited && + client->state.raintegration->update_main_window_handle) + { + client->state.raintegration->update_main_window_handle(main_window_handle); + } +} + +void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler) +{ + if (client && client->state.raintegration && client->state.raintegration->set_write_memory_function) + client->state.raintegration->set_write_memory_function(client, handler); +} + +void rc_client_raintegration_set_event_handler(rc_client_t* client, + rc_client_raintegration_event_handler_t handler) +{ + if (client && client->state.raintegration && client->state.raintegration->set_event_handler) + client->state.raintegration->set_event_handler(client, handler); +} + +const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_client_t* client) +{ + if (!client || !client->state.raintegration || + !client->state.raintegration->bIsInited || + !client->state.raintegration->get_menu) + { + return NULL; + } + + return client->state.raintegration->get_menu(); +} + +void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu) +{ + HMENU hPopupMenu = NULL; + const rc_client_raintegration_menu_t* menu; + + if (!client || !client->state.raintegration) + return; + + /* destroy the existing menu */ + if (client->state.raintegration->hPopupMenu) + DestroyMenu(client->state.raintegration->hPopupMenu); + + /* create the popup menu */ + hPopupMenu = CreatePopupMenu(); + + menu = rc_client_raintegration_get_menu(client); + if (menu && menu->num_items) + { + const rc_client_raintegration_menu_item_t* menuitem = menu->items; + const rc_client_raintegration_menu_item_t* stop = menu->items + menu->num_items; + + for (; menuitem < stop; ++menuitem) + { + if (menuitem->id == 0) + AppendMenuA(hPopupMenu, MF_SEPARATOR, 0U, NULL); + else + { + UINT flags = MF_STRING; + if (menuitem->checked) + flags |= MF_CHECKED; + if (!menuitem->enabled) + flags |= MF_DISABLED | MF_GRAYED; + + AppendMenuA(hPopupMenu, flags, menuitem->id, menuitem->label); + } + } + } + + /* add/update the item containing the popup menu */ + { + int nIndex = GetMenuItemCount(hMenu); + const char* menuText = "&RetroAchievements"; + char buffer[64]; + + UINT flags = MF_POPUP | MF_STRING; + if (!menu || !menu->num_items) + flags |= MF_DISABLED | MF_GRAYED; + + while (--nIndex >= 0) + { + if (GetMenuStringA(hMenu, nIndex, buffer, sizeof(buffer) - 1, MF_BYPOSITION)) + { + if (strcmp(buffer, menuText) == 0) + break; + } + } + + if (nIndex == -1) + AppendMenuA(hMenu, flags, (UINT_PTR)hPopupMenu, menuText); + else + ModifyMenuA(hMenu, nIndex, flags | MF_BYPOSITION, (UINT_PTR)hPopupMenu, menuText); + } + + client->state.raintegration->hPopupMenu = hPopupMenu; +} + +void rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menuitem) +{ + if (client && client->state.raintegration && client->state.raintegration->hPopupMenu) + { + UINT flags = MF_STRING; + if (menuitem->checked) + flags |= MF_CHECKED; + + CheckMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); + } +} + +int rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t nMenuItemId) +{ + if (!client || !client->state.raintegration || !client->state.raintegration->activate_menu_item) + return 0; + + return client->state.raintegration->activate_menu_item(nMenuItemId); +} + +void rc_client_unload_raintegration(rc_client_t* client) +{ + HINSTANCE hDLL; + + if (!client || !client->state.raintegration) + return; + + RC_CLIENT_LOG_INFO(client, "Unloading RA_Integration") + + if (client->state.raintegration->shutdown) + client->state.raintegration->shutdown(); + + rc_mutex_lock(&client->state.mutex); + hDLL = client->state.raintegration->hDLL; + client->state.raintegration = NULL; + client->state.external_client = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (hDLL) + FreeLibrary(hDLL); +} + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ diff --git a/3rdparty/rcheevos/src/rc_client_raintegration_internal.h b/3rdparty/rcheevos/src/rc_client_raintegration_internal.h new file mode 100644 index 0000000000..530d98e1a8 --- /dev/null +++ b/3rdparty/rcheevos/src/rc_client_raintegration_internal.h @@ -0,0 +1,52 @@ +#ifndef RC_CLIENT_RAINTEGRATION_INTERNAL_H +#define RC_CLIENT_RAINTEGRATION_INTERNAL_H + +#include "rc_client_raintegration.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +#include "rc_client_external.h" +#include "rc_compat.h" + +RC_BEGIN_C_DECLS + +/* RAIntegration follows the same calling convention as rcheevos */ + +typedef void (RC_CCONV* rc_client_raintegration_action_func_t)(void); +typedef const char* (RC_CCONV* rc_client_raintegration_get_string_func_t)(void); +typedef int (RC_CCONV* rc_client_raintegration_init_client_func_t)(HWND hMainWnd, const char* sClientName, const char* sClientVersion); +typedef int (RC_CCONV* rc_client_raintegration_get_external_client_func_t)(rc_client_external_t* pClient, int nVersion); +typedef void (RC_CCONV* rc_client_raintegration_hwnd_action_func_t)(HWND hWnd); +typedef const rc_client_raintegration_menu_t* (RC_CCONV* rc_client_raintegration_get_menu_func_t)(void); +typedef int (RC_CCONV* rc_client_raintegration_activate_menuitem_func_t)(uint32_t nMenuItemId); +typedef void (RC_CCONV* rc_client_raintegration_set_write_memory_func_t)(rc_client_t* pClient, rc_client_raintegration_write_memory_func_t handler); +typedef void (RC_CCONV* rc_client_raintegration_set_event_handler_func_t)(rc_client_t* pClient, rc_client_raintegration_event_handler_t handler); + +typedef struct rc_client_raintegration_t +{ + HINSTANCE hDLL; + HMENU hPopupMenu; + uint8_t bIsInited; + + rc_client_raintegration_get_string_func_t get_version; + rc_client_raintegration_get_string_func_t get_host_url; + rc_client_raintegration_init_client_func_t init_client; + rc_client_raintegration_init_client_func_t init_client_offline; + rc_client_raintegration_action_func_t shutdown; + + rc_client_raintegration_hwnd_action_func_t update_main_window_handle; + + rc_client_raintegration_set_write_memory_func_t set_write_memory_function; + rc_client_raintegration_set_event_handler_func_t set_event_handler; + rc_client_raintegration_get_menu_func_t get_menu; + rc_client_raintegration_activate_menuitem_func_t activate_menu_item; + + rc_client_raintegration_get_external_client_func_t get_external_client; + +} rc_client_raintegration_t; + +RC_END_C_DECLS + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ + +#endif /* RC_CLIENT_RAINTEGRATION_INTERNAL_H */ diff --git a/3rdparty/rcheevos/src/rc_compat.c b/3rdparty/rcheevos/src/rc_compat.c new file mode 100644 index 0000000000..6a8a5de57a --- /dev/null +++ b/3rdparty/rcheevos/src/rc_compat.c @@ -0,0 +1,164 @@ +#include "rc_compat.h" + +#include +#include + +#ifdef RC_C89_HELPERS + +int rc_strncasecmp(const char* left, const char* right, size_t length) +{ + while (length) + { + if (*left != *right) + { + const int diff = tolower(*left) - tolower(*right); + if (diff != 0) + return diff; + } + + ++left; + ++right; + --length; + } + + return 0; +} + +int rc_strcasecmp(const char* left, const char* right) +{ + while (*left || *right) + { + if (*left != *right) + { + const int diff = tolower(*left) - tolower(*right); + if (diff != 0) + return diff; + } + + ++left; + ++right; + } + + return 0; +} + +char* rc_strdup(const char* str) +{ + const size_t length = strlen(str); + char* buffer = (char*)malloc(length + 1); + if (buffer) + memcpy(buffer, str, length + 1); + return buffer; +} + +int rc_snprintf(char* buffer, size_t size, const char* format, ...) +{ + int result; + va_list args; + + va_start(args, format); + +#ifdef __STDC_WANT_SECURE_LIB__ + result = vsprintf_s(buffer, size, format, args); +#else + /* assume buffer is large enough and ignore size */ + (void)size; + result = vsprintf(buffer, format, args); +#endif + + va_end(args); + + return result; +} + +#endif + +#ifndef __STDC_WANT_SECURE_LIB__ + +struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) +{ + struct tm* tm = gmtime(timer); + memcpy(buf, tm, sizeof(*tm)); + return buf; +} + +#endif + +#ifndef RC_NO_THREADS + +#if defined(_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); +} + +#elif defined(GEKKO) + +/* https://github.com/libretro/RetroArch/pull/16116 */ + +void rc_mutex_init(rc_mutex_t* mutex) +{ + LWP_MutexInit(mutex, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + LWP_MutexDestroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + LWP_MutexLock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + LWP_MutexUnlock(mutex); +} + +#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/3rdparty/rcheevos/src/rc_compat.h b/3rdparty/rcheevos/src/rc_compat.h new file mode 100644 index 0000000000..2ef9ae0c5b --- /dev/null +++ b/3rdparty/rcheevos/src/rc_compat.h @@ -0,0 +1,97 @@ +#ifndef RC_COMPAT_H +#define RC_COMPAT_H + +#include "rc_export.h" + +#include +#include +#include + +RC_BEGIN_C_DECLS + +#if defined(MINGW) || defined(__MINGW32__) || defined(__MINGW64__) + +/* MinGW redefinitions */ + +#define RC_NO_VARIADIC_MACROS 1 + +#elif defined(_MSC_VER) + +/* Visual Studio redefinitions */ + +#ifndef strcasecmp + #define strcasecmp _stricmp +#endif +#ifndef strncasecmp + #define strncasecmp _strnicmp +#endif +#ifndef strdup + #define strdup _strdup +#endif + +#elif __STDC_VERSION__ < 199901L + +/* 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 +#endif + +#ifndef strncasecmp + extern int rc_strncasecmp(const char* left, const char* right, size_t length); + #define strncasecmp rc_strncasecmp +#endif + +#ifndef strcasecmp + extern int rc_strcasecmp(const char* left, const char* right); + #define strcasecmp rc_strcasecmp +#endif + +#ifndef strdup + extern char* rc_strdup(const char* str); + #define strdup rc_strdup +#endif + +#endif /* __STDC_VERSION__ < 199901L */ + +#ifndef __STDC_WANT_SECURE_LIB__ + /* _CRT_SECURE_NO_WARNINGS redefinitions */ + #define strcpy_s(dest, sz, src) strcpy(dest, src) + #define sscanf_s sscanf + + /* NOTE: Microsoft secure gmtime_s parameter order differs from C11 standard */ + #include + extern struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer); + #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 + +RC_END_C_DECLS + +#endif /* RC_COMPAT_H */ diff --git a/3rdparty/rcheevos/src/rc_libretro.c b/3rdparty/rcheevos/src/rc_libretro.c new file mode 100644 index 0000000000..d94d6d5b4c --- /dev/null +++ b/3rdparty/rcheevos/src/rc_libretro.c @@ -0,0 +1,831 @@ +/* This file provides a series of functions for integrating RetroAchievements with libretro. + * These functions will be called by a libretro frontend to validate certain expected behaviors + * and simplify mapping core data to the RAIntegration DLL. + * + * Originally designed to be shared between RALibretro and RetroArch, but will simplify + * integrating with any other frontends. + */ + +#include "rc_libretro.h" + +#include "rc_consoles.h" +#include "rc_compat.h" + +#include +#include + +/* internal helper functions in hash.c */ +extern void* rc_file_open(const char* path); +extern void rc_file_seek(void* file_handle, int64_t offset, int origin); +extern int64_t rc_file_tell(void* file_handle); +extern size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes); +extern void rc_file_close(void* file_handle); +extern int rc_path_compare_extension(const char* path, const char* ext); +extern int rc_hash_error(const char* message); + + +static rc_libretro_message_callback rc_libretro_verbose_message_callback = NULL; + +/* a value that starts with a comma is a CSV. + * if it starts with an exclamation point, it's everything but the provided value. + * if it starts with an exclamntion point followed by a comma, it's everything but the CSV values. + * values are case-insensitive */ +typedef struct rc_disallowed_core_settings_t +{ + const char* library_name; + const rc_disallowed_setting_t* disallowed_settings; +} rc_disallowed_core_settings_t; + +static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { + { "bsnes_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_cap32_settings[] = { + { "cap32_autorun", "disabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = { + { "dolphin_cheats_enabled", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dosbox_pure_settings[] = { + { "dosbox_pure_strict_mode", "false" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = { + { "duckstation_CDROM.LoadImagePatches", "true" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { + { "ecwolf-invulnerability", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { + { "fbneo-allow-patched-romsets", "enabled" }, + { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, + { "fbneo-dipswitch-*", "Universe BIOS*" }, + { "fbneo-neogeo-mode", "UNIBIOS" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { + { "fceumm_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { + { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_wide_settings[] = { + { "genesis_plus_gx_wide_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_wide_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_settings[] = { + { "mesen_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_s_settings[] = { + { "mesen-s_region", "PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_neocd_settings[] = { + { "neocd_bios", "uni-bios*" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { + { "pcsx_rearmed_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_picodrive_settings[] = { + { "picodrive_region", ",Europe,Japan PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = { + { "ppsspp_cheats", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_quasi88_settings[] = { + { "q88_cpu_clock", ",1,2" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_smsplus_settings[] = { + { "smsplus_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { + { "snes9x_gfx_clip", "disabled" }, + { "snes9x_gfx_transp", "disabled" }, + { "snes9x_layer_*", "disabled" }, + { "snes9x_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { + { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ + { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = { + { "virtualjaguar_pal", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { + { "bsnes-mercury", _rc_disallowed_bsnes_settings }, + { "cap32", _rc_disallowed_cap32_settings }, + { "dolphin-emu", _rc_disallowed_dolphin_settings }, + { "DOSBox-pure", _rc_disallowed_dosbox_pure_settings }, + { "DuckStation", _rc_disallowed_duckstation_settings }, + { "ecwolf", _rc_disallowed_ecwolf_settings }, + { "FCEUmm", _rc_disallowed_fceumm_settings }, + { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, + { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, + { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, + { "Mesen", _rc_disallowed_mesen_settings }, + { "Mesen-S", _rc_disallowed_mesen_s_settings }, + { "NeoCD", _rc_disallowed_neocd_settings }, + { "PPSSPP", _rc_disallowed_ppsspp_settings }, + { "PCSX-ReARMed", _rc_disallowed_pcsx_rearmed_settings }, + { "PicoDrive", _rc_disallowed_picodrive_settings }, + { "QUASI88", _rc_disallowed_quasi88_settings }, + { "SMS Plus GX", _rc_disallowed_smsplus_settings }, + { "Snes9x", _rc_disallowed_snes9x_settings }, + { "VICE x64", _rc_disallowed_vice_settings }, + { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, + { NULL, NULL } +}; + +static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* value) { + char c1, c2; + while ((c1 = *test++)) { + if (tolower(c1) != tolower(c2 = *value++)) + return (c2 == '*'); + } + + return (*value == '\0'); +} + +static int rc_libretro_match_value(const char* val, const char* match) { + /* if value starts with a comma, it's a CSV list of potential matches */ + if (*match == ',') { + do { + const char* ptr = ++match; + size_t size; + + while (*match && *match != ',') + ++match; + + size = match - ptr; + if (val[size] == '\0') { + if (memcmp(ptr, val, size) == 0) { + return 1; + } + else { + char buffer[128]; + memcpy(buffer, ptr, size); + buffer[size] = '\0'; + if (rc_libretro_string_equal_nocase_wildcard(buffer, val)) + return 1; + } + } + } while (*match == ','); + + return 0; + } + + /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ + if (*match == '!') + return !rc_libretro_match_value(val, &match[1]); + + /* just a single value, attempt to match it */ + return rc_libretro_string_equal_nocase_wildcard(val, match); +} + +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { + const char* key; + size_t key_len; + + for (; disallowed_settings->setting; ++disallowed_settings) { + key = disallowed_settings->setting; + key_len = strlen(key); + + if (key[key_len - 1] == '*') { + if (memcmp(setting, key, key_len - 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + else { + if (memcmp(setting, key, key_len + 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + } + + return 1; +} + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name) { + const rc_disallowed_core_settings_t* core_filter = rc_disallowed_core_settings; + size_t library_name_length; + + if (!library_name || !library_name[0]) + return NULL; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) + return core_filter->disallowed_settings; + + ++core_filter; + } + + return NULL; +} + +typedef struct rc_disallowed_core_systems_t +{ + const char* library_name; + const uint32_t disallowed_consoles[4]; +} rc_disallowed_core_systems_t; + +static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { + /* https://github.com/libretro/Mesen-S/issues/8 */ + { "Mesen-S", { RC_CONSOLE_GAMEBOY, RC_CONSOLE_GAMEBOY_COLOR, 0 }}, + { NULL, { 0 } } +}; + +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; + + if (!library_name || !library_name[0]) + return 1; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) { + for (i = 0; i < sizeof(core_filter->disallowed_consoles) / sizeof(core_filter->disallowed_consoles[0]); ++i) { + if (core_filter->disallowed_consoles[i] == console_id) + return 0; + } + break; + } + + ++core_filter; + } + + return 1; +} + +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]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + if (avail) + *avail = (uint32_t)(size - address); + + return ®ions->data[i][address]; + } + + address -= (uint32_t)size; + } + + if (avail) + *avail = 0; + + return NULL; +} + +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 bytes_read = 0; + uint32_t avail; + uint32_t i; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address >= size) { + /* address is not in this block, adjust and look at next block */ + address -= (unsigned)size; + continue; + } + + if (regions->data[i] == NULL) /* no memory associated to this block. abort */ + break; + + avail = (unsigned)(size - address); + if (avail >= num_bytes) { + /* requested memory is fully within this block, copy and return it */ + memcpy(buffer, ®ions->data[i][address], num_bytes); + bytes_read += num_bytes; + return bytes_read; + } + + /* copy whatever is available in this block, and adjust for the next block */ + memcpy(buffer, ®ions->data[i][address], avail); + buffer += avail; + bytes_read += avail; + num_bytes -= avail; + address = 0; + } + + return bytes_read; +} + +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { + rc_libretro_verbose_message_callback = callback; +} + +static void rc_libretro_verbose(const char* message) { + if (rc_libretro_verbose_message_callback) + rc_libretro_verbose_message_callback(message); +} + +static const char* rc_memory_type_str(int type) { + switch (type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return "SRAM"; + case RC_MEMORY_TYPE_VIDEO_RAM: + return "VRAM"; + case RC_MEMORY_TYPE_UNUSED: + return "UNUSED"; + default: + break; + } + + return "SYSTEM RAM"; +} + +static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, + uint8_t* data, size_t size, const char* description) { + if (size == 0) + return; + + if (regions->count == (sizeof(regions->size) / sizeof(regions->size[0]))) { + rc_libretro_verbose("Too many memory memory regions to register"); + return; + } + + if (!data && regions->count > 0 && !regions->data[regions->count - 1]) { + /* extend null region */ + regions->size[regions->count - 1] += size; + } + else if (data && regions->count > 0 && + data == (regions->data[regions->count - 1] + regions->size[regions->count - 1])) { + /* extend non-null region */ + regions->size[regions->count - 1] += size; + } + else { + /* create new region */ + regions->data[regions->count] = data; + regions->size[regions->count] = size; + ++regions->count; + } + + regions->total_size += size; + + if (rc_libretro_verbose_message_callback) { + char message[128]; + snprintf(message, sizeof(message), "Registered 0x%04X bytes of %s at $%06X (%s)", (unsigned)size, + rc_memory_type_str(type), (unsigned)(regions->total_size - size), description); + rc_libretro_verbose_message_callback(message); + } +} + +static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info) { + /* no regions specified, assume system RAM followed by save RAM */ + char description[64]; + rc_libretro_core_memory_info_t info; + + snprintf(description, sizeof(description), "offset 0x%06x", 0); + + get_core_memory_info(RETRO_MEMORY_SYSTEM_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SYSTEM_RAM, info.data, info.size, description); + + get_core_memory_info(RETRO_MEMORY_SAVE_RAM, &info); + if (info.size) + 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, 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; + + for (; desc < end; desc++) { + if (desc->select == 0) { + /* if select is 0, attempt to explcitly match the address */ + if (real_address >= desc->start && real_address < desc->start + desc->len) { + *offset = real_address - desc->start; + return desc; + } + } + else { + /* otherwise, attempt to match the address by matching the select bits */ + /* 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 */ + 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. */ + uint32_t disconnect_mask = (unsigned)desc->disconnect; + while (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; + } + + /* calculate the offset within the descriptor */ + *offset = reduced_address; + + /* sanity check - make sure the descriptor is large enough to hold the target address */ + if (reduced_address < desc->len) + return desc; + } + } + } + + *offset = 0; + return NULL; +} + +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]; + 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; + 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", + (unsigned)(real_address - console_region->real_address + console_region->start_address)); + rc_libretro_verbose(description); + } + + if (disconnect_size && console_region_size > disconnect_size) { + rc_libretro_memory_register_region(regions, console_region->type, NULL, disconnect_size, "null filler"); + console_region_size -= disconnect_size; + real_address += disconnect_size; + disconnect_size = 0; + continue; + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + break; + } + + snprintf(description, sizeof(description), "descriptor %u, offset 0x%06X%s", + (unsigned)(desc - mmap->descriptors) + 1, (int)offset, desc->ptr ? "" : " [no pointer]"); + + if (desc->ptr) { + desc_start = (uint8_t*)desc->ptr + desc->offset; + region_start = desc_start + offset; + } + else { + region_start = NULL; + } + + desc_size = desc->len - offset; + if (desc->disconnect && desc_size > desc->disconnect) { + /* if we need to extract a disconnect bit, the largest block we can read is up to + * the next time that bit flips */ + /* https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit */ + disconnect_size = (desc->disconnect & -((int)desc->disconnect)); + desc_size = disconnect_size - (real_address & (disconnect_size - 1)); + } + + if (console_region_size > desc_size) { + 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", + (unsigned)(real_address - console_region->real_address + console_region->start_address)); + rc_libretro_verbose(description); + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + console_region_size = 0; + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, desc_size, description); + console_region_size -= desc_size; + real_address += (unsigned)desc_size; + } + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, console_region_size, description); + console_region_size = 0; + } + } + } +} + +static uint32_t rc_libretro_memory_console_region_to_ram_type(uint8_t region_type) { + switch (region_type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return RETRO_MEMORY_SAVE_RAM; + case RC_MEMORY_TYPE_VIDEO_RAM: + return RETRO_MEMORY_VIDEO_RAM; + default: + break; + } + + return RETRO_MEMORY_SYSTEM_RAM; +} + +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]; + 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 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]; + if (rc_libretro_memory_console_region_to_ram_type(console_region2->type) == type) { + base_address = console_region2->start_address; + break; + } + } + offset = console_region->start_address - base_address; + + get_core_memory_info(type, &info); + + if (offset < info.size) { + info.size -= offset; + + if (info.data) { + snprintf(description, sizeof(description), "offset 0x%06X", (int)offset); + info.data += offset; + } + else { + snprintf(description, sizeof(description), "null filler"); + } + } + 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", (unsigned)console_region->start_address); + rc_libretro_verbose(description); + } + + info.data = NULL; + info.size = 0; + } + + if (console_region_size > info.size) { + /* want more than what is available, take what we can and null fill the rest */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, info.size, description); + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size - info.size, "null filler"); + } + else { + /* only take as much as we need */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, console_region_size, description); + } + } +} + +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, uint32_t console_id) { + 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; + uint32_t i; + + if (!regions) + return 0; + + memset(&new_regions, 0, sizeof(new_regions)); + + if (console_regions == NULL || console_regions->num_regions == 0) + rc_libretro_memory_init_without_regions(&new_regions, get_core_memory_info); + else if (mmap && mmap->num_descriptors != 0) + rc_libretro_memory_init_from_memory_map(&new_regions, mmap, console_regions); + else + rc_libretro_memory_init_from_unmapped_memory(&new_regions, get_core_memory_info, console_regions); + + /* determine if any valid regions were found */ + for (i = 0; i < new_regions.count; i++) { + if (new_regions.data[i]) { + has_valid_region = 1; + break; + } + } + + memcpy(regions, &new_regions, sizeof(*regions)); + return has_valid_region; +} + +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions) { + memset(regions, 0, sizeof(*regions)); +} + +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) { + char image_path[1024]; + char* m3u_contents; + char* ptr; + int64_t file_len; + void* file_handle; + int index = 0; + + memset(hash_set, 0, sizeof(*hash_set)); + + if (!rc_path_compare_extension(m3u_path, "m3u")) + return; + + file_handle = rc_file_open(m3u_path); + if (!file_handle) { + rc_hash_error("Could not open playlist"); + return; + } + + rc_file_seek(file_handle, 0, SEEK_END); + file_len = rc_file_tell(file_handle); + rc_file_seek(file_handle, 0, SEEK_SET); + + m3u_contents = (char*)malloc((size_t)file_len + 1); + if (m3u_contents) { + rc_file_read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + rc_file_close(file_handle); + + ptr = m3u_contents; + do + { + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') { + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) { + /* 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; + } + + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; + + } while (*ptr); + + free(m3u_contents); + } + + if (hash_set->entries_count > 0) { + /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by + * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */ + if (!get_image_path(index - 1, image_path, sizeof(image_path))) + hash_set->entries_count = 0; + } +} + +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { + if (hash_set->entries) + free(hash_set->entries); + memset(hash_set, 0, sizeof(*hash_set)); +} + +static uint32_t rc_libretro_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; +} + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + 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;; + + if (path_djb2) { + /* attempt to match the path */ + for (scan = hash_set->entries; scan < stop; ++scan) { + if (scan->path_djb2 == path_djb2) { + entry = scan; + break; + } + } + } + + if (!entry) + { + /* entry not found, allocate a new one */ + if (hash_set->entries_size == 0) { + hash_set->entries_size = 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*) + malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + else if (hash_set->entries_count == hash_set->entries_size) { + hash_set->entries_size += 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries, + hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + + if (hash_set->entries == NULL) /* unexpected, but better than crashing */ + return; + + entry = hash_set->entries + hash_set->entries_count++; + } + + /* update the entry */ + entry->path_djb2 = path_djb2; + entry->game_id = game_id; + memcpy(entry->hash, hash, sizeof(entry->hash)); +} + +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* 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) { + if (scan->path_djb2 == path_djb2) + return scan->hash; + } + + return NULL; +} + +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash) +{ + 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) { + if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0) + return scan->game_id; + } + + return 0; +} diff --git a/3rdparty/rcheevos/src/rc_libretro.h b/3rdparty/rcheevos/src/rc_libretro.h new file mode 100644 index 0000000000..66ab9a3528 --- /dev/null +++ b/3rdparty/rcheevos/src/rc_libretro.h @@ -0,0 +1,93 @@ +#ifndef RC_LIBRETRO_H +#define RC_LIBRETRO_H + +#include "rc_export.h" + +/* this file comes from the libretro repository, which is not an explicit submodule. + * the integration must set up paths appropriately to find it. */ +#include + +#include +#include + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Disallowed Settings | +\*****************************************************************************/ + +typedef struct rc_disallowed_setting_t +{ + const char* setting; + const char* value; +} rc_disallowed_setting_t; + +RC_EXPORT const rc_disallowed_setting_t* RC_CCONV rc_libretro_get_disallowed_settings(const char* library_name); +RC_EXPORT int RC_CCONV rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); +RC_EXPORT int RC_CCONV rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id); + +/*****************************************************************************\ +| Memory Mapping | +\*****************************************************************************/ + +/* specifies a function to call for verbose logging */ +typedef void (RC_CCONV *rc_libretro_message_callback)(const char*); +RC_EXPORT void RC_CCONV rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback); + +#define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 +typedef struct rc_libretro_memory_regions_t +{ + uint8_t* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t total_size; + uint32_t count; +} rc_libretro_memory_regions_t; + +typedef struct rc_libretro_core_memory_info_t +{ + uint8_t* data; + size_t size; +} rc_libretro_core_memory_info_t; + +typedef void (RC_CCONV *rc_libretro_get_core_memory_info_func)(uint32_t id, rc_libretro_core_memory_info_t* info); + +RC_EXPORT int RC_CCONV 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, uint32_t console_id); +RC_EXPORT void RC_CCONV rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); + +RC_EXPORT uint8_t* RC_CCONV rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address); +RC_EXPORT uint8_t* RC_CCONV rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail); +RC_EXPORT uint32_t RC_CCONV rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, uint8_t* buffer, uint32_t num_bytes); + +/*****************************************************************************\ +| Disk Identification | +\*****************************************************************************/ + +typedef struct rc_libretro_hash_entry_t +{ + uint32_t path_djb2; + uint32_t game_id; + char hash[33]; +} rc_libretro_hash_entry_t; + +typedef struct rc_libretro_hash_set_t +{ + struct rc_libretro_hash_entry_t* entries; + uint16_t entries_count; + uint16_t entries_size; +} rc_libretro_hash_set_t; + +typedef int (RC_CCONV *rc_libretro_get_image_path_func)(uint32_t index, char* buffer, size_t buffer_size); + +RC_EXPORT void RC_CCONV 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); +RC_EXPORT void RC_CCONV rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); + +RC_EXPORT void RC_CCONV rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, uint32_t game_id, const char hash[33]); +RC_EXPORT const char* RC_CCONV rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); +RC_EXPORT int RC_CCONV rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); + +RC_END_C_DECLS + +#endif /* RC_LIBRETRO_H */ diff --git a/3rdparty/rcheevos/src/rc_util.c b/3rdparty/rcheevos/src/rc_util.c new file mode 100644 index 0000000000..fa369a3b0d --- /dev/null +++ b/3rdparty/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->start); + 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/3rdparty/rcheevos/src/rc_version.c b/3rdparty/rcheevos/src/rc_version.c new file mode 100644 index 0000000000..a29d0a5f52 --- /dev/null +++ b/3rdparty/rcheevos/src/rc_version.c @@ -0,0 +1,11 @@ +#include "rc_version.h" + +uint32_t rc_version(void) +{ + return RCHEEVOS_VERSION; +} + +const char* rc_version_string(void) +{ + return RCHEEVOS_VERSION_STRING; +} diff --git a/3rdparty/rcheevos/src/rc_version.h b/3rdparty/rcheevos/src/rc_version.h new file mode 100644 index 0000000000..daf57e1cb5 --- /dev/null +++ b/3rdparty/rcheevos/src/rc_version.h @@ -0,0 +1,32 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#include "rc_export.h" + +#include + +RC_BEGIN_C_DECLS + +#define RCHEEVOS_VERSION_MAJOR 11 +#define RCHEEVOS_VERSION_MINOR 1 +#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 + +RC_EXPORT uint32_t rc_version(void); +RC_EXPORT const char* rc_version_string(void); + +RC_END_C_DECLS + +#endif /* RC_VERSION_H */ diff --git a/3rdparty/rcheevos/src/rcheevos/alloc.c b/3rdparty/rcheevos/src/rcheevos/alloc.c new file mode 100644 index 0000000000..0aa4e5cb5f --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/alloc.c @@ -0,0 +1,118 @@ +#include "rc_internal.h" + +#include +#include + +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) +{ + void* data; + + /* if we have a real buffer, then allocate the data there */ + if (pointer) + return rc_alloc(pointer, offset, size, alignment, NULL, scratch_object_pointer_offset); + + /* update how much space will be required in the real buffer */ + { + const int32_t aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); + *offset += (aligned_offset - *offset); + *offset += size; + } + + /* find a scratch buffer to hold the temporary data */ + data = rc_buffer_alloc(&scratch->buffer, size); + if (!data) { + *offset = RC_OUT_OF_MEMORY; + return NULL; + } + + return data; +} + +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); + + if (pointer != 0) { + /* valid buffer, grab the next chunk */ + ptr = (void*)((char*)pointer + *offset); + } + else if (scratch != 0 && scratch_object_pointer_offset < sizeof(scratch->objs)) { + /* only allocate one instance of each object type (indentified by scratch_object_pointer_offset) */ + void** scratch_object_pointer = (void**)((char*)&scratch->objs + scratch_object_pointer_offset); + ptr = *scratch_object_pointer; + if (!ptr) { + int32_t used; + ptr = *scratch_object_pointer = rc_alloc_scratch(NULL, &used, size, alignment, scratch, -1); + } + } + else { + /* nowhere to get memory from, return NULL */ + ptr = NULL; + } + + *offset += size; + return ptr; +} + +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; + while (*next) { + int diff = strncmp(text, (*next)->value, length); + if (diff == 0) { + diff = (*next)->value[length]; + if (diff == 0) + return (*next)->value; + } + + if (diff < 0) + next = &(*next)->left; + else + next = &(*next)->right; + } + + *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, (uint32_t)length + 1, RC_ALIGNOF(char), &parse->scratch, -1); + + if (!ptr || !*next) { + if (parse->offset >= 0) + parse->offset = RC_OUT_OF_MEMORY; + + return NULL; + } + + memcpy(ptr, text, length); + ptr[length] = '\0'; + + (*next)->left = NULL; + (*next)->right = NULL; + (*next)->value = ptr; + + return ptr; +} + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer, lua_State* L, int funcs_ndx) +{ + /* could use memset here, but rc_parse_state_t contains a 512 byte buffer that doesn't need to be initialized */ + parse->offset = 0; + parse->L = L; + parse->funcs_ndx = funcs_ndx; + parse->buffer = buffer; + parse->scratch.strings = NULL; + rc_buffer_init(&parse->scratch.buffer); + memset(&parse->scratch.objs, 0, sizeof(parse->scratch.objs)); + parse->first_memref = 0; + parse->variables = 0; + parse->measured_target = 0; + parse->lines_read = 0; + parse->has_required_hits = 0; + parse->measured_as_percent = 0; +} + +void rc_destroy_parse_state(rc_parse_state_t* parse) +{ + rc_buffer_destroy(&parse->scratch.buffer); +} diff --git a/3rdparty/rcheevos/src/rcheevos/condition.c b/3rdparty/rcheevos/src/rcheevos/condition.c new file mode 100644 index 0000000000..236dff3734 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/condition.c @@ -0,0 +1,555 @@ +#include "rc_internal.h" + +#include +#include + +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; + case RC_OPERATOR_LT: return value1 < value2; + case RC_OPERATOR_LE: return value1 <= value2; + case RC_OPERATOR_GT: return value1 > value2; + case RC_OPERATOR_GE: return value1 >= value2; + default: return 1; + } +} + +static char rc_condition_determine_comparator(const rc_condition_t* self) { + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + break; + + default: + /* not a comparison. should not be getting compared. but if it is, legacy behavior was to return 1 */ + return RC_PROCESSING_COMPARE_ALWAYS_TRUE; + } + + if ((self->operand1.type == RC_OPERAND_ADDRESS || self->operand1.type == RC_OPERAND_DELTA) && + !self->operand1.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand1)) { + /* left side is an integer memory reference */ + int needs_translate = (self->operand1.size != self->operand1.value.memref->value.size); + + if (self->operand2.type == RC_OPERAND_CONST) { + /* right side is a constant */ + if (self->operand1.type == RC_OPERAND_ADDRESS) + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_CONST; + + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_CONST; + } + else if ((self->operand2.type == RC_OPERAND_ADDRESS || self->operand2.type == RC_OPERAND_DELTA) && + !self->operand2.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand2)) { + /* right side is an integer memory reference */ + const int is_same_memref = (self->operand1.value.memref == self->operand2.value.memref); + needs_translate |= (self->operand2.size != self->operand2.value.memref->value.size); + + if (self->operand1.type == RC_OPERAND_ADDRESS) { + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref && !needs_translate) { + /* comparing a memref to itself, will evaluate to a constant */ + return rc_test_condition_compare(0, 0, self->oper) ? RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF; + } + + assert(self->operand2.type == RC_OPERAND_DELTA); + + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_DELTA; + } + } + else { + assert(self->operand1.type == RC_OPERAND_DELTA); + + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_MEMREF; + } + } + } + } + } + + if (self->operand1.type == RC_OPERAND_CONST && self->operand2.type == RC_OPERAND_CONST) { + /* comparing constants will always generate a constant result */ + return rc_test_condition_compare(self->operand1.value.num, self->operand2.value.num, self->oper) ? + RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return RC_PROCESSING_COMPARE_DEFAULT; +} + +static int rc_parse_operator(const char** memaddr) { + const char* oper = *memaddr; + + switch (*oper) { + case '=': + ++(*memaddr); + (*memaddr) += (**memaddr == '='); + return RC_OPERATOR_EQ; + + case '!': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_NE; + } + /* fall through */ + default: + return RC_INVALID_OPERATOR; + + case '<': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_LE; + } + + ++(*memaddr); + return RC_OPERATOR_LT; + + case '>': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_GE; + } + + ++(*memaddr); + return RC_OPERATOR_GT; + + case '*': + ++(*memaddr); + return RC_OPERATOR_MULT; + + case '/': + ++(*memaddr); + return RC_OPERATOR_DIV; + + case '&': + ++(*memaddr); + return RC_OPERATOR_AND; + + case '^': + ++(*memaddr); + return RC_OPERATOR_XOR; + + case '\0':/* end of string */ + case '_': /* next condition */ + case 'S': /* next condset */ + case ')': /* end of macro */ + case '$': /* maximum of values */ + /* valid condition separator, condition may not have an operator */ + return RC_OPERATOR_NONE; + } +} + +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; + int can_modify = 0; + + aux = *memaddr; + self = RC_ALLOC(rc_condition_t, parse); + self->current_hits = 0; + self->is_true = 0; + self->pause = 0; + self->optimized_comparator = RC_PROCESSING_COMPARE_DEFAULT; + + if (*aux != 0 && aux[1] == ':') { + switch (*aux) { + case 'p': case 'P': self->type = RC_CONDITION_PAUSE_IF; break; + case 'r': case 'R': self->type = RC_CONDITION_RESET_IF; break; + case 'a': case 'A': self->type = RC_CONDITION_ADD_SOURCE; can_modify = 1; break; + case 'b': case 'B': self->type = RC_CONDITION_SUB_SOURCE; can_modify = 1; break; + case 'c': case 'C': self->type = RC_CONDITION_ADD_HITS; break; + case 'd': case 'D': self->type = RC_CONDITION_SUB_HITS; break; + case 'n': case 'N': self->type = RC_CONDITION_AND_NEXT; break; + case 'o': case 'O': self->type = RC_CONDITION_OR_NEXT; break; + case 'm': case 'M': self->type = RC_CONDITION_MEASURED; break; + case 'q': case 'Q': self->type = RC_CONDITION_MEASURED_IF; break; + case 'i': case 'I': self->type = RC_CONDITION_ADD_ADDRESS; can_modify = 1; break; + case 't': case 'T': self->type = RC_CONDITION_TRIGGER; break; + case 'z': case 'Z': self->type = RC_CONDITION_RESET_NEXT_IF; break; + case 'g': case 'G': + parse->measured_as_percent = 1; + self->type = RC_CONDITION_MEASURED; + break; + /* e f h j k l s u v w x y */ + default: parse->offset = RC_INVALID_CONDITION_TYPE; return 0; + } + + aux += 2; + } + else { + self->type = RC_CONDITION_STANDARD; + } + + result = rc_parse_operand(&self->operand1, &aux, is_indirect, parse); + if (result < 0) { + parse->offset = result; + return 0; + } + + result = rc_parse_operator(&aux); + if (result < 0) { + parse->offset = result; + return 0; + } + + self->oper = (char)result; + switch (self->oper) { + case RC_OPERATOR_NONE: + /* non-modifying statements must have a second operand */ + if (!can_modify) { + /* measured does not require a second operand when used in a value */ + if (self->type != RC_CONDITION_MEASURED) { + parse->offset = RC_INVALID_OPERATOR; + return 0; + } + } + + /* provide dummy operand of '1' and no required hits */ + self->operand2.type = RC_OPERAND_CONST; + self->operand2.value.num = 1; + self->required_hits = 0; + *memaddr = aux; + return self; + + case RC_OPERATOR_MULT: + case RC_OPERATOR_DIV: + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + /* modifying operators are only valid on modifying statements */ + if (can_modify) + break; + /* fallthrough */ + + default: + /* comparison operators are not valid on modifying statements */ + if (can_modify) { + switch (self->type) { + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_ADDRESS: + /* prevent parse errors on legacy achievements where a condition was present before changing the type */ + self->oper = RC_OPERATOR_NONE; + break; + + default: + parse->offset = RC_INVALID_OPERATOR; + return 0; + } + } + break; + } + + result = rc_parse_operand(&self->operand2, &aux, is_indirect, parse); + if (result < 0) { + parse->offset = result; + return 0; + } + + if (self->oper == RC_OPERATOR_NONE) { + /* if operator is none, explicitly clear out the right side */ + self->operand2.type = RC_OPERAND_CONST; + self->operand2.value.num = 0; + } + + if (*aux == '(') { + char* end; + self->required_hits = (unsigned)strtoul(++aux, &end, 10); + + if (end == aux || *end != ')') { + parse->offset = RC_INVALID_REQUIRED_HITS; + return 0; + } + + /* if operator is none, explicitly clear out the required hits */ + if (self->oper == RC_OPERATOR_NONE) + self->required_hits = 0; + else + parse->has_required_hits = 1; + + aux = end + 1; + } + else if (*aux == '.') { + char* end; + self->required_hits = (unsigned)strtoul(++aux, &end, 10); + + if (end == aux || *end != '.') { + parse->offset = RC_INVALID_REQUIRED_HITS; + return 0; + } + + /* if operator is none, explicitly clear out the required hits */ + if (self->oper == RC_OPERATOR_NONE) + self->required_hits = 0; + else + parse->has_required_hits = 1; + + aux = end + 1; + } + else { + self->required_hits = 0; + } + + if (parse->buffer != 0) + self->optimized_comparator = rc_condition_determine_comparator(self); + + *memaddr = aux; + return self; +} + +int rc_condition_is_combining(const rc_condition_t* self) { + switch (self->type) { + case RC_CONDITION_STANDARD: + case RC_CONDITION_PAUSE_IF: + case RC_CONDITION_RESET_IF: + case RC_CONDITION_MEASURED_IF: + case RC_CONDITION_TRIGGER: + case RC_CONDITION_MEASURED: + return 0; + + default: + return 1; + } +} + +static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { + 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 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 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); +} + +static int rc_test_condition_compare_memref_to_delta(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->value, memref->prior, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->prior, memref->value, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const uint32_t value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +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 uint32_t value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref_transformed(rc_condition_t* self) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = self->operand2.value.memref->value.value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->prior; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->prior; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { + rc_typed_value_t value1, value2; + + if (eval_state->add_value.type != RC_VALUE_TYPE_NONE) { + /* if there's an accumulator, we can't use the optimized comparators */ + rc_evaluate_operand(&value1, &self->operand1, eval_state); + rc_typed_value_add(&value1, &eval_state->add_value); + } else { + /* use an optimized comparator whenever possible */ + switch (self->optimized_comparator) { + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST: + return rc_test_condition_compare_memref_to_const(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA: + return rc_test_condition_compare_memref_to_delta(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF: + return rc_test_condition_compare_memref_to_memref(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST: + return rc_test_condition_compare_delta_to_const(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF: + return rc_test_condition_compare_delta_to_memref(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_memref_to_const_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED: + return rc_test_condition_compare_memref_to_delta_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_memref_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_delta_to_const_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_delta_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_ALWAYS_TRUE: + return 1; + case RC_PROCESSING_COMPARE_ALWAYS_FALSE: + return 0; + default: + rc_evaluate_operand(&value1, &self->operand1, eval_state); + break; + } + } + + rc_evaluate_operand(&value2, &self->operand2, eval_state); + + return rc_typed_value_compare(&value1, &value2, self->oper); +} + +void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state) { + rc_typed_value_t amount; + + rc_evaluate_operand(value, &self->operand1, eval_state); + rc_evaluate_operand(&amount, &self->operand2, eval_state); + + switch (self->oper) { + case RC_OPERATOR_MULT: + rc_typed_value_multiply(value, &amount); + break; + + case RC_OPERATOR_DIV: + rc_typed_value_divide(value, &amount); + break; + + case RC_OPERATOR_AND: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 &= amount.value.u32; + break; + + case RC_OPERATOR_XOR: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 ^= amount.value.u32; + break; + } +} diff --git a/3rdparty/rcheevos/src/rcheevos/condset.c b/3rdparty/rcheevos/src/rcheevos/condset.c new file mode 100644 index 0000000000..23a0e30e1f --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/condset.c @@ -0,0 +1,437 @@ +#include "rc_internal.h" + +#include /* memcpy */ + +static void rc_update_condition_pause(rc_condition_t* condition) { + rc_condition_t* subclause = condition; + + while (condition) { + if (condition->type == RC_CONDITION_PAUSE_IF) { + while (subclause != condition) { + subclause->pause = 1; + subclause = subclause->next; + } + condition->pause = 1; + } + else { + condition->pause = 0; + } + + if (!rc_condition_is_combining(condition)) + subclause = condition->next; + + condition = condition->next; + } +} + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, int is_value) { + rc_condset_t* self; + rc_condition_t** next; + int in_add_address; + uint32_t measured_target = 0; + + self = RC_ALLOC(rc_condset_t, parse); + self->has_pause = self->is_paused = self->has_indirect_memrefs = 0; + next = &self->conditions; + + if (**memaddr == 'S' || **memaddr == 's' || !**memaddr) { + /* empty group - editor allows it, so we have to support it */ + *next = 0; + return self; + } + + in_add_address = 0; + for (;;) { + *next = rc_parse_condition(memaddr, parse, in_add_address); + + if (parse->offset < 0) { + return 0; + } + + if ((*next)->oper == RC_OPERATOR_NONE) { + switch ((*next)->type) { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + /* these conditions don't require a right hand size (implied *1) */ + break; + + case RC_CONDITION_MEASURED: + /* right hand side is not required when Measured is used in a value */ + if (is_value) + break; + /* fallthrough */ /* to default */ + + default: + parse->offset = RC_INVALID_OPERATOR; + return 0; + } + } + + self->has_pause |= (*next)->type == RC_CONDITION_PAUSE_IF; + in_add_address = (*next)->type == RC_CONDITION_ADD_ADDRESS; + self->has_indirect_memrefs |= in_add_address; + + switch ((*next)->type) { + case RC_CONDITION_MEASURED: + if (measured_target != 0) { + /* multiple Measured flags cannot exist in the same group */ + parse->offset = RC_MULTIPLE_MEASURED; + return 0; + } + else if (is_value) { + measured_target = (unsigned)-1; + switch ((*next)->oper) + { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_DIV: + case RC_OPERATOR_MULT: + case RC_OPERATOR_NONE: + /* measuring value. leave required_hits at 0 */ + break; + + default: + /* comparison operator, measuring hits. set required_hits to MAX_INT */ + (*next)->required_hits = measured_target; + break; + } + } + else if ((*next)->required_hits != 0) { + measured_target = (*next)->required_hits; + } + else if ((*next)->operand2.type == RC_OPERAND_CONST) { + measured_target = (*next)->operand2.value.num; + } + else if ((*next)->operand2.type == RC_OPERAND_FP) { + measured_target = (unsigned)(*next)->operand2.value.dbl; + } + else { + parse->offset = RC_INVALID_MEASURED_TARGET; + return 0; + } + + if (parse->measured_target && measured_target != parse->measured_target) { + /* multiple Measured flags in separate groups must have the same target */ + parse->offset = RC_MULTIPLE_MEASURED; + return 0; + } + + parse->measured_target = measured_target; + break; + + case RC_CONDITION_STANDARD: + case RC_CONDITION_TRIGGER: + /* these flags are not allowed in value expressions */ + if (is_value) { + parse->offset = RC_INVALID_VALUE_FLAG; + return 0; + } + break; + + default: + break; + } + + next = &(*next)->next; + + if (**memaddr != '_') { + break; + } + + (*memaddr)++; + } + + *next = 0; + + if (parse->buffer != 0) + rc_update_condition_pause(self->conditions); + + return self; +} + +static void rc_condset_update_indirect_memrefs(rc_condition_t* condition, int processing_pause, rc_eval_state_t* eval_state) { + for (; condition != 0; condition = condition->next) { + if (condition->pause != processing_pause) + continue; + + if (condition->type == RC_CONDITION_ADD_ADDRESS) { + rc_typed_value_t value; + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + eval_state->add_address = value.value.u32; + continue; + } + + /* call rc_get_memref_value to update the indirect memrefs. it won't do anything with non-indirect + * memrefs and avoids a second check of is_indirect. also, we ignore the response, so it doesn't + * matter what operand type we pass. assume RC_OPERAND_ADDRESS is the quickest. */ + if (rc_operand_is_memref(&condition->operand1)) + rc_get_memref_value(condition->operand1.value.memref, RC_OPERAND_ADDRESS, eval_state); + + if (rc_operand_is_memref(&condition->operand2)) + rc_get_memref_value(condition->operand2.value.memref, RC_OPERAND_ADDRESS, eval_state); + + eval_state->add_address = 0; + } +} + +static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc_eval_state_t* eval_state) { + rc_condition_t* condition; + 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; + uint32_t total_hits; + + measured_value.type = RC_VALUE_TYPE_NONE; + measured_from_hits = 0; + can_measure = 1; + total_hits = 0; + + eval_state->primed = 1; + set_valid = 1; + and_next = 1; + or_next = 0; + reset_next = 0; + eval_state->add_value.type = RC_VALUE_TYPE_NONE; + eval_state->add_hits = eval_state->add_address = 0; + + for (condition = self->conditions; condition != 0; condition = condition->next) { + if (condition->pause != processing_pause) + continue; + + /* STEP 1: process modifier conditions */ + switch (condition->type) { + case RC_CONDITION_ADD_SOURCE: + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_add(&eval_state->add_value, &value); + eval_state->add_address = 0; + continue; + + case RC_CONDITION_SUB_SOURCE: + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_negate(&value); + rc_typed_value_add(&eval_state->add_value, &value); + eval_state->add_address = 0; + continue; + + case RC_CONDITION_ADD_ADDRESS: + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + eval_state->add_address = value.value.u32; + continue; + + case RC_CONDITION_MEASURED: + if (condition->required_hits == 0 && can_measure) { + /* Measured condition without a hit target measures the value of the left operand */ + rc_evaluate_condition_value(&measured_value, condition, eval_state); + rc_typed_value_add(&measured_value, &eval_state->add_value); + } + break; + + default: + break; + } + + /* STEP 2: evaluate the current condition */ + condition->is_true = (char)rc_test_condition(condition, eval_state); + eval_state->add_value.type = RC_VALUE_TYPE_NONE; + eval_state->add_address = 0; + + /* apply logic flags and reset them for the next condition */ + cond_valid = condition->is_true; + cond_valid &= and_next; + cond_valid |= or_next; + and_next = 1; + or_next = 0; + + if (reset_next) { + /* previous ResetNextIf resets the hit count on this condition and prevents it from being true */ + if (condition->current_hits) + eval_state->was_cond_reset = 1; + + condition->current_hits = 0; + cond_valid = 0; + } + else if (cond_valid) { + /* true conditions should update hit count */ + eval_state->has_hits = 1; + + if (condition->required_hits == 0) { + /* no target hit count, just keep tallying */ + ++condition->current_hits; + } + else if (condition->current_hits < condition->required_hits) { + /* target hit count hasn't been met, tally and revalidate - only true if hit count becomes met */ + ++condition->current_hits; + cond_valid = (condition->current_hits == condition->required_hits); + } + else { + /* target hit count has been met, do nothing */ + } + } + else if (condition->current_hits > 0) { + /* target has been true in the past, if the hit target is met, consider it true now */ + eval_state->has_hits = 1; + cond_valid = (condition->current_hits == condition->required_hits); + } + + /* STEP 3: handle logic flags */ + switch (condition->type) { + case RC_CONDITION_ADD_HITS: + eval_state->add_hits += condition->current_hits; + reset_next = 0; /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + continue; + + case RC_CONDITION_SUB_HITS: + eval_state->add_hits -= condition->current_hits; + reset_next = 0; /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + continue; + + case RC_CONDITION_RESET_NEXT_IF: + reset_next = cond_valid; + continue; + + case RC_CONDITION_AND_NEXT: + and_next = cond_valid; + continue; + + case RC_CONDITION_OR_NEXT: + or_next = cond_valid; + continue; + + default: + break; + } + + /* reset logic flags for next condition */ + reset_next = 0; + + /* STEP 4: calculate total hits */ + total_hits = condition->current_hits; + + if (eval_state->add_hits) { + if (condition->required_hits != 0) { + /* if the condition has a target hit count, we have to recalculate cond_valid including the AddHits counter */ + const int signed_hits = (int)condition->current_hits + eval_state->add_hits; + total_hits = (signed_hits >= 0) ? (unsigned)signed_hits : 0; + cond_valid = (total_hits >= condition->required_hits); + } + else { + /* no target hit count. we can't tell if the add_hits value is from this frame or not, so ignore it. + complex condition will only be true if the current condition is true */ + } + + eval_state->add_hits = 0; + } + + /* STEP 5: handle special flags */ + switch (condition->type) { + case RC_CONDITION_PAUSE_IF: + /* as soon as we find a PauseIf that evaluates to true, stop processing the rest of the group */ + if (cond_valid) { + /* indirect memrefs are not updated as part of the rc_update_memref_values call. + * an active pause aborts processing of the remaining part of the pause subset and the entire non-pause subset. + * if the set has any indirect memrefs, manually update them now so the deltas are correct */ + if (self->has_indirect_memrefs) { + /* first, update any indirect memrefs in the remaining part of the pause subset */ + rc_condset_update_indirect_memrefs(condition->next, 1, eval_state); + + /* then, update all indirect memrefs in the non-pause subset */ + rc_condset_update_indirect_memrefs(self->conditions, 0, eval_state); + } + + return 1; + } + + /* if we make it to the end of the function, make sure we indicate that nothing matched. if we do find + a later PauseIf match, it'll automatically return true via the previous condition. */ + set_valid = 0; + + if (condition->required_hits == 0) { + /* PauseIf didn't evaluate true, and doesn't have a HitCount, reset the HitCount to indicate the condition didn't match */ + condition->current_hits = 0; + } + else { + /* PauseIf has a HitCount that hasn't been met, ignore it for now. */ + } + + continue; + + case RC_CONDITION_RESET_IF: + if (cond_valid) { + eval_state->was_reset = 1; /* let caller know to reset all hit counts */ + set_valid = 0; /* cannot be valid if we've hit a reset condition */ + } + continue; + + case RC_CONDITION_MEASURED: + if (condition->required_hits != 0) { + /* if there's a hit target, capture the current hits for recording Measured value later */ + measured_from_hits = 1; + if (can_measure) { + measured_value.value.u32 = total_hits; + measured_value.type = RC_VALUE_TYPE_UNSIGNED; + } + } + break; + + case RC_CONDITION_MEASURED_IF: + if (!cond_valid) { + measured_value.value.u32 = 0; + measured_value.type = RC_VALUE_TYPE_UNSIGNED; + can_measure = 0; + } + break; + + case RC_CONDITION_TRIGGER: + /* update truthiness of set, but do not update truthiness of primed state */ + set_valid &= cond_valid; + continue; + + default: + break; + } + + /* STEP 5: update overall truthiness of set and primed state */ + eval_state->primed &= cond_valid; + set_valid &= cond_valid; + } + + if (measured_value.type != RC_VALUE_TYPE_NONE) { + /* if no previous Measured value was captured, or the new one is greater, keep the new one */ + if (eval_state->measured_value.type == RC_VALUE_TYPE_NONE || + rc_typed_value_compare(&measured_value, &eval_state->measured_value, RC_OPERATOR_GT)) { + memcpy(&eval_state->measured_value, &measured_value, sizeof(measured_value)); + eval_state->measured_from_hits = (char)measured_from_hits; + } + } + + return set_valid; +} + +int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state) { + if (self->conditions == 0) { + /* important: empty group must evaluate true */ + return 1; + } + + if (self->has_pause) { + /* one or more Pause conditions exists, if any of them are true, stop processing this group */ + self->is_paused = (char)rc_test_condset_internal(self, 1, eval_state); + if (self->is_paused) { + eval_state->primed = 0; + return 0; + } + } + + return rc_test_condset_internal(self, 0, eval_state); +} + +void rc_reset_condset(rc_condset_t* self) { + rc_condition_t* condition; + + for (condition = self->conditions; condition != 0; condition = condition->next) { + condition->current_hits = 0; + } +} diff --git a/3rdparty/rcheevos/src/rcheevos/consoleinfo.c b/3rdparty/rcheevos/src/rcheevos/consoleinfo.c new file mode 100644 index 0000000000..427db73b22 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/consoleinfo.c @@ -0,0 +1,1094 @@ +#include "rc_consoles.h" + +#include + +const char* rc_console_name(uint32_t console_id) +{ + switch (console_id) + { + case RC_CONSOLE_3DO: + return "3DO"; + + case RC_CONSOLE_AMIGA: + return "Amiga"; + + case RC_CONSOLE_AMSTRAD_PC: + return "Amstrad CPC"; + + case RC_CONSOLE_APPLE_II: + return "Apple II"; + + case RC_CONSOLE_ARCADE: + return "Arcade"; + + case RC_CONSOLE_ARCADIA_2001: + return "Arcadia 2001"; + + case RC_CONSOLE_ARDUBOY: + return "Arduboy"; + + case RC_CONSOLE_ATARI_2600: + return "Atari 2600"; + + case RC_CONSOLE_ATARI_5200: + return "Atari 5200"; + + case RC_CONSOLE_ATARI_7800: + return "Atari 7800"; + + case RC_CONSOLE_ATARI_JAGUAR: + return "Atari Jaguar"; + + case RC_CONSOLE_ATARI_JAGUAR_CD: + return "Atari Jaguar CD"; + + case RC_CONSOLE_ATARI_LYNX: + return "Atari Lynx"; + + case RC_CONSOLE_ATARI_ST: + return "Atari ST"; + + case RC_CONSOLE_CASSETTEVISION: + return "CassetteVision"; + + case RC_CONSOLE_CDI: + return "CD-I"; + + case RC_CONSOLE_COLECOVISION: + return "ColecoVision"; + + case RC_CONSOLE_COMMODORE_64: + return "Commodore 64"; + + case RC_CONSOLE_DREAMCAST: + return "Dreamcast"; + + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + return "Elektor TV Games Computer"; + + case RC_CONSOLE_EVENTS: + return "Events"; + + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return "Fairchild Channel F"; + + case RC_CONSOLE_FM_TOWNS: + return "FM Towns"; + + case RC_CONSOLE_GAME_AND_WATCH: + return "Game & Watch"; + + case RC_CONSOLE_GAMEBOY: + return "GameBoy"; + + case RC_CONSOLE_GAMEBOY_ADVANCE: + return "GameBoy Advance"; + + case RC_CONSOLE_GAMEBOY_COLOR: + return "GameBoy Color"; + + case RC_CONSOLE_GAMECUBE: + return "GameCube"; + + case RC_CONSOLE_GAME_GEAR: + return "Game Gear"; + + case RC_CONSOLE_HUBS: + return "Hubs"; + + case RC_CONSOLE_INTELLIVISION: + return "Intellivision"; + + case RC_CONSOLE_INTERTON_VC_4000: + return "Interton VC 4000"; + + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + return "Magnavox Odyssey 2"; + + case RC_CONSOLE_MASTER_SYSTEM: + return "Master System"; + + case RC_CONSOLE_MEGA_DRIVE: + return "Sega Genesis"; + + case RC_CONSOLE_MEGADUCK: + return "Mega Duck"; + + case RC_CONSOLE_MS_DOS: + return "MS-DOS"; + + case RC_CONSOLE_MSX: + return "MSX"; + + case RC_CONSOLE_NEO_GEO_CD: + return "Neo Geo CD"; + + case RC_CONSOLE_NEOGEO_POCKET: + return "Neo Geo Pocket"; + + case RC_CONSOLE_NINTENDO: + return "Nintendo Entertainment System"; + + case RC_CONSOLE_NINTENDO_64: + return "Nintendo 64"; + + case RC_CONSOLE_NINTENDO_DS: + return "Nintendo DS"; + + case RC_CONSOLE_NINTENDO_DSI: + return "Nintendo DSi"; + + case RC_CONSOLE_NINTENDO_3DS: + return "Nintendo 3DS"; + + case RC_CONSOLE_NOKIA_NGAGE: + return "Nokia N-Gage"; + + case RC_CONSOLE_ORIC: + return "Oric"; + + case RC_CONSOLE_PC6000: + return "PC-6000"; + + case RC_CONSOLE_PC8800: + return "PC-8000/8800"; + + case RC_CONSOLE_PC9800: + return "PC-9800"; + + case RC_CONSOLE_PCFX: + return "PC-FX"; + + case RC_CONSOLE_PC_ENGINE: + return "PC Engine"; + + case RC_CONSOLE_PC_ENGINE_CD: + return "PC Engine CD"; + + case RC_CONSOLE_PLAYSTATION: + return "PlayStation"; + + case RC_CONSOLE_PLAYSTATION_2: + return "PlayStation 2"; + + case RC_CONSOLE_PSP: + return "PlayStation Portable"; + + case RC_CONSOLE_POKEMON_MINI: + return "Pokemon Mini"; + + case RC_CONSOLE_SEGA_32X: + return "Sega 32X"; + + case RC_CONSOLE_SEGA_CD: + return "Sega CD"; + + case RC_CONSOLE_PICO: + return "Sega Pico"; + + case RC_CONSOLE_SATURN: + return "Sega Saturn"; + + case RC_CONSOLE_SG1000: + return "SG-1000"; + + case RC_CONSOLE_SHARPX1: + return "Sharp X1"; + + case RC_CONSOLE_STANDALONE: + return "Standalone"; + + case RC_CONSOLE_SUPER_NINTENDO: + return "Super Nintendo Entertainment System"; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return "Super CassetteVision"; + + case RC_CONSOLE_SUPERVISION: + return "Watara Supervision"; + + case RC_CONSOLE_THOMSONTO8: + return "Thomson TO8"; + + case RC_CONSOLE_TI83: + return "TI-83"; + + case RC_CONSOLE_TIC80: + return "TIC-80"; + + case RC_CONSOLE_UZEBOX: + return "Uzebox"; + + case RC_CONSOLE_VECTREX: + return "Vectrex"; + + case RC_CONSOLE_VIC20: + return "VIC-20"; + + case RC_CONSOLE_VIRTUAL_BOY: + return "Virtual Boy"; + + case RC_CONSOLE_WASM4: + return "WASM-4"; + + case RC_CONSOLE_WII: + return "Wii"; + + case RC_CONSOLE_WII_U: + return "Wii-U"; + + case RC_CONSOLE_WONDERSWAN: + return "WonderSwan"; + + case RC_CONSOLE_X68K: + return "X68K"; + + case RC_CONSOLE_XBOX: + return "XBOX"; + + case RC_CONSOLE_ZEEBO: + return "Zeebo"; + + case RC_CONSOLE_ZX81: + return "ZX-81"; + + case RC_CONSOLE_ZX_SPECTRUM: + return "ZX Spectrum"; + + default: + return "Unknown"; + } +} + +/* ===== 3DO ===== */ +/* http://www.arcaderestoration.com/memorymap/48/3DO+Bios.aspx */ +/* NOTE: the Opera core attempts to expose the NVRAM as RETRO_SAVE_RAM, but the 3DO documentation + * says that applications should only access NVRAM through API calls as it's shared across mulitple + * games. This suggests that even if the core does expose it, it may change depending on which other + * games the user has played - so ignore it. + */ +static const rc_memory_region_t _rc_memory_regions_3do[] = { + { 0x000000U, 0x1FFFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_3do = { _rc_memory_regions_3do, 1 }; + +/* ===== Amiga ===== */ +/* http://amigadev.elowar.com/read/ADCD_2.1/Hardware_Manual_guide/node00D3.html */ +static const rc_memory_region_t _rc_memory_regions_amiga[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, /* 512KB main RAM */ + { 0x080000U, 0x0FFFFFU, 0x080000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, /* 512KB extended RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_amiga = { _rc_memory_regions_amiga, 2 }; + +/* ===== Amstrad CPC ===== */ +/* http://www.cpcalive.com/docs/amstrad_cpc_6128_memory_map.html */ +/* https://www.cpcwiki.eu/index.php/File:AWMG_page151.jpg */ +/* The original CPC only had 64KB of memory, but the newer model has 128KB (expandable to 576KB) */ +/* https://www.grimware.org/doku.php/documentations/devices/gatearraydo=export_xhtml#mmr */ +static const rc_memory_region_t _rc_memory_regions_amstrad_pc[] = { + { 0x000000U, 0x00003FU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Firmware" }, + { 0x000040U, 0x00B0FFU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00B100U, 0x00BFFFU, 0x00B100U, RC_MEMORY_TYPE_SYSTEM_RAM, "Stack and Firmware Data" }, + { 0x00C000U, 0x00FFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen Memory" }, + { 0x010000U, 0x08FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_amstrad_pc = { _rc_memory_regions_amstrad_pc, 5 }; + +/* ===== Apple II ===== */ +static const rc_memory_region_t _rc_memory_regions_appleii[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, + { 0x010000U, 0x01FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Auxillary RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_appleii = { _rc_memory_regions_appleii, 2 }; + +/* ===== Arcadia 2001 ===== */ +/* https://amigan.yatho.com/a-coding.txt */ +/* RAM banks 1 and 2 only exist on some variant models - no game actually uses them */ +static const rc_memory_region_t _rc_memory_regions_arcadia_2001[] = { + { 0x000000U, 0x0000FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 3 */ + { 0x000100U, 0x0001FFU, 0x001900U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, + { 0x000200U, 0x0002FFU, 0x001A00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 4 */ +}; +static const rc_memory_regions_t rc_memory_regions_arcadia_2001 = { _rc_memory_regions_arcadia_2001, 3 }; + +/* ===== Arduboy ===== */ +/* https://scienceprog.com/avr-microcontroller-memory-map/ (Atmega32) */ +static const rc_memory_region_t _rc_memory_regions_arduboy[] = { + { 0x000000U, 0x0000FFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Registers" }, + /* https://www.dailydot.com/debug/arduboy-kickstarter/ 2.5KB of RAM */ + /* https://github.com/buserror/simavr/blob/1d227277b3d0039f9faef9ea62880ca3051b14f8/simavr/cores/avr/iom32u4.h#L1444-L1445 */ + { 0x000100U, 0x000AFFU, 0x00000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* 1KB of EEPROM https://github.com/libretro/arduous/blob/93e1a6289b42ef48de1fcfb96443981725955ad0/src/arduous/arduous.cpp#L453-L455 + * https://github.com/buserror/simavr/blob/1d227277b3d0039f9faef9ea62880ca3051b14f8/simavr/cores/avr/iom32u4.h#L1450 */ + /* EEPROM has it's own addressing scheme starting at $0000. I've chosen to virtualize the address + * at $80000000 to avoid a conflict */ + { 0x000B00U, 0x000EFFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "EEPROM" } +}; +static const rc_memory_regions_t rc_memory_regions_arduboy = { _rc_memory_regions_arduboy, 3 }; + +/* ===== Atari 2600 ===== */ +static const rc_memory_region_t _rc_memory_regions_atari2600[] = { + { 0x000000U, 0x00007FU, 0x000080U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari2600 = { _rc_memory_regions_atari2600, 1 }; + +/* ===== Atari 7800 ===== */ +/* http://www.atarihq.com/danb/files/78map.txt */ +/* http://pdf.textfiles.com/technical/7800_devkit.pdf */ +static const rc_memory_region_t _rc_memory_regions_atari7800[] = { + { 0x000000U, 0x0017FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware Interface" }, + { 0x001800U, 0x0027FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x002800U, 0x002FFFU, 0x002800U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x003000U, 0x0037FFU, 0x003000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x003800U, 0x003FFFU, 0x003800U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x008000U, 0x00FFFFU, 0x008000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari7800 = { _rc_memory_regions_atari7800, 7 }; + +/* ===== Atari Jaguar ===== */ +/* https://www.mulle-kybernetik.com/jagdox/memorymap.html */ +static const rc_memory_region_t _rc_memory_regions_atari_jaguar[] = { + { 0x000000U, 0x1FFFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari_jaguar = { _rc_memory_regions_atari_jaguar, 1 }; + +/* ===== Atari Lynx ===== */ +/* http://www.retroisle.com/atari/lynx/Technical/Programming/lynxprgdumm.php */ +static const rc_memory_region_t _rc_memory_regions_atari_lynx[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Zero Page" }, + { 0x000100U, 0x0001FFU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "Stack" }, + { 0x000200U, 0x00FBFFU, 0x000200U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00FC00U, 0x00FCFFU, 0x00FC00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "SUZY hardware access" }, + { 0x00FD00U, 0x00FDFFU, 0x00FD00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "MIKEY hardware access" }, + { 0x00FE00U, 0x00FFF7U, 0x00FE00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Boot ROM" }, + { 0x00FFF8U, 0x00FFFFU, 0x00FFF8U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware vectors" } +}; +static const rc_memory_regions_t rc_memory_regions_atari_lynx = { _rc_memory_regions_atari_lynx, 7 }; + +/* ===== ColecoVision ===== */ +static const rc_memory_region_t _rc_memory_regions_colecovision[] = { + { 0x000000U, 0x0003FFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_regions_colecovision, 1 }; + +/* ===== Commodore 64 ===== */ +/* https://www.c64-wiki.com/wiki/Memory_Map */ +/* https://sta.c64.org/cbm64mem.html */ +static const rc_memory_region_t _rc_memory_regions_c64[] = { + { 0x000000U, 0x0003FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x000400U, 0x0007FFU, 0x000400U, RC_MEMORY_TYPE_VIDEO_RAM, "Screen RAM" }, + { 0x000800U, 0x009FFFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* BASIC Program Storage Area */ + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / BASIC ROM Area */ + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area */ + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "I/O Area" }, /* also Character ROM */ + { 0x00E000U, 0x00FFFFU, 0x00E000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / Kernal ROM */ +}; +static const rc_memory_regions_t rc_memory_regions_c64 = { _rc_memory_regions_c64, 7 }; + +/* ===== Dreamcast ===== */ +/* http://archiv.sega-dc.de/munkeechuff/hardware/Memory.html */ +static const rc_memory_region_t _rc_memory_regions_dreamcast[] = { + { 0x00000000U, 0x00FFFFFFU, 0x0C000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_dreamcast = { _rc_memory_regions_dreamcast, 1 }; + +/* ===== Elektor TV Games Computer ===== */ +/* https://amigan.yatho.com/e-coding.txt */ +static const rc_memory_region_t _rc_memory_regions_elektor_tv_games[] = { + { 0x000000U, 0x0013FFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x001400U, 0x0014FFU, 0x001C00U, RC_MEMORY_TYPE_UNUSED, "Unused" }, /* mirror of $1D00-$1DFF */ + { 0x001500U, 0x0016FFU, 0x001D00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, /* two 256-byte I/O areas */ + { 0x001700U, 0x0017FFU, 0x001F00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_elektor_tv_games = { _rc_memory_regions_elektor_tv_games, 4 }; + +/* ===== Fairchild Channel F ===== */ +static const rc_memory_region_t _rc_memory_regions_fairchild_channel_f[] = { + /* "System RAM" is actually just a bunch of registers internal to CPU so all carts have it. + * "Video RAM" is part of the console so it's always available but it is write-only by the ROMs. + * "Cartridge RAM" is the cart BUS. Most carts only have ROMs on this bus. Exception are + * German Schach and homebrew carts that have 2K of RAM there in addition to ROM. + * "F2102 RAM" is used by Maze for 1K of RAM. + * https://discord.com/channels/310192285306454017/645777658319208448/967001438087708714 */ + { 0x00000000U, 0x0000003FU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00000040U, 0x0000083FU, 0x00300000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x00000840U, 0x0001083FU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x00010840U, 0x00010C3FU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "F2102 RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_fairchild_channel_f = { _rc_memory_regions_fairchild_channel_f, 4 }; + +/* ===== GameBoy / GameBoy Color ===== */ +static const rc_memory_region_t _rc_memory_regions_gameboy[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt vector" }, + { 0x000100U, 0x00014FU, 0x000100U, RC_MEMORY_TYPE_READONLY, "Cartridge header" }, + { 0x000150U, 0x003FFFU, 0x000150U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (fixed)" }, /* bank 0 */ + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (paged)" }, /* bank 1-XX (switchable) */ + { 0x008000U, 0x0097FFU, 0x008000U, RC_MEMORY_TYPE_VIDEO_RAM, "Tile RAM" }, + { 0x009800U, 0x009BFFU, 0x009800U, RC_MEMORY_TYPE_VIDEO_RAM, "BG1 map data" }, + { 0x009C00U, 0x009FFFU, 0x009C00U, RC_MEMORY_TYPE_VIDEO_RAM, "BG2 map data" }, + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM"}, + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (fixed)" }, + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (bank 1)" }, + { 0x00E000U, 0x00FDFFU, 0x00C000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Echo RAM" }, + { 0x00FE00U, 0x00FE9FU, 0x00FE00U, RC_MEMORY_TYPE_VIDEO_RAM, "Sprite RAM"}, + { 0x00FEA0U, 0x00FEFFU, 0x00FEA0U, RC_MEMORY_TYPE_UNUSED, ""}, + { 0x00FF00U, 0x00FF7FU, 0x00FF00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware I/O"}, + { 0x00FF80U, 0x00FFFEU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Quick RAM"}, + { 0x00FFFFU, 0x00FFFFU, 0x00FFFFU, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt enable"}, + + /* GameBoy Color provides six extra banks of memory that can be paged out through the $DXXX + * memory space, but the timing of that does not correspond with blanks, which is when achievements + * are processed. As such, it is desirable to always have access to these extra banks. We do this + * by expecting the extra banks to be addressable at addresses not supported by the native system. */ + { 0x010000U, 0x015FFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (banks 2-7, GBC only)" } +}; +static const rc_memory_regions_t rc_memory_regions_gameboy = { _rc_memory_regions_gameboy, 16 }; +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_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, 3 }; + +/* ===== GameCube ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_gamecube[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_gamecube = { _rc_memory_regions_gamecube, 1 }; + +/* ===== Game Gear ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_game_gear[] = { + { 0x000000U, 0x001FFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_game_gear = { _rc_memory_regions_game_gear, 1 }; + +/* ===== Intellivision ===== */ +/* http://wiki.intellivision.us/index.php/Memory_Map */ +/* NOTE: Intellivision memory addresses point at 16-bit values. FreeIntv exposes them as little-endian + * 32-bit values. As such, the addresses are off by a factor of 4 _and_ the data is only where we + * expect it on little-endian systems. + */ +static const rc_memory_region_t _rc_memory_regions_intellivision[] = { + /* For backwards compatibility, register a 128-byte chunk of video RAM so the system memory + * will start at $0080. $0000-$007F previously tried to map to the STIC video registers as + * RETRO_MEMORY_VIDEO_RAM, and FreeIntv didn't expose any RETRO_MEMORY_VIDEO_RAM, so the first + * byte of RETRO_MEMORY_SYSTEM_RAM was registered at $0080. The data at $0080 is actually the + * STIC registers (4 bytes each), so we need to provide an arbitrary 128-byte padding that + * claims to be video RAM to ensure the system RAM ends up at the right address. + */ + { 0x000000U, 0x00007FU, 0xFFFFFFU, RC_MEMORY_TYPE_VIDEO_RAM, "" }, + + /* RetroAchievements address = real address x4 + 0x80. + * These all have to map to RETRO_MEMORY_SYSTEM_RAM (even the video-related fields) as the + * entire block is exposed as a single entity by FreeIntv */ + + /* $0000-$007F: STIC registers, $0040-$007F are readonly */ + { 0x000080U, 0x00027FU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "STIC Registers" }, + /* $0080-$00FF: unused */ + { 0x000280U, 0x00047FU, 0x000080U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $0100-$035F: system RAM, $0100-$01EF is scratch memory and only 8-bits per address */ + { 0x000480U, 0x000DFFU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* $0360-$03FF: unused */ + { 0x000E00U, 0x00107FU, 0x000360U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $0400-$0FFF: cartridge RAM */ + { 0x001080U, 0x00407FU, 0x000400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + /* $1000-$1FFF: unused */ + { 0x004080U, 0x00807FU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $2000-$2FFF: cartridge RAM */ + { 0x008080U, 0x00C07FU, 0x002000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + /* $3000-$3FFF: video RAM */ + { 0x00C080U, 0x01007FU, 0x003000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Video RAM" }, + /* $4000-$FFFF: cartridge RAM */ + { 0x010080U, 0x04007FU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_intellivision = { _rc_memory_regions_intellivision, 10 }; + +/* ===== Interton VC 4000 ===== */ +/* https://amigan.yatho.com/i-coding.txt */ +/* Cartridge RAM is not persisted, it's just expanded storage */ +static const rc_memory_region_t _rc_memory_regions_interton_vc_4000[] = { + { 0x000000U, 0x0003FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x000400U, 0x0004FFU, 0x001E00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, + { 0x000500U, 0x0005FFU, 0x001F00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_interton_vc_4000 = { _rc_memory_regions_interton_vc_4000, 3 }; + +/* ===== Magnavox Odyssey 2 ===== */ +/* https://sudonull.com/post/76885-Architecture-and-programming-Philips-Videopac-Magnavox-Odyssey-2 */ +static const rc_memory_region_t _rc_memory_regions_magnavox_odyssey_2[] = { + /* Internal and external RAMs are reachable using unique instructions. + * The real addresses provided are virtual and for mapping purposes only. */ + { 0x000000U, 0x00003FU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Internal RAM" }, + { 0x000040U, 0x00013FU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "External RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_magnavox_odyssey_2 = { _rc_memory_regions_magnavox_odyssey_2, 2 }; + +/* ===== Master System ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_master_system[] = { + { 0x000000U, 0x001FFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_master_system = { _rc_memory_regions_master_system, 1 }; + +/* ===== MegaDrive (Genesis) ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_megadrive[] = { + { 0x000000U, 0x00FFFFU, 0xFF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x010000U, 0x01FFFFU, 0x000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; + +/* ===== MegaDrive 32X (Genesis 32X) ===== */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ +static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { + { 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 }; + +/* ===== MSX ===== */ +/* https://www.msx.org/wiki/The_Memory */ +/* MSX only has 64KB of addressable RAM, of which 32KB is reserved for the system/BIOS. + * However, the system has up to 512KB of RAM, which is paged into the addressable RAM + * We expect the raw RAM to be exposed, rather than force the devs to worry about the + * paging system. The entire RAM is expected to appear starting at $10000, which is not + * addressable by the system itself. + */ +static const rc_memory_region_t _rc_memory_regions_msx[] = { + { 0x000000U, 0x07FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_msx = { _rc_memory_regions_msx, 1 }; + +/* ===== MS DOS ===== */ +static const rc_memory_region_t _rc_memory_regions_ms_dos[] = { + /* DOS emulators split the 640 KB conventional memory into two regions. + * First the part of the conventional memory given to the running game at $000000. + * The part of the conventional memory containing DOS and BIOS controlled memory + * is at $100000. The length of these can vary depending on the hardware + * and DOS version (or emulated DOS shell). + * These first two regions will only ever total to 640 KB but the regions map + * to 1 MB bounds to make resulting memory addresses more readable. + * When emulating a game not under DOS (so called 'PC Booter' games), the entirety + * of the 640 KB conventional memory block will be at $000000. + */ + { 0x00000000U, 0x0009FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Game Conventional Memory" }, + { 0x000A0000U, 0x000FFFFFU, 0x000A0000U, RC_MEMORY_TYPE_UNUSED, "Padding to align OS Conventional Memory" }, + { 0x00100000U, 0x0019FFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "OS Conventional Memory" }, + { 0x001A0000U, 0x001FFFFFU, 0x001A0000U, RC_MEMORY_TYPE_UNUSED, "Padding to align Expanded Memory" }, + /* Last is all the expanded memory which for now we map up to 64 MB which should be + * enough for the games we want to cover. An emulator might emulate more than that. + */ + { 0x00200000U, 0x041FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Expanded Memory" } +}; +static const rc_memory_regions_t rc_memory_regions_ms_dos = { _rc_memory_regions_ms_dos, 5 }; + +/* ===== Neo Geo Pocket ===== */ +/* http://neopocott.emuunlim.com/docs/tech-11.txt */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = { + /* The docs suggest there's Work RAM exposed from $0000-$6FFF, Sound RAM from $7000-$7FFF, and Video + * RAM from $8000-$BFFF, but both MednafenNGP and FBNeo only expose system RAM from $4000-$7FFF */ + { 0x000000U, 0x003FFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_pocket = { _rc_memory_regions_neo_geo_pocket, 1 }; + +/* ===== Neo Geo CD ===== */ +/* https://wiki.neogeodev.org/index.php?title=68k_memory_map */ +/* NeoCD exposes $000000-$1FFFFF as System RAM, but it seems like only the WORKRAM section is used. + * This is consistent with http://www.hardmvs.fr/manuals/NeoGeoProgrammersGuide.pdf (page25), which says: + * + * Furthermore, the NEO-GEO provides addresses 100000H-10FFFFH as a work area, out of which the + * addresses 10F300H-10FFFFH are reserved exclusively for use by the system program. Therefore, + * every game is to use addresses 100000H-10F2FFH. + * + * Also note that PRG files (game ROM) can be loaded anywhere else in the $000000-$1FFFFF range. + * AoF3 illustrates this pretty clearly: https://wiki.neogeodev.org/index.php?title=IPL_file + * + * PROG_CD.PRG,0,0 + * PROG_CDX.PRG,0,058000 + * CNV_NM.PRG,0,0C0000 + * FIX_DATA.PRG,0,0FD000 + * OBJACTLK.PRG,0,130000 + * SSEL_CNV.PRG,0,15A000 + * SSEL_BAK.PRG,0,16F000 + * HITMSG.PRG,0,170000 + * SSEL_SPR.PRG,0,19D000 + */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_cd[] = { + { 0x000000U, 0x00F2FFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* NOTE: some BIOS settings are exposed through the reserved RAM: https://wiki.neogeodev.org/index.php?title=68k_ASM_defines */ + { 0x00F300U, 0x00FFFFU, 0x0010F300U, RC_MEMORY_TYPE_SYSTEM_RAM, "Reserved RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_cd = { _rc_memory_regions_neo_geo_cd, 2 }; + +/* ===== Nintendo Entertainment System ===== */ +/* https://wiki.nesdev.com/w/index.php/CPU_memory_map */ +static const rc_memory_region_t _rc_memory_regions_nes[] = { + { 0x0000U, 0x07FFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x0800U, 0x0FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1000U, 0x17FFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1800U, 0x1FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x2000U, 0x2007U, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "PPU Register" }, + { 0x2008U, 0x3FFFU, 0x2008U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored PPU Register" }, /* repeats every 8 bytes */ + { 0x4000U, 0x4017U, 0x4000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O register" }, + { 0x4018U, 0x401FU, 0x4018U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O test register" }, + + /* NOTE: these are for the original NES/Famicom */ + { 0x4020U, 0x5FFFU, 0x4020U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, /* varies by mapper */ + { 0x6000U, 0x7FFFU, 0x6000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM"}, + { 0x8000U, 0xFFFFU, 0x8000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM"}, + + /* NOTE: these are the correct mappings for FDS: https://fms.komkon.org/EMUL8/NES.html + * 0x6000-0xDFFF is RAM on the FDS system and 0xE000-0xFFFF is FDS BIOS. + * If the core implements a memory map, we should still be able to translate the addresses + * correctly as we only use the classifications when a memory map is not provided + + { 0x4020U, 0x40FFU, 0x4020U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "FDS I/O registers"}, + { 0x4100U, 0x5FFFU, 0x4100U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, // varies by mapper + { 0x6000U, 0xDFFFU, 0x6000U, RC_MEMORY_TYPE_SYSTEM_RAM, "FDS RAM"}, + { 0xE000U, 0xFFFFU, 0xE000U, RC_MEMORY_TYPE_READONLY, "FDS BIOS ROM"}, + + */ +}; +static const rc_memory_regions_t rc_memory_regions_nes = { _rc_memory_regions_nes, 11 }; + +/* ===== Nintendo 64 ===== */ +/* https://raw.githubusercontent.com/mikeryan/n64dev/master/docs/n64ops/n64ops%23h.txt */ +static const rc_memory_region_t _rc_memory_regions_n64[] = { + { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ + { 0x200000U, 0x3FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ + { 0x400000U, 0x7FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak - cannot find any details for real address */ +}; +static const rc_memory_regions_t rc_memory_regions_n64 = { _rc_memory_regions_n64, 3 }; + +/* ===== Nintendo DS ===== */ +/* https://www.akkit.org/info/gbatek.htm#dsmemorymaps */ +static const rc_memory_region_t _rc_memory_regions_nintendo_ds[] = { + { 0x000000U, 0x3FFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 1 }; + +/* ===== Nintendo DSi ===== */ +/* https://problemkaputt.de/gbatek.htm#dsiiomap */ +static const rc_memory_region_t _rc_memory_regions_nintendo_dsi[] = { + { 0x000000U, 0xFFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 1 }; + +/* ===== Oric ===== */ +static const rc_memory_region_t _rc_memory_regions_oric[] = { + /* actual size depends on machine type - up to 64KB */ + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_oric = { _rc_memory_regions_oric, 1 }; + +/* ===== PC-8800 ===== */ +static const rc_memory_region_t _rc_memory_regions_pc8800[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, + { 0x010000U, 0x010FFFU, 0x010000U, RC_MEMORY_TYPE_VIDEO_RAM, "Text VRAM" } /* technically VRAM, but often used as system RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_pc8800 = { _rc_memory_regions_pc8800, 2 }; + +/* ===== PC Engine ===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 1 }; + +/* ===== PC Engine CD===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine_cd[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x002000U, 0x011FFFU, 0x100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD RAM" }, + { 0x012000U, 0x041FFFU, 0x0D0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Super System Card RAM" }, + { 0x042000U, 0x0427FFU, 0x1EE000U, RC_MEMORY_TYPE_SAVE_RAM, "CD Battery-backed RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine_cd = { _rc_memory_regions_pc_engine_cd, 4 }; + +/* ===== PC-FX ===== */ +/* http://daifukkat.su/pcfx/data/memmap.html */ +static const rc_memory_region_t _rc_memory_regions_pcfx[] = { + { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x200000U, 0x207FFFU, 0xE0000000U, RC_MEMORY_TYPE_SAVE_RAM, "Internal Backup Memory" }, + { 0x208000U, 0x20FFFFU, 0xE8000000U, RC_MEMORY_TYPE_SAVE_RAM, "External Backup Memory" }, +}; +static const rc_memory_regions_t rc_memory_regions_pcfx = { _rc_memory_regions_pcfx, 3 }; + +/* ===== PlayStation ===== */ +/* http://www.raphnet.net/electronique/psx_adaptor/Playstation.txt */ +static const rc_memory_region_t _rc_memory_regions_playstation[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x010000U, 0x1FFFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_playstation = { _rc_memory_regions_playstation, 2 }; + +/* ===== PlayStation 2 ===== */ +/* https://psi-rockin.github.io/ps2tek/ */ +static const rc_memory_region_t _rc_memory_regions_playstation2[] = { + { 0x00000000U, 0x000FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x00100000U, 0x01FFFFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x02000000U, 0x02003FFFU, 0x70000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Scratchpad RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_playstation2 = { _rc_memory_regions_playstation2, 3 }; + +/* ===== PlayStation Portable ===== */ +/* https://github.com/uofw/upspd/wiki/Memory-map */ +static const rc_memory_region_t _rc_memory_regions_psp[] = { + { 0x00000000U, 0x007FFFFFU, 0x08000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x00800000U, 0x01FFFFFFU, 0x08800000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_psp = { _rc_memory_regions_psp, 2 }; + +/* ===== Pokemon Mini ===== */ +/* https://www.pokemon-mini.net/documentation/memory-map/ */ +static const rc_memory_region_t _rc_memory_regions_pokemini[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "BIOS RAM" }, + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_pokemini = { _rc_memory_regions_pokemini, 2 }; + +/* ===== Sega CD ===== */ +/* https://en.wikibooks.org/wiki/Genesis_Programming#MegaCD_Changes */ +static const rc_memory_region_t _rc_memory_regions_segacd[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "68000 RAM" }, + { 0x010000U, 0x08FFFFU, 0x80020000U, RC_MEMORY_TYPE_SAVE_RAM, "CD PRG RAM" } /* normally banked into $020000-$03FFFF */ +}; +static const rc_memory_regions_t rc_memory_regions_segacd = { _rc_memory_regions_segacd, 2 }; + +/* ===== Sega Saturn ===== */ +/* https://segaretro.org/Sega_Saturn_hardware_notes_(2004-04-27) */ +static const rc_memory_region_t _rc_memory_regions_saturn[] = { + { 0x000000U, 0x0FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Work RAM Low" }, + { 0x100000U, 0x1FFFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Work RAM High" } +}; +static const rc_memory_regions_t rc_memory_regions_saturn = { _rc_memory_regions_saturn, 2 }; + +/* ===== SG-1000 ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_sg1000[] = { + { 0x000000U, 0x0003FFU, 0xC000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* https://github.com/libretro/FBNeo/blob/697801c6262be6ca91615cf905444d3e039bc06f/src/burn/drv/sg1000/d_sg1000.cpp#L210-L237 */ + /* Expansion mode B exposes 8KB at $C000. The first 2KB hides the System RAM, but since the address matches, + we'll leverage that definition and expand it another 6KB */ + { 0x000400U, 0x001FFFU, 0xC400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, + /* Expansion mode A exposes 8KB at $2000 */ + { 0x002000U, 0x003FFFU, 0x2000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, + /* Othello exposes 2KB at $8000, and The Castle exposes 8KB at $8000 */ + { 0x004000U, 0x005FFFU, 0x8000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" } +}; +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" }, /* BIOS */ + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, /* only really goes to $33FF? */ + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 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, 6 }; + +/* ===== Super Nintendo ===== */ +/* https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM */ +static const rc_memory_region_t _rc_memory_regions_snes[] = { + { 0x000000U, 0x01FFFFU, 0x7E0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x020000U, 0x03FFFFU, 0xFE0000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_snes = { _rc_memory_regions_snes, 2 }; + +/* ===== Thomson TO8 ===== */ +/* https://github.com/mamedev/mame/blob/master/src/mame/drivers/thomson.cpp#L1617 */ +static const rc_memory_region_t _rc_memory_regions_thomson_to8[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_thomson_to8 = { _rc_memory_regions_thomson_to8, 1 }; + +/* ===== TI-83 ===== */ +/* https://tutorials.eeems.ca/ASMin28Days/lesson/day03.html#mem */ +static const rc_memory_region_t _rc_memory_regions_ti83[] = { + { 0x000000U, 0x007FFFU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_ti83 = { _rc_memory_regions_ti83, 1 }; + +/* ===== TIC-80 ===== */ +/* https://github.com/nesbox/TIC-80/wiki/RAM */ +static const rc_memory_region_t _rc_memory_regions_tic80[] = { + { 0x000000U, 0x003FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Video RAM" }, /* have to classify this as system RAM because the core exposes it as part of the RETRO_MEMORY_SYSTEM_RAM */ + { 0x004000U, 0x005FFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Tile RAM" }, + { 0x006000U, 0x007FFFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Sprite RAM" }, + { 0x008000U, 0x00FF7FU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "MAP RAM" }, + { 0x00FF80U, 0x00FF8BU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Input State" }, + { 0x00FF8CU, 0x014003U, 0x00FF8CU, RC_MEMORY_TYPE_SYSTEM_RAM, "Sound RAM" }, + { 0x014004U, 0x014403U, 0x014004U, RC_MEMORY_TYPE_SAVE_RAM, "Persistent Memory" }, /* this is also returned as part of RETRO_MEMORY_SYSTEM_RAM, but can be extrapolated correctly because the pointer starts at the first SYSTEM_RAM region */ + { 0x014404U, 0x014603U, 0x014404U, RC_MEMORY_TYPE_SYSTEM_RAM, "Sprite Flags" }, + { 0x014604U, 0x014E03U, 0x014604U, RC_MEMORY_TYPE_SYSTEM_RAM, "System Font" }, + { 0x014E04U, 0x017FFFU, 0x014E04U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM"} +}; +static const rc_memory_regions_t rc_memory_regions_tic80 = { _rc_memory_regions_tic80, 10 }; + +/* ===== Uzebox ===== */ +/* https://uzebox.org/index.php */ +static const rc_memory_region_t _rc_memory_regions_uzebox[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_uzebox = { _rc_memory_regions_uzebox, 1 }; + +/* ===== Vectrex ===== */ +/* https://roadsidethoughts.com/vectrex/vectrex-memory-map.htm */ +static const rc_memory_region_t _rc_memory_regions_vectrex[] = { + { 0x000000U, 0x0003FFU, 0x00C800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_vectrex = { _rc_memory_regions_vectrex, 1 }; + +/* ===== Virtual Boy ===== */ +static const rc_memory_region_t _rc_memory_regions_virtualboy[] = { + { 0x000000U, 0x00FFFFU, 0x05000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x010000U, 0x01FFFFU, 0x06000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_virtualboy = { _rc_memory_regions_virtualboy, 2 }; + +/* ===== Watara Supervision ===== */ +/* https://github.com/libretro/potator/blob/b5e5ba02914fcdf4a8128072dbc709da28e08832/common/memorymap.c#L231-L259 */ +static const rc_memory_region_t _rc_memory_regions_watara_supervision[] = { + { 0x0000U, 0x001FFFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x2000U, 0x003FFFU, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Registers" }, + { 0x4000U, 0x005FFFU, 0x4000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_watara_supervision = { _rc_memory_regions_watara_supervision, 3 }; + +/* ===== WASM-4 ===== */ +/* fantasy console that runs specifically designed WebAssembly games */ +/* https://github.com/aduros/wasm4/blob/main/site/docs/intro.md#hardware-specs */ +static const rc_memory_region_t _rc_memory_regions_wasm4[] = { + { 0x000000U, 0x00FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Persistent storage is not directly accessible from the game. It has to be loaded into System RAM first + { 0x010000U, 0x0103FFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "Disk Storage"} + */ +}; +static const rc_memory_regions_t rc_memory_regions_wasm4 = { _rc_memory_regions_wasm4, 1 }; + +/* ===== Wii ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_wii[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x01800000U, 0x057FFFFF, 0x90000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wii = { _rc_memory_regions_wii, 2 }; + +/* ===== WonderSwan ===== */ +/* http://daifukkat.su/docs/wsman/#ovr_memmap */ +static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { + /* RAM ends at 0x3FFF for WonderSwan, WonderSwan color uses all 64KB */ + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Only 64KB of SRAM is accessible via the addressing scheme, but the cartridge + * may have up to 512KB of SRAM. http://daifukkat.su/docs/wsman/#cart_meta + * Since beetle_wswan exposes it as a contiguous block, assume its contiguous + * even though the documentation says $20000-$FFFFF is ROM data. If this causes + * a conflict in the future, we can revisit. A new region with a virtual address + * could be added to pick up the additional SRAM data. As long as it immediately + * follows the 64KB at $10000, all existing achievements should be unaffected. + */ + { 0x010000U, 0x08FFFFU, 0x010000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_regions_wonderswan, 2 }; + +/* ===== default ===== */ +static const rc_memory_regions_t rc_memory_regions_none = { 0, 0 }; + +const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) +{ + switch (console_id) + { + case RC_CONSOLE_3DO: + return &rc_memory_regions_3do; + + case RC_CONSOLE_AMIGA: + return &rc_memory_regions_amiga; + + case RC_CONSOLE_AMSTRAD_PC: + return &rc_memory_regions_amstrad_pc; + + case RC_CONSOLE_APPLE_II: + return &rc_memory_regions_appleii; + + case RC_CONSOLE_ARCADIA_2001: + return &rc_memory_regions_arcadia_2001; + + case RC_CONSOLE_ARDUBOY: + return &rc_memory_regions_arduboy; + + case RC_CONSOLE_ATARI_2600: + return &rc_memory_regions_atari2600; + + case RC_CONSOLE_ATARI_7800: + return &rc_memory_regions_atari7800; + + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_ATARI_JAGUAR_CD: + return &rc_memory_regions_atari_jaguar; + + case RC_CONSOLE_ATARI_LYNX: + return &rc_memory_regions_atari_lynx; + + case RC_CONSOLE_COLECOVISION: + return &rc_memory_regions_colecovision; + + case RC_CONSOLE_COMMODORE_64: + return &rc_memory_regions_c64; + + case RC_CONSOLE_DREAMCAST: + return &rc_memory_regions_dreamcast; + + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + return &rc_memory_regions_elektor_tv_games; + + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return &rc_memory_regions_fairchild_channel_f; + + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_GAMEBOY: + return &rc_memory_regions_gameboy; + + case RC_CONSOLE_GAMEBOY_COLOR: + return &rc_memory_regions_gameboy_color; + + case RC_CONSOLE_GAMEBOY_ADVANCE: + return &rc_memory_regions_gameboy_advance; + + case RC_CONSOLE_GAMECUBE: + return &rc_memory_regions_gamecube; + + case RC_CONSOLE_GAME_GEAR: + return &rc_memory_regions_game_gear; + + case RC_CONSOLE_INTELLIVISION: + return &rc_memory_regions_intellivision; + + case RC_CONSOLE_INTERTON_VC_4000: + return &rc_memory_regions_interton_vc_4000; + + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + return &rc_memory_regions_magnavox_odyssey_2; + + case RC_CONSOLE_MASTER_SYSTEM: + return &rc_memory_regions_master_system; + + case RC_CONSOLE_MEGA_DRIVE: + return &rc_memory_regions_megadrive; + + case RC_CONSOLE_SEGA_32X: + return &rc_memory_regions_megadrive_32x; + + case RC_CONSOLE_MSX: + return &rc_memory_regions_msx; + + case RC_CONSOLE_MS_DOS: + return &rc_memory_regions_ms_dos; + + case RC_CONSOLE_NEOGEO_POCKET: + return &rc_memory_regions_neo_geo_pocket; + + case RC_CONSOLE_NEO_GEO_CD: + return &rc_memory_regions_neo_geo_cd; + + case RC_CONSOLE_NINTENDO: + return &rc_memory_regions_nes; + + case RC_CONSOLE_NINTENDO_64: + return &rc_memory_regions_n64; + + case RC_CONSOLE_NINTENDO_DS: + return &rc_memory_regions_nintendo_ds; + + case RC_CONSOLE_NINTENDO_DSI: + return &rc_memory_regions_nintendo_dsi; + + case RC_CONSOLE_ORIC: + return &rc_memory_regions_oric; + + case RC_CONSOLE_PC8800: + return &rc_memory_regions_pc8800; + + case RC_CONSOLE_PC_ENGINE: + return &rc_memory_regions_pc_engine; + + case RC_CONSOLE_PC_ENGINE_CD: + return &rc_memory_regions_pc_engine_cd; + + case RC_CONSOLE_PCFX: + return &rc_memory_regions_pcfx; + + case RC_CONSOLE_PLAYSTATION: + return &rc_memory_regions_playstation; + + case RC_CONSOLE_PLAYSTATION_2: + return &rc_memory_regions_playstation2; + + case RC_CONSOLE_PSP: + return &rc_memory_regions_psp; + + case RC_CONSOLE_POKEMON_MINI: + return &rc_memory_regions_pokemini; + + case RC_CONSOLE_SATURN: + return &rc_memory_regions_saturn; + + case RC_CONSOLE_SEGA_CD: + return &rc_memory_regions_segacd; + + case RC_CONSOLE_SG1000: + return &rc_memory_regions_sg1000; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return &rc_memory_regions_scv; + + case RC_CONSOLE_SUPER_NINTENDO: + return &rc_memory_regions_snes; + + case RC_CONSOLE_SUPERVISION: + return &rc_memory_regions_watara_supervision; + + case RC_CONSOLE_THOMSONTO8: + return &rc_memory_regions_thomson_to8; + + case RC_CONSOLE_TI83: + return &rc_memory_regions_ti83; + + case RC_CONSOLE_TIC80: + return &rc_memory_regions_tic80; + + case RC_CONSOLE_UZEBOX: + return &rc_memory_regions_uzebox; + + case RC_CONSOLE_VECTREX: + return &rc_memory_regions_vectrex; + + case RC_CONSOLE_VIRTUAL_BOY: + return &rc_memory_regions_virtualboy; + + case RC_CONSOLE_WASM4: + return &rc_memory_regions_wasm4; + + case RC_CONSOLE_WII: + return &rc_memory_regions_wii; + + case RC_CONSOLE_WONDERSWAN: + return &rc_memory_regions_wonderswan; + + default: + return &rc_memory_regions_none; + } +} diff --git a/3rdparty/rcheevos/src/rcheevos/format.c b/3rdparty/rcheevos/src/rcheevos/format.c new file mode 100644 index 0000000000..0aa44ee6a1 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/format.c @@ -0,0 +1,280 @@ +#include "rc_internal.h" + +#include "../rc_compat.h" + +#include +#include + +int rc_parse_format(const char* format_str) { + switch (*format_str++) { + case 'F': + if (!strcmp(format_str, "RAMES")) { + return RC_FORMAT_FRAMES; + } + if (!strncmp(format_str, "LOAT", 4) && format_str[4] >= '1' && format_str[4] <= '6' && format_str[5] == '\0') { + return RC_FORMAT_FLOAT1 + (format_str[4] - '1'); + } + if (!strncmp(format_str, "IXED", 4) && format_str[4] >= '1' && format_str[4] <= '3' && format_str[5] == '\0') { + return RC_FORMAT_FIXED1 + (format_str[4] - '1'); + } + + break; + + case 'T': + if (!strcmp(format_str, "IME")) { + return RC_FORMAT_FRAMES; + } + if (!strcmp(format_str, "IMESECS")) { + return RC_FORMAT_SECONDS; + } + if (!strcmp(format_str, "HOUSANDS")) { + return RC_FORMAT_THOUSANDS; + } + if (!strcmp(format_str, "ENS")) { + return RC_FORMAT_TENS; + } + + break; + + case 'S': + if (!strcmp(format_str, "ECS")) { + return RC_FORMAT_SECONDS; + } + if (!strcmp(format_str, "CORE")) { + return RC_FORMAT_SCORE; + } + if (!strcmp(format_str, "ECS_AS_MINS")) { + return RC_FORMAT_SECONDS_AS_MINUTES; + } + + break; + + case 'M': + if (!strcmp(format_str, "ILLISECS")) { + return RC_FORMAT_CENTISECS; + } + if (!strcmp(format_str, "INUTES")) { + return RC_FORMAT_MINUTES; + } + + break; + + case 'P': + if (!strcmp(format_str, "OINTS")) { + return RC_FORMAT_SCORE; + } + + break; + + case 'V': + if (!strcmp(format_str, "ALUE")) { + return RC_FORMAT_VALUE; + } + + break; + + case 'U': + if (!strcmp(format_str, "NSIGNED")) { + return RC_FORMAT_UNSIGNED_VALUE; + } + + break; + + case 'O': + if (!strcmp(format_str, "THER")) { + return RC_FORMAT_SCORE; + } + + break; + + case 'H': + if (!strcmp(format_str, "UNDREDS")) { + return RC_FORMAT_HUNDREDS; + } + + break; + } + + return RC_FORMAT_VALUE; +} + +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, size_t size, uint32_t seconds) { + uint32_t hours, minutes; + + /* apply modulus math to split the seconds into hours/minutes/seconds */ + minutes = seconds / 60; + seconds -= minutes * 60; + if (minutes < 60) { + return snprintf(buffer, size, "%u:%02u", minutes, seconds); + } + + hours = minutes / 60; + minutes -= hours * 60; + return snprintf(buffer, size, "%uh%02u:%02u", hours, minutes, 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 */ + seconds = centiseconds / 100; + centiseconds -= seconds * 100; + + chars = rc_format_value_seconds(buffer, size, seconds); + if (chars > 0) { + chars2 = snprintf(buffer + chars, size - chars, ".%02u", centiseconds); + if (chars2 > 0) { + chars += chars2; + } else { + chars = chars2; + } + } + + return chars; +} + +static int rc_format_value_fixed(char* buffer, size_t size, const char* format, int32_t value, int32_t factor) +{ + if (value >= 0) + return snprintf(buffer, size, format, value / factor, value % factor); + + return snprintf(buffer, size, format, value / factor, (-value) % factor); +} + +static int rc_format_value_padded(char* buffer, size_t size, const char* format, int32_t value) +{ + if (value == 0) + return snprintf(buffer, size, "0"); + + return snprintf(buffer, size, format, value); +} + +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; + + memcpy(&converted_value, value, sizeof(converted_value)); + + switch (format) { + default: + case RC_FORMAT_VALUE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = snprintf(buffer, size, "%d", converted_value.value.i32); + break; + + case RC_FORMAT_FRAMES: + /* 60 frames per second = 100 centiseconds / 60 frames; multiply frames by 100 / 60 */ + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_centiseconds(buffer, size, converted_value.value.u32 * 10 / 6); + break; + + case RC_FORMAT_CENTISECS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_centiseconds(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SECONDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_seconds(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SECONDS_AS_MINUTES: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_minutes(buffer, size, converted_value.value.u32 / 60); + break; + + case RC_FORMAT_MINUTES: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_minutes(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SCORE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = snprintf(buffer, size, "%06d", converted_value.value.i32); + break; + + case RC_FORMAT_FLOAT1: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.1f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT2: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.2f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT3: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.3f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT4: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.4f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT5: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.5f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT6: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.6f", converted_value.value.f32); + break; + + case RC_FORMAT_FIXED1: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_fixed(buffer, size, "%d.%u", converted_value.value.i32, 10); + break; + + case RC_FORMAT_FIXED2: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_fixed(buffer, size, "%d.%02u", converted_value.value.i32, 100); + break; + + case RC_FORMAT_FIXED3: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_fixed(buffer, size, "%d.%03u", converted_value.value.i32, 1000); + break; + + case RC_FORMAT_TENS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_padded(buffer, size, "%d0", converted_value.value.i32); + break; + + case RC_FORMAT_HUNDREDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_padded(buffer, size, "%d00", converted_value.value.i32); + break; + + case RC_FORMAT_THOUSANDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_padded(buffer, size, "%d000", converted_value.value.i32); + break; + + case RC_FORMAT_UNSIGNED_VALUE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = snprintf(buffer, size, "%u", converted_value.value.u32); + break; + } + + return chars; +} + +int rc_format_value(char* buffer, int size, int32_t value, int format) { + rc_typed_value_t typed_value; + + typed_value.value.i32 = value; + typed_value.type = RC_VALUE_TYPE_SIGNED; + return rc_format_typed_value(buffer, size, &typed_value, format); +} diff --git a/3rdparty/rcheevos/src/rcheevos/lboard.c b/3rdparty/rcheevos/src/rcheevos/lboard.c new file mode 100644 index 0000000000..98b4ec2f17 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/lboard.c @@ -0,0 +1,278 @@ +#include "rc_internal.h" + +enum { + RC_LBOARD_START = 1 << 0, + RC_LBOARD_CANCEL = 1 << 1, + RC_LBOARD_SUBMIT = 1 << 2, + RC_LBOARD_VALUE = 1 << 3, + RC_LBOARD_PROGRESS = 1 << 4, + RC_LBOARD_COMPLETE = RC_LBOARD_START | RC_LBOARD_CANCEL | RC_LBOARD_SUBMIT | RC_LBOARD_VALUE +}; + +void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse) { + int found; + + self->progress = 0; + found = 0; + + for (;;) + { + if ((memaddr[0] == 's' || memaddr[0] == 'S') && + (memaddr[1] == 't' || memaddr[1] == 'T') && + (memaddr[2] == 'a' || memaddr[2] == 'A') && memaddr[3] == ':') { + if ((found & RC_LBOARD_START) != 0) { + parse->offset = RC_DUPLICATED_START; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_START; + rc_parse_trigger_internal(&self->start, &memaddr, parse); + self->start.memrefs = 0; + } + } + else if ((memaddr[0] == 'c' || memaddr[0] == 'C') && + (memaddr[1] == 'a' || memaddr[1] == 'A') && + (memaddr[2] == 'n' || memaddr[2] == 'N') && memaddr[3] == ':') { + if ((found & RC_LBOARD_CANCEL) != 0) { + parse->offset = RC_DUPLICATED_CANCEL; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_CANCEL; + rc_parse_trigger_internal(&self->cancel, &memaddr, parse); + self->cancel.memrefs = 0; + } + } + else if ((memaddr[0] == 's' || memaddr[0] == 'S') && + (memaddr[1] == 'u' || memaddr[1] == 'U') && + (memaddr[2] == 'b' || memaddr[2] == 'B') && memaddr[3] == ':') { + if ((found & RC_LBOARD_SUBMIT) != 0) { + parse->offset = RC_DUPLICATED_SUBMIT; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_SUBMIT; + rc_parse_trigger_internal(&self->submit, &memaddr, parse); + self->submit.memrefs = 0; + } + } + else if ((memaddr[0] == 'v' || memaddr[0] == 'V') && + (memaddr[1] == 'a' || memaddr[1] == 'A') && + (memaddr[2] == 'l' || memaddr[2] == 'L') && memaddr[3] == ':') { + if ((found & RC_LBOARD_VALUE) != 0) { + parse->offset = RC_DUPLICATED_VALUE; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_VALUE; + rc_parse_value_internal(&self->value, &memaddr, parse); + self->value.memrefs = 0; + } + } + else if ((memaddr[0] == 'p' || memaddr[0] == 'P') && + (memaddr[1] == 'r' || memaddr[1] == 'R') && + (memaddr[2] == 'o' || memaddr[2] == 'O') && memaddr[3] == ':') { + if ((found & RC_LBOARD_PROGRESS) != 0) { + parse->offset = RC_DUPLICATED_PROGRESS; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_PROGRESS; + + self->progress = RC_ALLOC(rc_value_t, parse); + rc_parse_value_internal(self->progress, &memaddr, parse); + self->progress->memrefs = 0; + } + } + + /* encountered an error parsing one of the parts */ + if (parse->offset < 0) + return; + + /* end of string, or end of quoted string - stop processing */ + if (memaddr[0] == '\0' || memaddr[0] == '\"') + break; + + /* expect two colons between fields */ + if (memaddr[0] != ':' || memaddr[1] != ':') { + parse->offset = RC_INVALID_LBOARD_FIELD; + return; + } + + memaddr += 2; + } + + if ((found & RC_LBOARD_COMPLETE) != RC_LBOARD_COMPLETE) { + if ((found & RC_LBOARD_START) == 0) { + parse->offset = RC_MISSING_START; + } + else if ((found & RC_LBOARD_CANCEL) == 0) { + parse->offset = RC_MISSING_CANCEL; + } + else if ((found & RC_LBOARD_SUBMIT) == 0) { + parse->offset = RC_MISSING_SUBMIT; + } + else if ((found & RC_LBOARD_VALUE) == 0) { + parse->offset = RC_MISSING_VALUE; + } + + return; + } + + self->state = RC_LBOARD_STATE_WAITING; +} + +int rc_lboard_size(const char* memaddr) { + rc_lboard_t* self; + rc_parse_state_t parse; + rc_memref_t* first_memref; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &first_memref); + + self = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(self, memaddr, &parse); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { + rc_lboard_t* self; + rc_parse_state_t parse; + + if (!buffer || !memaddr) + return 0; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_lboard_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + + rc_parse_lboard_internal(self, memaddr, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : 0; +} + +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); + + if (self->state == RC_LBOARD_STATE_INACTIVE || self->state == RC_LBOARD_STATE_DISABLED) + return RC_LBOARD_STATE_INACTIVE; + + /* these are always tested once every frame, to ensure hit counts work properly */ + start_ok = rc_test_trigger(&self->start, peek, peek_ud, L); + cancel_ok = rc_test_trigger(&self->cancel, peek, peek_ud, L); + submit_ok = rc_test_trigger(&self->submit, peek, peek_ud, L); + + switch (self->state) + { + case RC_LBOARD_STATE_WAITING: + case RC_LBOARD_STATE_TRIGGERED: + case RC_LBOARD_STATE_CANCELED: + /* don't activate/reactivate until the start condition becomes false */ + if (start_ok) { + *value = 0; + return RC_LBOARD_STATE_INACTIVE; /* just return inactive for all of these */ + } + + /* start condition is false, allow the leaderboard to start on future frames */ + self->state = RC_LBOARD_STATE_ACTIVE; + break; + + case RC_LBOARD_STATE_ACTIVE: + /* leaderboard attempt is not in progress. if the start condition is true and the cancel condition is not, start the attempt */ + if (start_ok && !cancel_ok) { + if (submit_ok) { + /* start and submit are both true in the same frame, just submit without announcing the leaderboard is available */ + self->state = RC_LBOARD_STATE_TRIGGERED; + } + else if (self->start.requirement == 0 && self->start.alternative == 0) { + /* start condition is empty - this leaderboard is submit-only with no measured progress */ + } + else { + /* start the leaderboard attempt */ + self->state = RC_LBOARD_STATE_STARTED; + + /* reset any hit counts in the value */ + if (self->progress) + rc_reset_value(self->progress); + + rc_reset_value(&self->value); + } + } + break; + + case RC_LBOARD_STATE_STARTED: + /* leaderboard attempt in progress */ + if (cancel_ok) { + /* cancel condition is true, abort the attempt */ + self->state = RC_LBOARD_STATE_CANCELED; + } + else if (submit_ok) { + /* submit condition is true, submit the current value */ + self->state = RC_LBOARD_STATE_TRIGGERED; + } + break; + } + + /* Calculate the value */ + switch (self->state) { + case RC_LBOARD_STATE_STARTED: + if (self->progress) { + *value = rc_evaluate_value(self->progress, peek, peek_ud, L); + break; + } + /* fallthrough */ /* to RC_LBOARD_STATE_TRIGGERED */ + + case RC_LBOARD_STATE_TRIGGERED: + *value = rc_evaluate_value(&self->value, peek, peek_ud, L); + break; + + default: + *value = 0; + break; + } + + return self->state; +} + +int rc_lboard_state_active(int state) { + switch (state) + { + case RC_LBOARD_STATE_DISABLED: + case RC_LBOARD_STATE_INACTIVE: + return 0; + + default: + return 1; + } +} + +void rc_reset_lboard(rc_lboard_t* self) { + if (!self) + return; + + self->state = RC_LBOARD_STATE_WAITING; + + rc_reset_trigger(&self->start); + rc_reset_trigger(&self->submit); + rc_reset_trigger(&self->cancel); + + if (self->progress) + rc_reset_value(self->progress); + + rc_reset_value(&self->value); +} diff --git a/3rdparty/rcheevos/src/rcheevos/memref.c b/3rdparty/rcheevos/src/rcheevos/memref.c new file mode 100644 index 0000000000..87f6ec0bf1 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/memref.c @@ -0,0 +1,494 @@ +#include "rc_internal.h" + +#include /* malloc/realloc */ +#include /* memcpy */ +#include /* INFINITY/NAN */ + +#define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF + +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; + + if (!is_indirect) { + /* attempt to find an existing memref that can be shared */ + next_memref = parse->first_memref; + while (*next_memref) { + memref = *next_memref; + if (!memref->value.is_indirect && memref->address == address && memref->value.size == size) + return memref; + + next_memref = &memref->next; + } + + /* no match found, create a new entry */ + memref = RC_ALLOC_SCRATCH(rc_memref_t, parse); + *next_memref = memref; + } + else { + /* indirect references always create a new entry because we can't guarantee that the + * indirection amount will be the same between references. because they aren't shared, + * don't bother putting them in the chain. + */ + memref = RC_ALLOC(rc_memref_t, parse); + } + + memset(memref, 0, sizeof(*memref)); + memref->address = address; + memref->value.size = size; + memref->value.is_indirect = is_indirect; + + return memref; +} + +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address) { + const char* aux = *memaddr; + char* end; + unsigned long value; + + if (aux[0] == '0') { + if (aux[1] != 'x' && aux[1] != 'X') + return RC_INVALID_MEMORY_OPERAND; + + aux += 2; + switch (*aux++) { + /* ordered by estimated frequency in case compiler doesn't build a jump table */ + case 'h': case 'H': *size = RC_MEMSIZE_8_BITS; break; + case ' ': *size = RC_MEMSIZE_16_BITS; break; + case 'x': case 'X': *size = RC_MEMSIZE_32_BITS; break; + + case 'm': case 'M': *size = RC_MEMSIZE_BIT_0; break; + case 'n': case 'N': *size = RC_MEMSIZE_BIT_1; break; + case 'o': case 'O': *size = RC_MEMSIZE_BIT_2; break; + case 'p': case 'P': *size = RC_MEMSIZE_BIT_3; break; + case 'q': case 'Q': *size = RC_MEMSIZE_BIT_4; break; + case 'r': case 'R': *size = RC_MEMSIZE_BIT_5; break; + case 's': case 'S': *size = RC_MEMSIZE_BIT_6; break; + case 't': case 'T': *size = RC_MEMSIZE_BIT_7; break; + case 'l': case 'L': *size = RC_MEMSIZE_LOW; break; + case 'u': case 'U': *size = RC_MEMSIZE_HIGH; break; + case 'k': case 'K': *size = RC_MEMSIZE_BITCOUNT; break; + case 'w': case 'W': *size = RC_MEMSIZE_24_BITS; break; + case 'g': case 'G': *size = RC_MEMSIZE_32_BITS_BE; break; + case 'i': case 'I': *size = RC_MEMSIZE_16_BITS_BE; break; + case 'j': case 'J': *size = RC_MEMSIZE_24_BITS_BE; break; + + /* case 'v': case 'V': */ + /* case 'y': case 'Y': 64 bit? */ + /* case 'z': case 'Z': 128 bit? */ + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + /* legacy support - addresses without a size prefix are assumed to be 16-bit */ + aux--; + *size = RC_MEMSIZE_16_BITS; + break; + + default: + return RC_INVALID_MEMORY_OPERAND; + } + } + else if (aux[0] == 'f' || aux[0] == 'F') { + ++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; + + default: + return RC_INVALID_FP_OPERAND; + } + } + else { + return RC_INVALID_MEMORY_OPERAND; + } + + value = strtoul(aux, &end, 16); + + if (end == aux) + return RC_INVALID_MEMORY_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + *address = (uint32_t)value; + *memaddr = end; + return RC_OK; +} + +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 uint32_t implied_bit = 1 << 23; + const uint32_t mantissa = mantissa_bits | implied_bit; + double dbl = ((double)mantissa) / ((double)implied_bit); + + if (exponent > 127) { + /* exponent above 127 is a special number */ + if (mantissa_bits == 0) { + /* infinity */ +#ifdef INFINITY /* INFINITY and NAN #defines require C99 */ + dbl = INFINITY; +#else + dbl = -log(0.0); +#endif + } + else { + /* NaN */ +#ifdef NAN + dbl = NAN; +#else + dbl = -sqrt(-1); +#endif + } + } + else if (exponent > 0) { + /* exponent from 1 to 127 is a number greater than 1 */ + while (exponent > 30) { + dbl *= (double)(1 << 30); + exponent -= 30; + } + dbl *= (double)((long long)1 << exponent); + } + else if (exponent < 0) { + /* exponent from -1 to -127 is a number less than 1 */ + + if (exponent == -127) { + /* exponent -127 (all exponent bits were zero) is a denormalized value + * (no implied leading bit) with exponent -126 */ + dbl = ((double)mantissa_bits) / ((double)implied_bit); + exponent = 126; + } else { + exponent = -exponent; + } + + while (exponent > 30) { + dbl /= (double)(1 << 30); + exponent -= 30; + } + dbl /= (double)((long long)1 << exponent); + } + else { + /* exponent of 0 requires no adjustment */ + } + + return (sign) ? (float)-dbl : (float)dbl; +} + +static void rc_transform_memref_float(rc_typed_value_t* value) { + /* decodes an IEEE 754 float */ + 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 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 & 0xFF) - 129; + const int sign = (value->value.u32 & 0x00008000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + +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 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) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + +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, uint8_t size) { + /* ASSERT: value->type == RC_VALUE_TYPE_UNSIGNED */ + switch (size) + { + case RC_MEMSIZE_8_BITS: + value->value.u32 = (value->value.u32 & 0x000000ff); + break; + + case RC_MEMSIZE_16_BITS: + value->value.u32 = (value->value.u32 & 0x0000ffff); + break; + + case RC_MEMSIZE_24_BITS: + value->value.u32 = (value->value.u32 & 0x00ffffff); + break; + + case RC_MEMSIZE_32_BITS: + break; + + case RC_MEMSIZE_BIT_0: + value->value.u32 = (value->value.u32 >> 0) & 1; + break; + + case RC_MEMSIZE_BIT_1: + value->value.u32 = (value->value.u32 >> 1) & 1; + break; + + case RC_MEMSIZE_BIT_2: + value->value.u32 = (value->value.u32 >> 2) & 1; + break; + + case RC_MEMSIZE_BIT_3: + value->value.u32 = (value->value.u32 >> 3) & 1; + break; + + case RC_MEMSIZE_BIT_4: + value->value.u32 = (value->value.u32 >> 4) & 1; + break; + + case RC_MEMSIZE_BIT_5: + value->value.u32 = (value->value.u32 >> 5) & 1; + break; + + case RC_MEMSIZE_BIT_6: + value->value.u32 = (value->value.u32 >> 6) & 1; + break; + + case RC_MEMSIZE_BIT_7: + value->value.u32 = (value->value.u32 >> 7) & 1; + break; + + case RC_MEMSIZE_LOW: + value->value.u32 = value->value.u32 & 0x0f; + break; + + case RC_MEMSIZE_HIGH: + value->value.u32 = (value->value.u32 >> 4) & 0x0f; + break; + + case RC_MEMSIZE_BITCOUNT: + value->value.u32 = rc_bits_set[(value->value.u32 & 0x0F)] + + rc_bits_set[((value->value.u32 >> 4) & 0x0F)]; + break; + + case RC_MEMSIZE_16_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF00) >> 8) | + ((value->value.u32 & 0x00FF) << 8); + break; + + case RC_MEMSIZE_24_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF0000) >> 16) | + (value->value.u32 & 0x00FF00) | + ((value->value.u32 & 0x0000FF) << 16); + break; + + case RC_MEMSIZE_32_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x0000FF00) << 8) | + ((value->value.u32 & 0x000000FF) << 24); + break; + + case RC_MEMSIZE_FLOAT: + 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; + + case RC_MEMSIZE_MBF32_LE: + rc_transform_memref_mbf32_le(value); + break; + + default: + break; + } +} + +static const uint32_t rc_memref_masks[] = { + 0x000000ff, /* RC_MEMSIZE_8_BITS */ + 0x0000ffff, /* RC_MEMSIZE_16_BITS */ + 0x00ffffff, /* RC_MEMSIZE_24_BITS */ + 0xffffffff, /* RC_MEMSIZE_32_BITS */ + 0x0000000f, /* RC_MEMSIZE_LOW */ + 0x000000f0, /* RC_MEMSIZE_HIGH */ + 0x00000001, /* RC_MEMSIZE_BIT_0 */ + 0x00000002, /* RC_MEMSIZE_BIT_1 */ + 0x00000004, /* RC_MEMSIZE_BIT_2 */ + 0x00000008, /* RC_MEMSIZE_BIT_3 */ + 0x00000010, /* RC_MEMSIZE_BIT_4 */ + 0x00000020, /* RC_MEMSIZE_BIT_5 */ + 0x00000040, /* RC_MEMSIZE_BIT_6 */ + 0x00000080, /* RC_MEMSIZE_BIT_7 */ + 0x000000ff, /* RC_MEMSIZE_BITCOUNT */ + 0x0000ffff, /* RC_MEMSIZE_16_BITS_BE */ + 0x00ffffff, /* RC_MEMSIZE_24_BITS_BE */ + 0xffffffff, /* RC_MEMSIZE_32_BITS_BE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT */ + 0xffffffff, /* RC_MEMSIZE_MBF32 */ + 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT_BE */ + 0xffffffff /* RC_MEMSIZE_VARIABLE */ +}; + +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; + + return rc_memref_masks[index]; +} + +/* 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 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 */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_LOW */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_HIGH */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_0 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_1 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_2 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_3 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_4 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_5 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_6 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_7 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BITCOUNT */ + RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS_BE */ + 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 */ +}; + +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; + + return rc_memref_shared_sizes[index]; +} + +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud) { + if (!peek) + return 0; + + switch (size) + { + case RC_MEMSIZE_8_BITS: + return peek(address, 1, ud); + + case RC_MEMSIZE_16_BITS: + return peek(address, 2, ud); + + case RC_MEMSIZE_32_BITS: + return peek(address, 4, ud); + + default: + { + 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; + + /* fetch the larger value and mask off the bits associated to the specified size + * for correct deduction of prior value. non-prior memrefs should already be using + * shared size memrefs to minimize the total number of memory reads required. */ + value = rc_peek_value(address, rc_memref_shared_sizes[index], peek, ud); + return value & rc_memref_masks[index]; + } + } +} + +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t new_value) { + if (memref->value == new_value) { + memref->changed = 0; + } + else { + memref->prior = memref->value; + memref->value = new_value; + memref->changed = 1; + } +} + +void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud) { + while (memref) { + /* indirect memory references are not shared and will be updated in rc_get_memref_value */ + if (!memref->value.is_indirect) + rc_update_memref_value(&memref->value, rc_peek_value(memref->address, memref->value.size, peek, ud)); + + memref = memref->next; + } +} + +void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) { + parse->first_memref = memrefs; + *memrefs = 0; +} + +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. + * this helps the compiler to optimize if it turns the switch into a series of if/elses */ + case RC_OPERAND_ADDRESS: + return memref->value; + + case RC_OPERAND_DELTA: + if (!memref->changed) { + /* fallthrough */ + default: + return memref->value; + } + /* fallthrough */ + case RC_OPERAND_PRIOR: + return memref->prior; + } +} + +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 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)); + } + + return rc_get_memref_value_value(&memref->value, operand_type); +} diff --git a/3rdparty/rcheevos/src/rcheevos/operand.c b/3rdparty/rcheevos/src/rcheevos/operand.c new file mode 100644 index 0000000000..2522582752 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/operand.c @@ -0,0 +1,475 @@ +#include "rc_internal.h" + +#include +#include +#include + +#ifndef RC_DISABLE_LUA + +RC_BEGIN_C_DECLS + +#include +#include + +RC_END_C_DECLS + +#endif /* RC_DISABLE_LUA */ + +static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { + const char* aux = *memaddr; +#ifndef RC_DISABLE_LUA + const char* id; +#endif + + if (*aux++ != '@') { + return RC_INVALID_LUA_OPERAND; + } + + if (!isalpha((unsigned char)*aux)) { + return RC_INVALID_LUA_OPERAND; + } + +#ifndef RC_DISABLE_LUA + id = aux; +#endif + + while (isalnum((unsigned char)*aux) || *aux == '_') { + aux++; + } + +#ifndef RC_DISABLE_LUA + + if (parse->L != 0) { + if (!lua_istable(parse->L, parse->funcs_ndx)) { + return RC_INVALID_LUA_OPERAND; + } + + lua_pushlstring(parse->L, id, aux - id); + lua_gettable(parse->L, parse->funcs_ndx); + + if (!lua_isfunction(parse->L, -1)) { + lua_pop(parse->L, 1); + return RC_INVALID_LUA_OPERAND; + } + + self->value.luafunc = luaL_ref(parse->L, LUA_REGISTRYINDEX); + } + +#else + (void)parse; +#endif /* RC_DISABLE_LUA */ + + self->type = RC_OPERAND_LUA; + *memaddr = aux; + return RC_OK; +} + +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; + uint32_t address; + uint8_t size; + int ret; + + switch (*aux) { + case 'd': case 'D': + self->type = RC_OPERAND_DELTA; + ++aux; + break; + + case 'p': case 'P': + self->type = RC_OPERAND_PRIOR; + ++aux; + break; + + case 'b': case 'B': + self->type = RC_OPERAND_BCD; + ++aux; + break; + + case '~': + self->type = RC_OPERAND_INVERTED; + ++aux; + break; + + default: + self->type = RC_OPERAND_ADDRESS; + break; + } + + ret = rc_parse_memref(&aux, &self->size, &address); + if (ret != RC_OK) + return ret; + + size = rc_memref_shared_size(self->size); + if (size != self->size && self->type == RC_OPERAND_PRIOR) { + /* if the shared size differs from the requested size and it's a prior operation, we + * have to check to make sure both sizes use the same mask, or the prior value may be + * updated when bits outside the mask are modified, which would make it look like the + * current value once the mask is applied. if the mask differs, create a new + * non-shared record for tracking the prior data. */ + if (rc_memref_mask(size) != rc_memref_mask(self->size)) + size = self->size; + } + + self->value.memref = rc_alloc_memref(parse, address, size, is_indirect); + if (parse->offset < 0) + return parse->offset; + + *memaddr = aux; + return RC_OK; +} + +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; + unsigned long value; + int negative; + int allow_decimal = 0; + + self->size = RC_MEMSIZE_32_BITS; + + switch (*aux) { + case 'h': case 'H': /* hex constant */ + if (aux[2] == 'x' || aux[2] == 'X') { + /* H0x1234 is a typo - either H1234 or 0xH1234 was probably meant */ + return RC_INVALID_CONST_OPERAND; + } + + value = strtoul(++aux, &end, 16); + if (end == aux) + return RC_INVALID_CONST_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + self->type = RC_OPERAND_CONST; + self->value.num = (unsigned)value; + + aux = end; + break; + + case 'f': case 'F': /* floating point constant */ + if (isalpha((unsigned char)aux[1])) { + ret = rc_parse_operand_memory(self, &aux, parse, is_indirect); + + if (ret < 0) + return ret; + + break; + } + allow_decimal = 1; + /* fall through */ + case 'v': case 'V': /* signed integer constant */ + ++aux; + /* fall through */ + case '+': case '-': /* signed integer constant */ + negative = 0; + if (*aux == '-') { + negative = 1; + ++aux; + } + else if (*aux == '+') { + ++aux; + } + + value = strtoul(aux, &end, 10); + + if (*end == '.' && allow_decimal) { + /* custom parser for decimal values to ignore locale */ + unsigned long shift = 1; + unsigned long fraction = 0; + + aux = end + 1; + if (*aux < '0' || *aux > '9') + return RC_INVALID_FP_OPERAND; + + do { + /* only keep as many digits as will fit in a 32-bit value to prevent overflow. + * float only has around 7 digits of precision anyway. */ + if (shift < 1000000000) { + fraction *= 10; + fraction += (*aux - '0'); + shift *= 10; + } + ++aux; + } while (*aux >= '0' && *aux <= '9'); + + if (fraction != 0) { + /* non-zero fractional part, convert to double and merge in integer portion */ + const double dbl_fraction = ((double)fraction) / ((double)shift); + if (negative) + self->value.dbl = ((double)(-((long)value))) - dbl_fraction; + else + self->value.dbl = (double)value + dbl_fraction; + } + else { + /* fractional part is 0, just convert the integer portion */ + if (negative) + self->value.dbl = (double)(-((long)value)); + else + self->value.dbl = (double)value; + } + + self->type = RC_OPERAND_FP; + } + else { + /* not a floating point value, make sure something was read and advance the read pointer */ + if (end == aux) + return allow_decimal ? RC_INVALID_FP_OPERAND : RC_INVALID_CONST_OPERAND; + + aux = end; + + if (value > 0x7fffffffU) + value = 0x7fffffffU; + + self->type = RC_OPERAND_CONST; + + if (negative) + self->value.num = (unsigned)(-((long)value)); + else + self->value.num = (unsigned)value; + } + break; + + case '0': + if (aux[1] == 'x' || aux[1] == 'X') { /* hex integer constant */ + /* fallthrough */ /* to default */ + default: + ret = rc_parse_operand_memory(self, &aux, parse, is_indirect); + + if (ret < 0) + return ret; + + break; + } + /* fallthrough */ /* to case '1' for case '0' where not '0x' */ + case '1': case '2': case '3': case '4': case '5': /* unsigned integer constant */ + case '6': case '7': case '8': case '9': + value = strtoul(aux, &end, 10); + if (end == aux) + return RC_INVALID_CONST_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + self->type = RC_OPERAND_CONST; + self->value.num = (unsigned)value; + + aux = end; + break; + + case '@': + ret = rc_parse_operand_lua(self, &aux, parse); + + if (ret < 0) + return ret; + + break; + } + + *memaddr = aux; + return RC_OK; +} + +#ifndef RC_DISABLE_LUA + +typedef struct { + rc_peek_t peek; + void* ud; +} +rc_luapeek_t; + +static int rc_luapeek(lua_State* L) { + 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); + + uint32_t value = luapeek->peek(address, num_bytes, luapeek->ud); + + lua_pushinteger(L, value); + return 1; +} + +#endif /* RC_DISABLE_LUA */ + +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; + + default: + return 0; + } +} + +int rc_operand_is_memref(const rc_operand_t* self) { + switch (self->type) { + case RC_OPERAND_CONST: + case RC_OPERAND_FP: + case RC_OPERAND_LUA: + return 0; + + default: + return 1; + } +} + +int rc_operand_is_float(const rc_operand_t* self) { + if (self->type == RC_OPERAND_FP) + return 1; + + return rc_operand_is_float_memref(self); +} + +uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { + switch (self->type) + { + case RC_OPERAND_BCD: + switch (self->size) + { + case RC_MEMSIZE_8_BITS: + value = ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + value = ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + value = ((value >> 20) & 0x0f) * 100000 + + ((value >> 16) & 0x0f) * 10000 + + ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_32_BITS_BE: + case RC_MEMSIZE_VARIABLE: + value = ((value >> 28) & 0x0f) * 10000000 + + ((value >> 24) & 0x0f) * 1000000 + + ((value >> 20) & 0x0f) * 100000 + + ((value >> 16) & 0x0f) * 10000 + + ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + default: + break; + } + break; + + case RC_OPERAND_INVERTED: + switch (self->size) + { + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + value ^= 0x0f; + break; + + case RC_MEMSIZE_8_BITS: + value ^= 0xff; + break; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + value ^= 0xffff; + break; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + value ^= 0xffffff; + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_32_BITS_BE: + case RC_MEMSIZE_VARIABLE: + value ^= 0xffffffff; + break; + + default: + value ^= 0x01; + break; + } + break; + + default: + break; + } + + return value; +} + +void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_state_t* eval_state) { +#ifndef RC_DISABLE_LUA + rc_luapeek_t luapeek; +#endif /* RC_DISABLE_LUA */ + + /* step 1: read memory */ + switch (self->type) { + case RC_OPERAND_CONST: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = self->value.num; + return; + + case RC_OPERAND_FP: + result->type = RC_VALUE_TYPE_FLOAT; + result->value.f32 = (float)self->value.dbl; + return; + + case RC_OPERAND_LUA: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = 0; + +#ifndef RC_DISABLE_LUA + if (eval_state->L != 0) { + lua_rawgeti(eval_state->L, LUA_REGISTRYINDEX, self->value.luafunc); + lua_pushcfunction(eval_state->L, rc_luapeek); + + luapeek.peek = eval_state->peek; + luapeek.ud = eval_state->peek_userdata; + + lua_pushlightuserdata(eval_state->L, &luapeek); + + if (lua_pcall(eval_state->L, 2, 1, 0) == LUA_OK) { + if (lua_isboolean(eval_state->L, -1)) { + result->value.u32 = (uint32_t)lua_toboolean(eval_state->L, -1); + } + else { + result->value.u32 = (uint32_t)lua_tonumber(eval_state->L, -1); + } + } + + lua_pop(eval_state->L, 1); + } + +#endif /* RC_DISABLE_LUA */ + + break; + + default: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = rc_get_memref_value(self->value.memref, self->type, eval_state); + break; + } + + /* step 2: convert read memory to desired format */ + rc_transform_memref_value(result, self->size); + + /* step 3: apply logic (BCD/invert) */ + if (result->type == RC_VALUE_TYPE_UNSIGNED) + result->value.u32 = rc_transform_operand_value(result->value.u32, self); +} diff --git a/3rdparty/rcheevos/src/rcheevos/rc_internal.h b/3rdparty/rcheevos/src/rcheevos/rc_internal.h new file mode 100644 index 0000000000..1354239809 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/rc_internal.h @@ -0,0 +1,197 @@ +#ifndef RC_INTERNAL_H +#define RC_INTERNAL_H + +#include "rc_runtime_types.h" +#include "rc_util.h" + +RC_BEGIN_C_DECLS + +typedef struct rc_scratch_string { + char* value; + struct rc_scratch_string* left; + struct rc_scratch_string* right; +} +rc_scratch_string_t; + +#define RC_ALLOW_ALIGN(T) struct __align_ ## T { char ch; T t; }; +RC_ALLOW_ALIGN(rc_condition_t) +RC_ALLOW_ALIGN(rc_condset_t) +RC_ALLOW_ALIGN(rc_lboard_t) +RC_ALLOW_ALIGN(rc_memref_t) +RC_ALLOW_ALIGN(rc_operand_t) +RC_ALLOW_ALIGN(rc_richpresence_t) +RC_ALLOW_ALIGN(rc_richpresence_display_t) +RC_ALLOW_ALIGN(rc_richpresence_display_part_t) +RC_ALLOW_ALIGN(rc_richpresence_lookup_t) +RC_ALLOW_ALIGN(rc_richpresence_lookup_item_t) +RC_ALLOW_ALIGN(rc_scratch_string_t) +RC_ALLOW_ALIGN(rc_trigger_t) +RC_ALLOW_ALIGN(rc_value_t) +RC_ALLOW_ALIGN(char) + +#define RC_ALIGNOF(T) (sizeof(struct __align_ ## T) - sizeof(T)) +#define RC_OFFSETOF(o, t) (int)((char*)&(o.t) - (char*)&(o)) + +#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_buffer_t buffer; + rc_scratch_string_t* strings; + + struct objs { + rc_condition_t* __rc_condition_t; + rc_condset_t* __rc_condset_t; + rc_lboard_t* __rc_lboard_t; + rc_memref_t* __rc_memref_t; + rc_operand_t* __rc_operand_t; + rc_richpresence_t* __rc_richpresence_t; + rc_richpresence_display_t* __rc_richpresence_display_t; + rc_richpresence_display_part_t* __rc_richpresence_display_part_t; + rc_richpresence_lookup_t* __rc_richpresence_lookup_t; + rc_richpresence_lookup_item_t* __rc_richpresence_lookup_item_t; + rc_scratch_string_t __rc_scratch_string_t; + rc_trigger_t* __rc_trigger_t; + rc_value_t* __rc_value_t; + } objs; +} +rc_scratch_t; + +enum { + RC_VALUE_TYPE_NONE, + RC_VALUE_TYPE_UNSIGNED, + RC_VALUE_TYPE_SIGNED, + RC_VALUE_TYPE_FLOAT +}; + +typedef struct { + union { + uint32_t u32; + int32_t i32; + float f32; + } value; + + char type; +} +rc_typed_value_t; + +#define RC_MEASURED_UNKNOWN 0xFFFFFFFF + +typedef struct { + rc_typed_value_t add_value;/* AddSource/SubSource */ + 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 */ + 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 { + int32_t offset; + + lua_State* L; + int funcs_ndx; + + void* buffer; + rc_scratch_t scratch; + + rc_memref_t** first_memref; + rc_value_t** variables; + + uint32_t measured_target; + int lines_read; + + uint8_t has_required_hits; + uint8_t measured_as_percent; +} +rc_parse_state_t; + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer, lua_State* L, int funcs_ndx); +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, 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, 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, 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); + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, int is_value); +int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state); +void rc_reset_condset(rc_condset_t* self); + +enum { + RC_PROCESSING_COMPARE_DEFAULT = 0, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_ALWAYS_TRUE, + RC_PROCESSING_COMPARE_ALWAYS_FALSE +}; + +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, 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); + +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); +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); +void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount); +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, 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); + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse); + +RC_END_C_DECLS + +#endif /* RC_INTERNAL_H */ diff --git a/3rdparty/rcheevos/src/rcheevos/rc_validate.c b/3rdparty/rcheevos/src/rcheevos/rc_validate.c new file mode 100644 index 0000000000..1dc6a87425 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/rc_validate.c @@ -0,0 +1,854 @@ +#include "rc_validate.h" + +#include "rc_consoles.h" +#include "rc_internal.h" + +#include "../rc_compat.h" + +#include +#include + +static int rc_validate_memref(const rc_memref_t* memref, char result[], const size_t result_size, uint32_t console_id, uint32_t max_address) +{ + if (memref->address > max_address) { + snprintf(result, result_size, "Address %04X out of range (max %04X)", memref->address, max_address); + return 0; + } + + switch (console_id) { + case RC_CONSOLE_NINTENDO: + if (memref->address >= 0x0800 && memref->address <= 0x1FFF) { + snprintf(result, result_size, "Mirror RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_COLOR: + if (memref->address >= 0xE000 && memref->address <= 0xFDFF) { + snprintf(result, result_size, "Echo RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_PLAYSTATION: + if (memref->address <= 0xFFFF) { + snprintf(result, result_size, "Kernel RAM may not be initialized without real BIOS (address %04X)", memref->address); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, uint32_t max_address) +{ + while (memref) { + if (!rc_validate_memref(memref, result, result_size, 0, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static uint32_t rc_console_max_address(uint32_t console_id) +{ + const rc_memory_regions_t* memory_regions; + memory_regions = rc_console_memory_regions(console_id); + if (memory_regions && memory_regions->num_regions > 0) + return memory_regions->region[memory_regions->num_regions - 1].end_address; + + return 0xFFFFFFFF; +} + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, uint32_t console_id) +{ + const uint32_t max_address = rc_console_max_address(console_id); + while (memref) { + if (!rc_validate_memref(memref, result, result_size, console_id, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static uint32_t rc_max_value(const rc_operand_t* operand) +{ + if (operand->type == RC_OPERAND_CONST) + return operand->value.num; + + if (!rc_operand_is_memref(operand)) + return 0xFFFFFFFF; + + switch (operand->size) { + case RC_MEMSIZE_BIT_0: + case RC_MEMSIZE_BIT_1: + case RC_MEMSIZE_BIT_2: + case RC_MEMSIZE_BIT_3: + case RC_MEMSIZE_BIT_4: + case RC_MEMSIZE_BIT_5: + case RC_MEMSIZE_BIT_6: + case RC_MEMSIZE_BIT_7: + return 1; + + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + return 0xF; + + case RC_MEMSIZE_BITCOUNT: + return 8; + + case RC_MEMSIZE_8_BITS: + return (operand->type == RC_OPERAND_BCD) ? 165 : 0xFF; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 16665 : 0xFFFF; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 1666665 : 0xFFFFFF; + + default: + return (operand->type == RC_OPERAND_BCD) ? 166666665 : 0xFFFFFFFF; + } +} + +static uint32_t rc_scale_value(uint32_t value, uint8_t oper, const rc_operand_t* operand) +{ + switch (oper) { + case RC_OPERATOR_MULT: + { + unsigned long long scaled = ((unsigned long long)value) * rc_max_value(operand); + if (scaled > 0xFFFFFFFF) + return 0xFFFFFFFF; + + return (uint32_t)scaled; + } + + case RC_OPERATOR_DIV: + { + const uint32_t min_val = (operand->type == RC_OPERAND_CONST) ? operand->value.num : 1; + return value / min_val; + } + + case RC_OPERATOR_AND: + return rc_max_value(operand); + + case RC_OPERATOR_XOR: + return value | rc_max_value(operand); + + default: + return value; + } +} + +static int rc_validate_get_condition_index(const rc_condset_t* condset, const rc_condition_t* condition) +{ + int index = 1; + const rc_condition_t* scan; + for (scan = condset->conditions; scan != NULL; scan = scan->next) + { + if (scan == condition) + return index; + + ++index; + } + + return 0; +} + +static int rc_validate_range(uint32_t min_val, uint32_t max_val, char oper, uint32_t max, char result[], const size_t result_size) +{ + switch (oper) { + case RC_OPERATOR_AND: + if (min_val > max) { + snprintf(result, result_size, "Mask has more bits than source"); + return 0; + } + else if (min_val == 0 && max_val == 0) { + snprintf(result, result_size, "Result of mask always 0"); + return 0; + } + break; + + case RC_OPERATOR_EQ: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_NE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GT: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_LE: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_LT: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_condset_internal(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t console_id, uint32_t max_address) +{ + const rc_condition_t* cond; + char buffer[128]; + uint32_t max_val; + int index = 1; + unsigned long long add_source_max = 0; + int in_add_hits = 0; + int in_add_address = 0; + int is_combining = 0; + + if (!condset) { + *result = '\0'; + return 1; + } + + for (cond = condset->conditions; cond; cond = cond->next, ++index) { + uint32_t max = rc_max_value(&cond->operand1); + const int is_memref1 = rc_operand_is_memref(&cond->operand1); + const int is_memref2 = rc_operand_is_memref(&cond->operand2); + + if (!in_add_address) { + if (is_memref1 && !rc_validate_memref(cond->operand1.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + if (is_memref2 && !rc_validate_memref(cond->operand2.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + } + else { + in_add_address = 0; + } + + switch (cond->type) { + case RC_CONDITION_ADD_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + add_source_max += max; + is_combining = 1; + continue; + + case RC_CONDITION_SUB_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + if (add_source_max < max) /* potential underflow - may be expected */ + add_source_max = 0xFFFFFFFF; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_ADDRESS: + if (cond->operand1.type == RC_OPERAND_DELTA || cond->operand1.type == RC_OPERAND_PRIOR) { + snprintf(result, result_size, "Condition %d: Using pointer from previous frame", index); + return 0; + } + in_add_address = 1; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_SUB_HITS: + in_add_hits = 1; + is_combining = 1; + break; + + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + is_combining = 1; + break; + + case RC_CONDITION_RESET_IF: + if (cond->required_hits == 1) { + snprintf(result, result_size, "Condition %d: Hit target of 1 is redundant on ResetIf", index); + return 0; + } + /* fallthrough */ /* to default */ + default: + if (in_add_hits) { + if (cond->required_hits == 0) { + snprintf(result, result_size, "Condition %d: Final condition in AddHits chain must have a hit target", index); + return 0; + } + + in_add_hits = 0; + } + + is_combining = 0; + break; + } + + /* if we're in an add source chain, check for overflow */ + if (add_source_max) { + const unsigned long long overflow = add_source_max + max; + if (overflow > 0xFFFFFFFFUL) + max = 0xFFFFFFFF; + else + max += (unsigned)add_source_max; + } + + /* check for comparing two differently sized memrefs */ + max_val = rc_max_value(&cond->operand2); + if (max_val != max && add_source_max == 0 && is_memref1 && is_memref2) { + snprintf(result, result_size, "Condition %d: Comparing different memory sizes", index); + return 0; + } + + /* if either side is a memref, or there's a running add source chain, check for impossible comparisons */ + if (is_memref1 || is_memref2 || add_source_max) { + const size_t prefix_length = snprintf(result, result_size, "Condition %d: ", index); + + uint32_t min_val; + switch (cond->operand2.type) { + case RC_OPERAND_CONST: + min_val = cond->operand2.value.num; + break; + + case RC_OPERAND_FP: + min_val = (int)cond->operand2.value.dbl; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref1 (because operand2==FP means !is_memref2) */ + if (!add_source_max && !rc_operand_is_float_memref(&cond->operand1) && + (float)min_val != cond->operand2.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + + default: + min_val = 0; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref2 (because operand1==FP means !is_memref1) */ + if (cond->operand1.type == RC_OPERAND_FP && !add_source_max && !rc_operand_is_float_memref(&cond->operand2) && + (float)((int)cond->operand1.value.dbl) != cond->operand1.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + } + + if (rc_operand_is_float(&cond->operand2) && rc_operand_is_float(&cond->operand1)) { + /* both sides are floats, don't validate range*/ + } else if (!rc_validate_range(min_val, max_val, cond->oper, max, result + prefix_length, result_size - prefix_length)) { + return 0; + } + } + + add_source_max = 0; + } + + if (is_combining) { + snprintf(result, result_size, "Final condition type expects another condition to follow"); + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t max_address) +{ + return rc_validate_condset_internal(condset, result, result_size, 0, max_address); +} + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t console_id) +{ + const uint32_t max_address = rc_console_max_address(console_id); + return rc_validate_condset_internal(condset, result, result_size, console_id, max_address); +} + +static int rc_validate_is_combining_condition(const rc_condition_t* condition) +{ + switch (condition->type) + { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_SUB_SOURCE: + return 1; + + default: + return 0; + } +} + +static const rc_condition_t* rc_validate_next_non_combining_condition(const rc_condition_t* condition) +{ + int is_combining = rc_validate_is_combining_condition(condition); + for (condition = condition->next; condition != NULL; condition = condition->next) + { + if (rc_validate_is_combining_condition(condition)) + is_combining = 1; + else if (is_combining) + is_combining = 0; + else + return condition; + } + + return NULL; +} + +static int rc_validate_get_opposite_comparison(int oper) +{ + switch (oper) + { + case RC_OPERATOR_EQ: return RC_OPERATOR_NE; + case RC_OPERATOR_NE: return RC_OPERATOR_EQ; + case RC_OPERATOR_LT: return RC_OPERATOR_GE; + case RC_OPERATOR_LE: return RC_OPERATOR_GT; + case RC_OPERATOR_GT: return RC_OPERATOR_LE; + case RC_OPERATOR_GE: return RC_OPERATOR_LT; + default: return oper; + } +} + +static const rc_operand_t* rc_validate_get_comparison(const rc_condition_t* condition, int* comparison, unsigned* value) +{ + if (rc_operand_is_memref(&condition->operand1)) + { + if (condition->operand2.type != RC_OPERAND_CONST) + return NULL; + + *comparison = condition->oper; + *value = condition->operand2.value.num; + return &condition->operand1; + } + + if (condition->operand1.type != RC_OPERAND_CONST) + return NULL; + + if (!rc_operand_is_memref(&condition->operand2)) + return NULL; + + *comparison = rc_validate_get_opposite_comparison(condition->oper); + *value = condition->operand1.value.num; + return &condition->operand2; +} + +enum { + RC_OVERLAP_NONE = 0, + RC_OVERLAP_CONFLICTING, + RC_OVERLAP_REDUNDANT, + RC_OVERLAP_DEFER +}; + +static int rc_validate_comparison_overlap(int comparison1, uint32_t value1, int comparison2, uint32_t value2) +{ + /* NOTE: this only cares if comp2 conflicts with comp1. + * If comp1 conflicts with comp2, we'll catch that later (return RC_OVERLAP_NONE for now) */ + switch (comparison2) + { + case RC_OPERATOR_EQ: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a == 1 | a == 1 && a == 2 | a == 2 && a == 1 */ + /* redundant conflict conflict */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a == 1 | a <= 1 && a == 2 | a <= 2 && a == 1 */ + /* defer conflict defer */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a == 1 | a >= 1 && a == 2 | a >= 2 && a == 1 */ + /* defer defer conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a == 1 | a != 1 && a == 2 | a != 2 && a == 1 */ + /* conflict defer defer */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_LT: /* a < 1 && a == 1 | a < 1 && a == 2 | a < 2 && a == 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a == 1 | a > 1 && a == 2 | a > 2 && a == 1 */ + /* conflict defer conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_NE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a != 1 | a == 1 && a != 2 | a == 2 && a != 1 */ + /* conflict redundant redundant */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_REDUNDANT; + case RC_OPERATOR_LE: /* a <= 1 && a != 1 | a <= 1 && a != 2 | a <= 2 && a != 1 */ + /* none redundant none */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a != 1 | a >= 1 && a != 2 | a >= 2 && a != 1 */ + /* none none redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a != 1 | a != 1 && a != 2 | a != 2 && a != 1 */ + /* redundant none none */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a != 1 | a < 1 && a != 2 | a < 2 && a != 1 */ + /* redundant redundant none */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a != 1 | a > 1 && a != 2 | a > 2 && a != 1 */ + /* redundant none redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a < 1 | a == 1 && a < 2 | a == 2 && a < 1 */ + /* conflict redundant conflict */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a < 1 | a <= 1 && a < 2 | a <= 2 && a < 1 */ + /* defer redundant defer */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a < 1 | a >= 1 && a < 2 | a >= 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a < 1 | a != 1 && a < 2 | a != 2 && a < 1 */ + /* defer none defer */ + return (value1 >= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a < 1 | a < 1 && a < 2 | a < 2 && a < 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a < 1 | a > 1 && a < 2 | a > 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a <= 1 | a == 1 && a <= 2 | a == 2 && a <= 1 */ + /* redundant redundant conflict */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a <= 1 | a <= 1 && a <= 2 | a <= 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a <= 1 | a >= 1 && a <= 2 | a >= 2 && a <= 1 */ + /* none none conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a <= 1 | a != 1 && a <= 2 | a != 2 && a <= 1 */ + /* none none defer */ + return (value1 > value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a <= 1 | a < 1 && a <= 2 | a < 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a <= 1 | a > 1 && a <= 2 | a > 2 && a <= 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_GT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a > 1 | a == 1 && a > 2 | a == 2 && a > 1 */ + /* conflict conflict redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a > 1 | a <= 1 && a > 2 | a <= 2 && a > 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a > 1 | a >= 1 && a > 2 | a >= 2 && a > 1 */ + /* defer defer redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a > 1 | a != 1 && a > 2 | a != 2 && a > 1 */ + /* defer defer none */ + return (value1 <= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a > 1 | a < 1 && a > 2 | a < 2 && a > 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a > 1 | a > 1 && a > 2 | a > 2 && a > 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_GE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a >= 1 | a == 1 && a >= 2 | a == 2 && a >= 1 */ + /* redundant conflict redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a >= 1 | a <= 1 && a >= 2 | a <= 2 && a >= 1 */ + /* none conflict none */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a >= 1 | a >= 1 && a >= 2 | a >= 2 && a >= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a >= 1 | a != 1 && a >= 2 | a != 2 && a >= 1 */ + /* none defer none */ + return (value1 < value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a >= 1 | a < 1 && a >= 2 | a < 2 && a >= 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a >= 1 | a > 1 && a >= 2 | a > 2 && a >= 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + } + + return RC_OVERLAP_NONE; +} + +static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, const rc_condset_t* compare_conditions, + const char* prefix, const char* compare_prefix, char result[], const size_t result_size) +{ + int comparison1, comparison2; + uint32_t value1, value2; + const rc_operand_t* operand1; + const rc_operand_t* operand2; + const rc_condition_t* compare_condition; + const rc_condition_t* condition; + int overlap; + + /* empty group */ + if (conditions == NULL || compare_conditions == NULL) + return 1; + + /* outer loop is the source conditions */ + for (condition = conditions->conditions; condition != NULL; + condition = rc_validate_next_non_combining_condition(condition)) + { + /* hits can be captured at any time, so any potential conflict will not be conflicting at another time */ + if (condition->required_hits) + continue; + + operand1 = rc_validate_get_comparison(condition, &comparison1, &value1); + if (!operand1) + continue; + + switch (condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison1 = rc_validate_get_opposite_comparison(comparison1); + break; + default: + if (rc_validate_is_combining_condition(condition)) + continue; + break; + } + + /* inner loop is the potentially conflicting conditions */ + for (compare_condition = compare_conditions->conditions; compare_condition != NULL; + compare_condition = rc_validate_next_non_combining_condition(compare_condition)) + { + if (compare_condition == condition) + continue; + + if (compare_condition->required_hits) + continue; + + operand2 = rc_validate_get_comparison(compare_condition, &comparison2, &value2); + if (!operand2 || operand2->value.memref->address != operand1->value.memref->address || + operand2->size != operand1->size || operand2->type != operand1->type) + continue; + + switch (compare_condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison2 = rc_validate_get_opposite_comparison(comparison2); + break; + default: + if (rc_validate_is_combining_condition(compare_condition)) + continue; + break; + } + + overlap = rc_validate_comparison_overlap(comparison1, value1, comparison2, value2); + switch (overlap) + { + case RC_OVERLAP_CONFLICTING: + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf conflicts between groups, unless both conditions are PauseIfs */ + if (conditions != compare_conditions && compare_condition->type != condition->type) + continue; + } + break; + + case RC_OVERLAP_REDUNDANT: + if (prefix != compare_prefix && strcmp(compare_prefix, "Core") == 0) + { + /* if the alt condition is more restrictive than the core condition, ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + } + + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + /* if the PauseIf is less restrictive than the other condition, it's just a guard. ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + + /* PauseIf redundant with ResetIf is a conflict (both are inverted comparisons) */ + if (compare_condition->type == RC_CONDITION_RESET_IF || condition->type == RC_CONDITION_RESET_IF) + overlap = RC_OVERLAP_CONFLICTING; + } + else if (compare_condition->type == RC_CONDITION_RESET_IF && condition->type != RC_CONDITION_RESET_IF) + { + /* only ever report the redundancy on the non-ResetIf condition. The ResetIf is allowed to + * fire when the non-ResetIf condition is not true. */ + continue; + } + else if (compare_condition->type == RC_CONDITION_MEASURED_IF || condition->type == RC_CONDITION_MEASURED_IF) + { + /* ignore MeasuredIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + if (compare_condition->type == RC_CONDITION_MEASURED_IF && condition->type != RC_CONDITION_MEASURED_IF) + { + /* only ever report the redundancy on the non-MeasuredIf condition. The MeasuredIf provides + * additional functionality. */ + continue; + } + } + else if (compare_condition->type == RC_CONDITION_TRIGGER || condition->type == RC_CONDITION_TRIGGER) + { + /* Trigger is allowed to be redundant with non-trigger conditions as there may be limits that start a + * challenge that are furhter reduced for the completion of the challenge */ + if (compare_condition->type != condition->type) + continue; + } + break; + + default: + continue; + } + + if (compare_prefix && *compare_prefix) + { + snprintf(result, result_size, "%s Condition %d: %s with %s Condition %d", + compare_prefix, rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + prefix, rc_validate_get_condition_index(conditions, condition)); + } + else + { + snprintf(result, result_size, "Condition %d: %s with Condition %d", + rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + rc_validate_get_condition_index(conditions, condition)); + } + return 0; + } + } + + return 1; +} + +static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t console_id, uint32_t max_address) +{ + const rc_condset_t* alt; + int index; + + if (!trigger->alternative) + { + if (!rc_validate_condset_internal(trigger->requirement, result, result_size, console_id, max_address)) + return 0; + + return rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "", "", result, result_size); + } + + snprintf(result, result_size, "Core "); + if (!rc_validate_condset_internal(trigger->requirement, result + 5, result_size - 5, console_id, max_address)) + return 0; + + /* compare core to itself */ + if (!rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "Core", "Core", result, result_size)) + return 0; + + index = 1; + for (alt = trigger->alternative; alt; alt = alt->next, ++index) { + char altname[16]; + const size_t prefix_length = snprintf(result, result_size, "Alt%d ", index); + if (!rc_validate_condset_internal(alt, result + prefix_length, result_size - prefix_length, console_id, max_address)) + return 0; + + /* compare alt to itself */ + snprintf(altname, sizeof(altname), "Alt%d", index); + if (!rc_validate_conflicting_conditions(alt, alt, altname, altname, result, result_size)) + return 0; + + /* compare alt to core */ + if (!rc_validate_conflicting_conditions(trigger->requirement, alt, "Core", altname, result, result_size)) + return 0; + + /* compare core to alt */ + if (!rc_validate_conflicting_conditions(alt, trigger->requirement, altname, "Core", result, result_size)) + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t max_address) +{ + return rc_validate_trigger_internal(trigger, result, result_size, 0, max_address); +} + +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t console_id) +{ + const uint32_t max_address = rc_console_max_address(console_id); + return rc_validate_trigger_internal(trigger, result, result_size, console_id, max_address); +} diff --git a/3rdparty/rcheevos/src/rcheevos/rc_validate.h b/3rdparty/rcheevos/src/rcheevos/rc_validate.h new file mode 100644 index 0000000000..09c90ab7f1 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/rc_validate.h @@ -0,0 +1,22 @@ +#ifndef RC_VALIDATE_H +#define RC_VALIDATE_H + +#include "rc_runtime_types.h" + +#include + +RC_BEGIN_C_DECLS + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, uint32_t max_address); + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t max_address); +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t max_address); + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, uint32_t console_id); + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t console_id); +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t console_id); + +RC_END_C_DECLS + +#endif /* RC_VALIDATE_H */ diff --git a/3rdparty/rcheevos/src/rcheevos/richpresence.c b/3rdparty/rcheevos/src/rcheevos/richpresence.c new file mode 100644 index 0000000000..0dd660e686 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/richpresence.c @@ -0,0 +1,829 @@ +#include "rc_internal.h" + +#include "../rc_compat.h" + +#include + +/* special formats only used by rc_richpresence_display_part_t.display_type. must not overlap other RC_FORMAT values */ +enum { + RC_FORMAT_STRING = 101, + RC_FORMAT_LOOKUP = 102, + RC_FORMAT_UNKNOWN_MACRO = 103, + RC_FORMAT_ASCIICHAR = 104, + RC_FORMAT_UNICODECHAR = 105 +}; + +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; + uint32_t address; + uint8_t size; + + /* single memory reference lookups without a modifier flag can be handled without a variable */ + end = memaddr; + if (rc_parse_memref(&end, &size, &address) == RC_OK) { + /* make sure the entire memaddr was consumed. if not, there's an operator and it's a comparison, not a memory reference */ + if (end == &memaddr[memaddr_len]) { + /* if it's not a derived size, we can reference the memref directly */ + if (rc_memref_shared_size(size) == size) + return &rc_alloc_memref(parse, address, size, 0)->value; + } + } + + /* not a simple memory reference, need to create a variable */ + variable = rc_alloc_helper_variable(memaddr, memaddr_len, parse); + if (!variable) + return NULL; + + return &variable->value; +} + +static const char* rc_parse_line(const char* line, const char** end, rc_parse_state_t* parse) { + const char* nextline; + const char* endline; + + /* get a single line */ + nextline = line; + while (*nextline && *nextline != '\n') + ++nextline; + + /* if a trailing comment marker (//) exists, the line stops there */ + endline = line; + while (endline < nextline && (endline[0] != '/' || endline[1] != '/' || (endline > line && endline[-1] == '\\'))) + ++endline; + + if (endline == nextline) { + /* trailing whitespace on a line without a comment marker may be significant, just remove the line ending */ + if (endline > line && endline[-1] == '\r') + --endline; + } else { + /* remove trailing whitespace before the comment marker */ + while (endline > line && isspace((int)((unsigned char*)endline)[-1])) + --endline; + } + + /* point end at the first character to ignore, it makes subtraction for length easier */ + *end = endline; + + /* tally the line */ + ++parse->lines_read; + + /* skip the newline character so we're pointing at the next line */ + if (*nextline == '\n') + ++nextline; + + return nextline; +} + +typedef struct rc_richpresence_builtin_macro_t { + const char* name; + size_t name_len; + 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) { + rc_richpresence_display_t* self; + rc_richpresence_display_part_t* part; + rc_richpresence_display_part_t** next; + rc_richpresence_lookup_t* lookup; + const char* ptr; + const char* in; + char* out; + + if (endline - line < 1) { + parse->offset = RC_MISSING_DISPLAY_STRING; + return 0; + } + + { + self = RC_ALLOC(rc_richpresence_display_t, parse); + memset(self, 0, sizeof(rc_richpresence_display_t)); + next = &self->display; + } + + /* break the string up on macros: text @macro() moretext */ + do { + ptr = line; + while (ptr < endline) { + if (*ptr == '@' && (ptr == line || ptr[-1] != '\\')) /* ignore escaped @s */ + break; + + ++ptr; + } + + if (ptr > line) { + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + /* handle string part */ + part->display_type = RC_FORMAT_STRING; + part->text = rc_alloc_str(parse, line, (int)(ptr - line)); + if (part->text) { + /* remove backslashes used for escaping */ + in = part->text; + while (*in && *in != '\\') + ++in; + + if (*in == '\\') { + out = (char*)in++; + while (*in) { + *out++ = *in++; + if (*in == '\\') + ++in; + } + *out = '\0'; + } + } + } + + if (*ptr == '@') { + /* handle macro part */ + size_t macro_len; + + line = ++ptr; + while (ptr < endline && *ptr != '(') + ++ptr; + + if (ptr == endline) { + parse->offset = RC_MISSING_VALUE; + return 0; + } + + macro_len = ptr - line; + + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + part->display_type = RC_FORMAT_UNKNOWN_MACRO; + + /* find the lookup and hook it up */ + lookup = first_lookup; + while (lookup) { + if (strncmp(lookup->name, line, macro_len) == 0 && lookup->name[macro_len] == '\0') { + part->text = lookup->name; + part->lookup = lookup; + part->display_type = lookup->format; + break; + } + + lookup = lookup->next; + } + + if (!lookup) { + static const rc_richpresence_builtin_macro_t builtin_macros[] = { + {"Number", 6, RC_FORMAT_VALUE}, + {"Score", 5, RC_FORMAT_SCORE}, + {"Centiseconds", 12, RC_FORMAT_CENTISECS}, + {"Seconds", 7, RC_FORMAT_SECONDS}, + {"Minutes", 7, RC_FORMAT_MINUTES}, + {"SecondsAsMinutes", 16, RC_FORMAT_SECONDS_AS_MINUTES}, + {"ASCIIChar", 9, RC_FORMAT_ASCIICHAR}, + {"UnicodeChar", 11, RC_FORMAT_UNICODECHAR}, + {"Float1", 6, RC_FORMAT_FLOAT1}, + {"Float2", 6, RC_FORMAT_FLOAT2}, + {"Float3", 6, RC_FORMAT_FLOAT3}, + {"Float4", 6, RC_FORMAT_FLOAT4}, + {"Float5", 6, RC_FORMAT_FLOAT5}, + {"Float6", 6, RC_FORMAT_FLOAT6}, + {"Fixed1", 6, RC_FORMAT_FIXED1}, + {"Fixed2", 6, RC_FORMAT_FIXED2}, + {"Fixed3", 6, RC_FORMAT_FIXED3}, + {"Unsigned", 8, RC_FORMAT_UNSIGNED_VALUE} + }; + size_t i; + + for (i = 0; i < sizeof(builtin_macros) / sizeof(builtin_macros[0]); ++i) { + if (macro_len == builtin_macros[i].name_len && + memcmp(builtin_macros[i].name, line, builtin_macros[i].name_len) == 0) { + part->text = builtin_macros[i].name; + part->lookup = NULL; + part->display_type = builtin_macros[i].display_type; + break; + } + } + } + + /* find the closing parenthesis */ + in = line; + line = ++ptr; + while (ptr < endline && *ptr != ')') + ++ptr; + + if (*ptr != ')') { + /* non-terminated macro, dump the macro and the remaining portion of the line */ + --in; /* already skipped over @ */ + part->display_type = RC_FORMAT_STRING; + part->text = rc_alloc_str(parse, in, (int)(ptr - in)); + } + else if (part->display_type != RC_FORMAT_UNKNOWN_MACRO) { + part->value = rc_alloc_helper_variable_memref_value(line, (int)(ptr - line), parse); + if (parse->offset < 0) + return 0; + + ++ptr; + } + else { + /* assert: the allocated string is going to be smaller than the memory used for the parameter of the macro */ + ++ptr; + part->text = rc_alloc_str(parse, in, (int)(ptr - in)); + } + } + + line = ptr; + } while (line < endline); + + *next = 0; + + return self; +} + +static int rc_richpresence_lookup_item_count(rc_richpresence_lookup_item_t* item) +{ + if (item == NULL) + return 0; + + return (rc_richpresence_lookup_item_count(item->left) + rc_richpresence_lookup_item_count(item->right) + 1); +} + +static void rc_rebalance_richpresence_lookup_get_items(rc_richpresence_lookup_item_t* root, + rc_richpresence_lookup_item_t** items, int* index) +{ + if (root->left != NULL) + rc_rebalance_richpresence_lookup_get_items(root->left, items, index); + + items[*index] = root; + ++(*index); + + if (root->right != NULL) + rc_rebalance_richpresence_lookup_get_items(root->right, items, index); +} + +static void rc_rebalance_richpresence_lookup_rebuild(rc_richpresence_lookup_item_t** root, + rc_richpresence_lookup_item_t** items, int first, int last) +{ + int mid = (first + last) / 2; + rc_richpresence_lookup_item_t* item = items[mid]; + *root = item; + + if (mid == first) + item->left = NULL; + else + rc_rebalance_richpresence_lookup_rebuild(&item->left, items, first, mid - 1); + + if (mid == last) + item->right = NULL; + else + rc_rebalance_richpresence_lookup_rebuild(&item->right, items, mid + 1, last); +} + +static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** root, rc_parse_state_t* parse) +{ + rc_richpresence_lookup_item_t** items; + int index; + int size; + + /* don't bother rebalancing one or two items */ + int count = rc_richpresence_lookup_item_count(*root); + if (count < 3) + return; + + /* allocate space for the flattened list in scratch memory */ + size = count * sizeof(rc_richpresence_lookup_item_t*); + items = (rc_richpresence_lookup_item_t**)rc_buffer_alloc(&parse->scratch.buffer, size); + + /* if allocation fails, we can still use the unbalanced tree, so just bail out */ + if (items == NULL) + return; + + /* flatten the list */ + index = 0; + rc_rebalance_richpresence_lookup_get_items(*root, items, &index); + + /* and rebuild it as a balanced tree */ + rc_rebalance_richpresence_lookup_rebuild(root, items, 0, count - 1); +} + +static void rc_insert_richpresence_lookup_item(rc_richpresence_lookup_t* lookup, + 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; + + next = &lookup->root; + while ((item = *next) != NULL) { + if (first > item->last) { + if (first == item->last + 1 && + strncmp(label, item->label, label_len) == 0 && item->label[label_len] == '\0') { + item->last = last; + return; + } + + next = &item->right; + } + else if (last < item->first) { + if (last == item->first - 1 && + strncmp(label, item->label, label_len) == 0 && item->label[label_len] == '\0') { + item->first = first; + return; + } + + next = &item->left; + } + else { + parse->offset = RC_DUPLICATED_VALUE; + return; + } + } + + item = RC_ALLOC_SCRATCH(rc_richpresence_lookup_item_t, parse); + item->first = first; + item->last = last; + item->label = rc_alloc_str(parse, label, label_len); + item->left = item->right = NULL; + + *next = item; +} + +static const char* rc_parse_richpresence_lookup(rc_richpresence_lookup_t* lookup, const char* nextline, rc_parse_state_t* parse) +{ + const char* line; + const char* endline; + const char* label; + char* endptr = 0; + uint32_t first, last; + int base; + + do + { + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + + if (endline - line < 2) { + /* ignore full line comments inside a lookup */ + if (line[0] == '/' && line[1] == '/') + continue; + + /* empty line indicates end of lookup */ + if (lookup->root) + rc_rebalance_richpresence_lookup(&lookup->root, parse); + break; + } + + /* "*=XXX" specifies default label if lookup does not provide a mapping for the value */ + if (line[0] == '*' && line[1] == '=') { + line += 2; + lookup->default_label = rc_alloc_str(parse, line, (int)(endline - line)); + continue; + } + + label = line; + while (label < endline && *label != '=') + ++label; + + if (label == endline) { + parse->offset = RC_MISSING_VALUE; + break; + } + ++label; + + do { + /* get the value for the mapping */ + if (line[0] == '0' && line[1] == 'x') { + line += 2; + base = 16; + } else { + base = 10; + } + + first = (unsigned)strtoul(line, &endptr, base); + + /* check for a range */ + if (*endptr != '-') { + /* no range, just set last to first */ + last = first; + } + else { + /* range, get last value */ + line = endptr + 1; + + if (line[0] == '0' && line[1] == 'x') { + line += 2; + base = 16; + } else { + base = 10; + } + + last = (unsigned)strtoul(line, &endptr, base); + } + + /* ignore spaces after the number - was previously ignored as string was split on equals */ + while (*endptr == ' ') + ++endptr; + + /* if we've found the equal sign, this is the last item */ + if (*endptr == '=') { + rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + break; + } + + /* otherwise, if it's not a comma, it's an error */ + if (*endptr != ',') { + parse->offset = RC_INVALID_CONST_OPERAND; + break; + } + + /* insert the current item and continue scanning the next one */ + rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + line = endptr + 1; + } while (line < endline); + + } while (parse->offset > 0); + + return nextline; +} + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse) { + rc_richpresence_display_t** nextdisplay; + rc_richpresence_lookup_t* firstlookup = NULL; + rc_richpresence_lookup_t** nextlookup = &firstlookup; + rc_richpresence_lookup_t* lookup; + rc_trigger_t* trigger; + char format[64]; + const char* display = 0; + const char* line; + const char* nextline; + const char* endline; + const char* ptr; + int hasdisplay = 0; + int display_line = 0; + int chars; + + /* special case for empty script to return 1 line read */ + if (!*script) { + parse->lines_read = 1; + parse->offset = RC_MISSING_DISPLAY_STRING; + return; + } + + /* first pass: process macro initializers */ + line = script; + while (*line) { + nextline = rc_parse_line(line, &endline, parse); + if (strncmp(line, "Lookup:", 7) == 0) { + line += 7; + + lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); + lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); + lookup->format = RC_FORMAT_LOOKUP; + lookup->root = NULL; + lookup->default_label = ""; + *nextlookup = lookup; + nextlookup = &lookup->next; + + nextline = rc_parse_richpresence_lookup(lookup, nextline, parse); + if (parse->offset < 0) + return; + + } else if (strncmp(line, "Format:", 7) == 0) { + line += 7; + + lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); + lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); + lookup->root = NULL; + lookup->default_label = ""; + *nextlookup = lookup; + nextlookup = &lookup->next; + + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + if (parse->buffer && strncmp(line, "FormatType=", 11) == 0) { + line += 11; + + chars = (int)(endline - line); + if (chars > 63) + chars = 63; + memcpy(format, line, chars); + format[chars] = '\0'; + + lookup->format = (uint8_t)rc_parse_format(format); + } else { + lookup->format = RC_FORMAT_VALUE; + } + } else if (strncmp(line, "Display:", 8) == 0) { + display = nextline; + display_line = parse->lines_read; + + /* scan as long as we find conditional lines or full line comments */ + do { + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + } while (*line == '?' || (line[0] == '/' && line[1] == '/')); + } + + line = nextline; + } + + *nextlookup = 0; + self->first_lookup = firstlookup; + + nextdisplay = &self->first_display; + + /* second pass, process display string*/ + if (display) { + /* point the parser back at the display strings */ + int lines_read = parse->lines_read; + parse->lines_read = display_line; + line = display; + + nextline = rc_parse_line(line, &endline, parse); + + do { + if (line[0] == '?') { + /* conditional display: ?trigger?string */ + ptr = ++line; + while (ptr < endline && *ptr != '?') + ++ptr; + + if (ptr < endline) { + *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); + if (parse->offset < 0) + return; + trigger = &((*nextdisplay)->trigger); + rc_parse_trigger_internal(trigger, &line, parse); + trigger->memrefs = 0; + if (parse->offset < 0) + return; + if (parse->buffer) + nextdisplay = &((*nextdisplay)->next); + } + } + else if (line[0] != '/' || line[1] != '/') { + break; + } + + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + } while (1); + + /* non-conditional display: string */ + *nextdisplay = rc_parse_richpresence_display_internal(line, endline, parse, firstlookup); + if (*nextdisplay) { + hasdisplay = 1; + nextdisplay = &((*nextdisplay)->next); + + /* restore the parser state */ + parse->lines_read = lines_read; + } + else { + /* this should only happen if the line is blank. + * expect parse->offset to be RC_MISSING_DISPLAY_STRING and leave parse->lines_read + * on the current line for error tracking. */ + } + } + + /* finalize */ + *nextdisplay = 0; + + if (!hasdisplay && parse->offset > 0) { + parse->offset = RC_MISSING_DISPLAY_STRING; + } +} + +int rc_richpresence_size_lines(const char* script, int* lines_read) { + rc_richpresence_t* self; + rc_parse_state_t parse; + rc_memref_t* first_memref; + rc_value_t* variables; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &first_memref); + rc_init_parse_state_variables(&parse, &variables); + + self = RC_ALLOC(rc_richpresence_t, &parse); + rc_parse_richpresence_internal(self, script, &parse); + + if (lines_read) + *lines_read = parse.lines_read; + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +int rc_richpresence_size(const char* script) { + return rc_richpresence_size_lines(script, NULL); +} + +rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx) { + rc_richpresence_t* self; + rc_parse_state_t parse; + + if (!buffer || !script) + return NULL; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_richpresence_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + rc_init_parse_state_variables(&parse, &self->variables); + + rc_parse_richpresence_internal(self, script, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : NULL; +} + +void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L) { + rc_richpresence_display_t* display; + + rc_update_memref_values(richpresence->memrefs, peek, peek_ud); + rc_update_variables(richpresence->variables, peek, peek_ud, L); + + for (display = richpresence->first_display; display; display = display->next) { + if (display->trigger.has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, L); + } +} + +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; + char tmp[256]; + char* ptr = buffer; + const char* text; + size_t chars; + + *ptr = '\0'; + while (part) { + switch (part->display_type) { + case RC_FORMAT_STRING: + text = part->text; + chars = strlen(text); + break; + + case RC_FORMAT_LOOKUP: + rc_typed_value_from_memref_value(&value, part->value); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + + text = part->lookup->default_label; + item = part->lookup->root; + while (item) { + if (value.value.u32 > item->last) { + item = item->right; + } + else if (value.value.u32 < item->first) { + item = item->left; + } + else { + text = item->label; + break; + } + } + + chars = strlen(text); + break; + + case RC_FORMAT_ASCIICHAR: + chars = 0; + text = tmp; + value.type = RC_VALUE_TYPE_UNSIGNED; + + do { + value.value.u32 = part->value->value; + if (value.value.u32 == 0) { + /* null terminator - skip over remaining character macros */ + while (part->next && part->next->display_type == RC_FORMAT_ASCIICHAR) + part = part->next; + break; + } + + if (value.value.u32 < 32 || value.value.u32 >= 127) + value.value.u32 = '?'; + + tmp[chars++] = (char)value.value.u32; + if (chars == sizeof(tmp) || !part->next || part->next->display_type != RC_FORMAT_ASCIICHAR) + break; + + part = part->next; + } while (1); + + tmp[chars] = '\0'; + break; + + case RC_FORMAT_UNICODECHAR: + chars = 0; + text = tmp; + value.type = RC_VALUE_TYPE_UNSIGNED; + + do { + value.value.u32 = part->value->value; + if (value.value.u32 == 0) { + /* null terminator - skip over remaining character macros */ + while (part->next && part->next->display_type == RC_FORMAT_UNICODECHAR) + part = part->next; + break; + } + + if (value.value.u32 < 32 || value.value.u32 > 65535) + value.value.u32 = 0xFFFD; /* unicode replacement char */ + + if (value.value.u32 < 0x80) { + tmp[chars++] = (char)value.value.u32; + } + else if (value.value.u32 < 0x0800) { + tmp[chars + 1] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars] = (char)(0xC0 | (value.value.u32 & 0x1F)); + chars += 2; + } + else { + /* surrogate pair not supported, convert to replacement char */ + if (value.value.u32 >= 0xD800 && value.value.u32 < 0xE000) + value.value.u32 = 0xFFFD; + + tmp[chars + 2] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars + 1] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars] = (char)(0xE0 | (value.value.u32 & 0x1F)); + chars += 3; + } + + if (chars >= sizeof(tmp) - 3 || !part->next || part->next->display_type != RC_FORMAT_UNICODECHAR) + break; + + part = part->next; + } while (1); + + tmp[chars] = '\0'; + break; + + case RC_FORMAT_UNKNOWN_MACRO: + chars = snprintf(tmp, sizeof(tmp), "[Unknown macro]%s", part->text); + text = tmp; + break; + + default: + rc_typed_value_from_memref_value(&value, part->value); + chars = rc_format_typed_value(tmp, sizeof(tmp), &value, part->display_type); + text = tmp; + break; + } + + if (chars > 0 && buffersize > 0) { + if ((unsigned)chars >= buffersize) { + /* prevent write past end of buffer */ + memcpy(ptr, text, buffersize - 1); + ptr[buffersize - 1] = '\0'; + buffersize = 0; + } + else { + memcpy(ptr, text, chars); + ptr[chars] = '\0'; + buffersize -= (unsigned)chars; + } + } + + ptr += chars; + part = part->next; + } + + return (int)(ptr - buffer); +} + +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) { + /* if we've reached the end of the condition list, process it */ + if (!display->next) + return rc_evaluate_richpresence_display(display->display, buffer, buffersize); + + /* triggers with required hits will be updated in rc_update_richpresence */ + if (!display->trigger.has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, L); + + /* if we've found a valid condition, process it */ + if (display->trigger.state == RC_TRIGGER_STATE_TRIGGERED) + return rc_evaluate_richpresence_display(display->display, buffer, buffersize); + } + + buffer[0] = '\0'; + return 0; +} + +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); +} + +void rc_reset_richpresence(rc_richpresence_t* self) { + rc_richpresence_display_t* display; + rc_value_t* variable; + + for (display = self->first_display; display; display = display->next) + rc_reset_trigger(&display->trigger); + + for (variable = self->variables; variable; variable = variable->next) + rc_reset_value(variable); +} diff --git a/3rdparty/rcheevos/src/rcheevos/runtime.c b/3rdparty/rcheevos/src/rcheevos/runtime.c new file mode 100644 index 0000000000..25e9885dcf --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/runtime.c @@ -0,0 +1,892 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +#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; + self->next_variable = &self->variables; +} + +void rc_runtime_destroy(rc_runtime_t* self) { + uint32_t i; + + if (self->triggers) { + for (i = 0; i < self->trigger_count; ++i) + free(self->triggers[i].buffer); + + free(self->triggers); + self->triggers = NULL; + + self->trigger_count = self->trigger_capacity = 0; + } + + if (self->lboards) { + for (i = 0; i < self->lboard_count; ++i) + free(self->lboards[i].buffer); + + free(self->lboards); + self->lboards = NULL; + + self->lboard_count = self->lboard_capacity = 0; + } + + while (self->richpresence) { + rc_runtime_richpresence_t* previous = self->richpresence->previous; + + free(self->richpresence->buffer); + free(self->richpresence); + self->richpresence = previous; + } + + self->next_memref = 0; + self->memrefs = 0; + + if (self->owns_self) { + free(self); + } +} + +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)); + md5_finish(&state, md5); +} + +static char rc_runtime_allocated_memrefs(rc_runtime_t* self) { + char owns_memref = 0; + + /* if at least one memref was allocated within the object, we can't free the buffer when the object is deactivated */ + if (*self->next_memref != NULL) { + owns_memref = 1; + /* advance through the new memrefs so we're ready for the next allocation */ + do { + self->next_memref = &(*self->next_memref)->next; + } while (*self->next_memref != NULL); + } + + /* if at least one variable was allocated within the object, we can't free the buffer when the object is deactivated */ + if (*self->next_variable != NULL) { + owns_memref = 1; + /* advance through the new variables so we're ready for the next allocation */ + do { + self->next_variable = &(*self->next_variable)->next; + } while (*self->next_variable != NULL); + } + + return owns_memref; +} + +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 + */ + rc_reset_trigger(self->triggers[index].trigger); + self->triggers[index].trigger = NULL; + } + else { + /* trigger doesn't own any memrefs, go ahead and free it, then replace it with the last trigger */ + free(self->triggers[index].buffer); + + if (--self->trigger_count > index) + memcpy(&self->triggers[index], &self->triggers[self->trigger_count], sizeof(rc_runtime_trigger_t)); + } +} + +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) + rc_runtime_deactivate_trigger_by_index(self, i); + } +} + +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; + uint8_t md5[16]; + int32_t size; + uint32_t i; + + if (memaddr == NULL) + return RC_INVALID_MEMORY_OPERAND; + + rc_runtime_checksum(memaddr, md5); + + /* check to see if the id is already registered with an active trigger */ + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) { + if (memcmp(self->triggers[i].md5, md5, 16) == 0) { + /* if the checksum hasn't changed, we can reuse the existing item */ + rc_reset_trigger(self->triggers[i].trigger); + return RC_OK; + } + + /* checksum has changed, deactivate the the item */ + rc_runtime_deactivate_trigger_by_index(self, i); + + /* deactivate may reorder the list so we should continue from the current index. however, we + * assume that only one trigger is active per id, so having found that, just stop scanning. + */ + break; + } + } + + /* check to see if a disabled trigger for the specific id matches the trigger being registered */ + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].id == id && memcmp(self->triggers[i].md5, md5, 16) == 0) { + /* retrieve the trigger pointer from the buffer */ + size = 0; + trigger = (rc_trigger_t*)rc_alloc(self->triggers[i].buffer, &size, sizeof(rc_trigger_t), RC_ALIGNOF(rc_trigger_t), NULL, -1); + self->triggers[i].trigger = trigger; + + rc_reset_trigger(trigger); + return RC_OK; + } + } + + /* item has not been previously registered, determine how much space we need for it, and allocate it */ + size = rc_trigger_size(memaddr); + if (size < 0) + return size; + + trigger_buffer = malloc(size); + if (!trigger_buffer) + return RC_OUT_OF_MEMORY; + + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, trigger_buffer, L, funcs_idx); + parse.first_memref = &self->memrefs; + trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(trigger, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + if (parse.offset < 0) { + free(trigger_buffer); + *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ + return parse.offset; + } + + /* grow the trigger buffer if necessary */ + if (self->trigger_count == self->trigger_capacity) { + self->trigger_capacity += 32; + if (!self->triggers) + self->triggers = (rc_runtime_trigger_t*)malloc(self->trigger_capacity * sizeof(rc_runtime_trigger_t)); + else + self->triggers = (rc_runtime_trigger_t*)realloc(self->triggers, self->trigger_capacity * sizeof(rc_runtime_trigger_t)); + + if (!self->triggers) { + free(trigger_buffer); + *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ + return RC_OUT_OF_MEMORY; + } + } + + /* assign the new trigger */ + runtime_trigger = &self->triggers[self->trigger_count]; + runtime_trigger->id = id; + runtime_trigger->trigger = trigger; + runtime_trigger->buffer = trigger_buffer; + runtime_trigger->invalid_memref = NULL; + memcpy(runtime_trigger->md5, md5, 16); + runtime_trigger->serialized_size = 0; + runtime_trigger->owns_memrefs = rc_runtime_allocated_memrefs(self); + ++self->trigger_count; + + /* reset it, and return it */ + trigger->memrefs = NULL; + rc_reset_trigger(trigger); + return RC_OK; +} + +rc_trigger_t* rc_runtime_get_achievement(const 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) + return self->triggers[i].trigger; + } + + return NULL; +} + +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) + return 0; + + if (!trigger) { + *measured_value = *measured_target = 0; + return 0; + } + + if (rc_trigger_state_active(trigger->state)) { + *measured_value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; + *measured_target = trigger->measured_target; + } + else { + /* don't report measured information for inactive triggers */ + *measured_value = *measured_target = 0; + } + + return 1; +} + +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); + uint32_t value; + if (!buffer || !buffer_size) + return 0; + + if (!trigger || /* no trigger */ + trigger->measured_target == 0 || /* not measured */ + !rc_trigger_state_active(trigger->state)) { /* don't report measured value for inactive triggers */ + *buffer = '\0'; + return 0; + } + + /* cap the value at the target so we can count past the target: "107 >= 100" */ + value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; + if (value > trigger->measured_target) + value = trigger->measured_target; + + if (trigger->measured_as_percent) { + 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, 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 + */ + rc_reset_lboard(self->lboards[index].lboard); + self->lboards[index].lboard = NULL; + } + else { + /* lboard doesn't own any memrefs, go ahead and free it, then replace it with the last lboard */ + free(self->lboards[index].buffer); + + if (--self->lboard_count > index) + memcpy(&self->lboards[index], &self->lboards[self->lboard_count], sizeof(rc_runtime_lboard_t)); + } +} + +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) + rc_runtime_deactivate_lboard_by_index(self, i); + } +} + +int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { + void* lboard_buffer; + uint8_t md5[16]; + rc_lboard_t* lboard; + rc_parse_state_t parse; + rc_runtime_lboard_t* runtime_lboard; + int size; + uint32_t i; + + if (memaddr == 0) + return RC_INVALID_MEMORY_OPERAND; + + rc_runtime_checksum(memaddr, md5); + + /* check to see if the id is already registered with an active lboard */ + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) { + if (memcmp(self->lboards[i].md5, md5, 16) == 0) { + /* if the checksum hasn't changed, we can reuse the existing item */ + rc_reset_lboard(self->lboards[i].lboard); + return RC_OK; + } + + /* checksum has changed, deactivate the the item */ + rc_runtime_deactivate_lboard_by_index(self, i); + + /* deactivate may reorder the list so we should continue from the current index. however, we + * assume that only one trigger is active per id, so having found that, just stop scanning. + */ + break; + } + } + + /* check to see if a disabled lboard for the specific id matches the lboard being registered */ + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].id == id && memcmp(self->lboards[i].md5, md5, 16) == 0) { + /* retrieve the lboard pointer from the buffer */ + size = 0; + lboard = (rc_lboard_t*)rc_alloc(self->lboards[i].buffer, &size, sizeof(rc_lboard_t), RC_ALIGNOF(rc_lboard_t), NULL, -1); + self->lboards[i].lboard = lboard; + + rc_reset_lboard(lboard); + return RC_OK; + } + } + + /* item has not been previously registered, determine how much space we need for it, and allocate it */ + size = rc_lboard_size(memaddr); + if (size < 0) + return size; + + lboard_buffer = malloc(size); + if (!lboard_buffer) + return RC_OUT_OF_MEMORY; + + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, lboard_buffer, L, funcs_idx); + lboard = RC_ALLOC(rc_lboard_t, &parse); + parse.first_memref = &self->memrefs; + rc_parse_lboard_internal(lboard, memaddr, &parse); + rc_destroy_parse_state(&parse); + + if (parse.offset < 0) { + free(lboard_buffer); + *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ + return parse.offset; + } + + /* grow the lboard buffer if necessary */ + if (self->lboard_count == self->lboard_capacity) { + self->lboard_capacity += 16; + if (!self->lboards) + self->lboards = (rc_runtime_lboard_t*)malloc(self->lboard_capacity * sizeof(rc_runtime_lboard_t)); + else + self->lboards = (rc_runtime_lboard_t*)realloc(self->lboards, self->lboard_capacity * sizeof(rc_runtime_lboard_t)); + + if (!self->lboards) { + free(lboard_buffer); + *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ + return RC_OUT_OF_MEMORY; + } + } + + /* assign the new lboard */ + runtime_lboard = &self->lboards[self->lboard_count++]; + runtime_lboard->id = id; + runtime_lboard->value = 0; + runtime_lboard->lboard = lboard; + runtime_lboard->buffer = lboard_buffer; + runtime_lboard->invalid_memref = NULL; + memcpy(runtime_lboard->md5, md5, 16); + runtime_lboard->serialized_size = 0; + runtime_lboard->owns_memrefs = rc_runtime_allocated_memrefs(self); + + /* reset it, and return it */ + lboard->memrefs = NULL; + rc_reset_lboard(lboard); + return RC_OK; +} + +rc_lboard_t* rc_runtime_get_lboard(const 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) + return self->lboards[i].lboard; + } + + return NULL; +} + +int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format) +{ + return rc_format_value(buffer, size, value, format); +} + +int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua_State* L, int funcs_idx) { + rc_richpresence_t* richpresence; + rc_runtime_richpresence_t* previous; + rc_runtime_richpresence_t** previous_ptr; + rc_parse_state_t parse; + uint8_t md5[16]; + int size; + + if (script == NULL) + return RC_MISSING_DISPLAY_STRING; + + rc_runtime_checksum(script, md5); + + /* look for existing match */ + previous_ptr = NULL; + previous = self->richpresence; + while (previous) { + if (previous && self->richpresence->richpresence && memcmp(self->richpresence->md5, md5, 16) == 0) { + /* unchanged. reset all of the conditions */ + rc_reset_richpresence(self->richpresence->richpresence); + + /* move to front of linked list*/ + if (previous_ptr) { + *previous_ptr = previous->previous; + if (!self->richpresence->owns_memrefs) { + free(self->richpresence->buffer); + previous->previous = self->richpresence->previous; + } + else { + previous->previous = self->richpresence; + } + + self->richpresence = previous; + } + + /* return success*/ + return RC_OK; + } + + previous_ptr = &previous->previous; + previous = previous->previous; + } + + /* no existing match found, parse script */ + size = rc_richpresence_size(script); + if (size < 0) + return size; + + /* if the previous script doesn't have any memrefs, free it */ + previous = self->richpresence; + if (previous) { + if (!previous->owns_memrefs) { + free(previous->buffer); + previous = previous->previous; + } + } + + /* allocate and process the new script */ + self->richpresence = (rc_runtime_richpresence_t*)malloc(sizeof(rc_runtime_richpresence_t)); + if (!self->richpresence) + return RC_OUT_OF_MEMORY; + + self->richpresence->previous = previous; + self->richpresence->owns_memrefs = 0; + memcpy(self->richpresence->md5, md5, sizeof(md5)); + self->richpresence->buffer = malloc(size); + + if (!self->richpresence->buffer) + return RC_OUT_OF_MEMORY; + + rc_init_parse_state(&parse, self->richpresence->buffer, L, funcs_idx); + self->richpresence->richpresence = richpresence = RC_ALLOC(rc_richpresence_t, &parse); + parse.first_memref = &self->memrefs; + parse.variables = &self->variables; + rc_parse_richpresence_internal(richpresence, script, &parse); + rc_destroy_parse_state(&parse); + + if (parse.offset < 0) { + free(self->richpresence->buffer); + free(self->richpresence); + self->richpresence = previous; + *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ + return parse.offset; + } + + self->richpresence->owns_memrefs = rc_runtime_allocated_memrefs(self); + + richpresence->memrefs = NULL; + richpresence->variables = NULL; + + if (!richpresence->first_display || !richpresence->first_display->display) { + /* non-existant rich presence */ + self->richpresence->richpresence = NULL; + } + else { + /* reset all of the conditions */ + rc_reset_richpresence(richpresence); + } + + return RC_OK; +} + +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); + + *buffer = '\0'; + return 0; +} + +void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L) { + rc_runtime_event_t runtime_event; + int i; + + runtime_event.value = 0; + + rc_update_memref_values(self->memrefs, peek, ud); + rc_update_variables(self->variables, peek, ud, L); + + for (i = self->trigger_count - 1; i >= 0; --i) { + rc_trigger_t* trigger = self->triggers[i].trigger; + int old_state, new_state; + uint32_t old_measured_value; + + if (!trigger) + continue; + + if (self->triggers[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED; + runtime_event.id = self->triggers[i].id; + runtime_event.value = self->triggers[i].invalid_memref->address; + + trigger->state = RC_TRIGGER_STATE_DISABLED; + self->triggers[i].invalid_memref = NULL; + + event_handler(&runtime_event); + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + continue; + } + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, peek, ud, L); + + /* trigger->state doesn't actually change to RESET, RESET just serves as a notification. + * handle the notification, then look at the actual state */ + if (new_state == RC_TRIGGER_STATE_RESET) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_RESET; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + + new_state = trigger->state; + } + + /* if the measured value changed and the achievement hasn't triggered, send a notification */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_target != 0 && trigger->measured_value <= trigger->measured_target && + new_state != RC_TRIGGER_STATE_TRIGGERED && + new_state != RC_TRIGGER_STATE_INACTIVE && new_state != RC_TRIGGER_STATE_WAITING) { + + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED; + runtime_event.id = self->triggers[i].id; + + if (trigger->measured_as_percent) { + /* if reporting measured value as a percentage, only send the notification if the percentage changes */ + 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); + } + } + else { + runtime_event.value = trigger->measured_value; + event_handler(&runtime_event); + } + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise an UNPRIMED event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + } + + /* raise events for each of the possible new states */ + switch (new_state) + { + case RC_TRIGGER_STATE_TRIGGERED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_PAUSED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_PRIMED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_ACTIVE: + /* only raise ACTIVATED event when transitioning from an inactive state. + * note that inactive in this case means active but cannot trigger. */ + if (old_state == RC_TRIGGER_STATE_WAITING || old_state == RC_TRIGGER_STATE_PAUSED) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + } + break; + } + } + + for (i = self->lboard_count - 1; i >= 0; --i) { + rc_lboard_t* lboard = self->lboards[i].lboard; + int lboard_state; + + if (!lboard) + continue; + + if (self->lboards[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_DISABLED; + runtime_event.id = self->lboards[i].id; + runtime_event.value = self->lboards[i].invalid_memref->address; + + lboard->state = RC_LBOARD_STATE_DISABLED; + self->lboards[i].invalid_memref = NULL; + + event_handler(&runtime_event); + continue; + } + + lboard_state = lboard->state; + switch (rc_evaluate_lboard(lboard, &runtime_event.value, peek, ud, L)) + { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (lboard_state != RC_LBOARD_STATE_STARTED) { + self->lboards[i].value = runtime_event.value; + + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_STARTED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + else if (runtime_event.value != self->lboards[i].value) { + self->lboards[i].value = runtime_event.value; + + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_UPDATED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (lboard_state != RC_LBOARD_STATE_CANCELED) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_CANCELED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (lboard_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_TRIGGERED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + } + } + + if (self->richpresence && self->richpresence->richpresence) + rc_update_richpresence(self->richpresence->richpresence, peek, ud, L); +} + +void rc_runtime_reset(rc_runtime_t* self) { + rc_value_t* variable; + uint32_t i; + + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].trigger) + rc_reset_trigger(self->triggers[i].trigger); + } + + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].lboard) + rc_reset_lboard(self->lboards[i].lboard); + } + + if (self->richpresence && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); + + for (variable = self->variables; variable; variable = variable->next) + rc_reset_value(variable); +} + +static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memref_t* memref) { + rc_condition_t* cond; + if (!condset) + return 0; + + for (cond = condset->conditions; cond; cond = cond->next) { + if (rc_operand_is_memref(&cond->operand1) && cond->operand1.value.memref == memref) + return 1; + if (rc_operand_is_memref(&cond->operand2) && cond->operand2.value.memref == memref) + return 1; + } + + return 0; +} + +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { + rc_condset_t* condset; + if (!value) + return 0; + + for (condset = value->conditions; condset; condset = condset->next) { + if (rc_condset_contains_memref(condset, memref)) + return 1; + } + + return 0; +} + +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { + rc_condset_t* condset; + if (!trigger) + return 0; + + if (rc_condset_contains_memref(trigger->requirement, memref)) + return 1; + + for (condset = trigger->alternative; condset; condset = condset->next) { + if (rc_condset_contains_memref(condset, memref)) + return 1; + } + + return 0; +} + +static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref) { + uint32_t i; + + /* disable any achievements dependent on the address */ + for (i = 0; i < self->trigger_count; ++i) { + if (!self->triggers[i].invalid_memref && rc_trigger_contains_memref(self->triggers[i].trigger, memref)) + self->triggers[i].invalid_memref = memref; + } + + /* disable any leaderboards dependent on the address */ + for (i = 0; i < self->lboard_count; ++i) { + if (!self->lboards[i].invalid_memref) { + rc_lboard_t* lboard = self->lboards[i].lboard; + if (lboard) { + if (rc_trigger_contains_memref(&lboard->start, memref)) { + lboard->start.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_trigger_contains_memref(&lboard->cancel, memref)) { + lboard->cancel.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_trigger_contains_memref(&lboard->submit, memref)) { + lboard->submit.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_value_contains_memref(&lboard->value, memref)) + self->lboards[i].invalid_memref = memref; + } + } + } +} + +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; + + while (memref) { + if (memref->address == address && !memref->value.is_indirect) { + /* remove the invalid memref from the chain so we don't try to evaluate it in the future. + * it's still there, so anything referencing it will continue to fetch 0. + */ + *last_memref = memref->next; + + rc_runtime_invalidate_memref(self, memref); + break; + } + + last_memref = &memref->next; + memref = *last_memref; + } +} + +void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, + rc_runtime_validate_address_t validate_handler) { + rc_memref_t** last_memref = &self->memrefs; + rc_memref_t* memref = self->memrefs; + int num_invalid = 0; + + while (memref) { + if (!memref->value.is_indirect && !validate_handler(memref->address)) { + /* remove the invalid memref from the chain so we don't try to evaluate it in the future. + * it's still there, so anything referencing it will continue to fetch 0. + */ + *last_memref = memref->next; + + rc_runtime_invalidate_memref(self, memref); + ++num_invalid; + } + else { + last_memref = &memref->next; + } + + memref = *last_memref; + } + + if (num_invalid) { + rc_runtime_event_t runtime_event; + int i; + + for (i = self->trigger_count - 1; i >= 0; --i) { + rc_trigger_t* trigger = self->triggers[i].trigger; + if (trigger && self->triggers[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED; + runtime_event.id = self->triggers[i].id; + runtime_event.value = self->triggers[i].invalid_memref->address; + + trigger->state = RC_TRIGGER_STATE_DISABLED; + self->triggers[i].invalid_memref = NULL; + + event_handler(&runtime_event); + } + } + + for (i = self->lboard_count - 1; i >= 0; --i) { + rc_lboard_t* lboard = self->lboards[i].lboard; + if (lboard && self->lboards[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_DISABLED; + runtime_event.id = self->lboards[i].id; + runtime_event.value = self->lboards[i].invalid_memref->address; + + lboard->state = RC_LBOARD_STATE_DISABLED; + self->lboards[i].invalid_memref = NULL; + + event_handler(&runtime_event); + } + } + } +} diff --git a/3rdparty/rcheevos/src/rcheevos/runtime_progress.c b/3rdparty/rcheevos/src/rcheevos/runtime_progress.c new file mode 100644 index 0000000000..fd951dbd5b --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/runtime_progress.c @@ -0,0 +1,881 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "rc_util.h" +#include "../rhash/md5.h" + +#include +#include + +#define RC_RUNTIME_MARKER 0x0A504152 /* RAP\n */ + +#define RC_RUNTIME_CHUNK_MEMREFS 0x4645524D /* MREF */ +#define RC_RUNTIME_CHUNK_VARIABLES 0x53524156 /* VARS */ +#define RC_RUNTIME_CHUNK_ACHIEVEMENT 0x56484341 /* ACHV */ +#define RC_RUNTIME_CHUNK_LEADERBOARD 0x4452424C /* LBRD */ +#define RC_RUNTIME_CHUNK_RICHPRESENCE 0x48434952 /* RICH */ + +#define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ + +typedef struct rc_runtime_progress_t { + const rc_runtime_t* runtime; + + uint32_t offset; + uint8_t* buffer; + + uint32_t chunk_size_offset; + + lua_State* L; +} rc_runtime_progress_t; + +#define RC_TRIGGER_STATE_UNUPDATED 0x7F + +#define RC_MEMREF_FLAG_CHANGED_THIS_FRAME 0x00010000 + +#define RC_VAR_FLAG_HAS_COND_DATA 0x01000000 + +#define RC_COND_FLAG_IS_TRUE 0x00000001 +#define RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF 0x00010000 +#define RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME 0x00020000 +#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, uint32_t value) +{ + if (progress->buffer) { + progress->buffer[progress->offset + 0] = value & 0xFF; value >>= 8; + progress->buffer[progress->offset + 1] = value & 0xFF; value >>= 8; + progress->buffer[progress->offset + 2] = value & 0xFF; value >>= 8; + progress->buffer[progress->offset + 3] = value & 0xFF; + } + + progress->offset += 4; +} + +static uint32_t rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) +{ + 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); + + progress->offset += 4; + return value; +} + +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); + + progress->offset += 16; +} + +static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, uint8_t* md5) +{ + int result = 0; + if (progress->buffer) + result = (memcmp(&progress->buffer[progress->offset], md5, 16) == 0); + + progress->offset += 16; + + return result; +} + +static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, uint32_t chunk_id) +{ + rc_runtime_progress_write_uint(progress, chunk_id); + + progress->chunk_size_offset = progress->offset; + + progress->offset += 4; +} + +static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) +{ + 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 = (uint32_t)(progress->offset - progress->chunk_size_offset - 4); + + /* temporarily update the write pointer to write the chunk size field */ + offset = progress->offset; + progress->offset = progress->chunk_size_offset; + rc_runtime_progress_write_uint(progress, length); + progress->offset = offset; + } +} + +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; + progress->L = L; +} + +static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) +{ + rc_memref_t* memref = progress->runtime->memrefs; + uint32_t flags = 0; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_MEMREFS); + + if (!progress->buffer) { + while (memref) { + progress->offset += 16; + memref = memref->next; + } + } + else { + while (memref) { + flags = memref->value.size; + if (memref->value.changed) + flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + + rc_runtime_progress_write_uint(progress, memref->address); + rc_runtime_progress_write_uint(progress, flags); + rc_runtime_progress_write_uint(progress, memref->value.value); + rc_runtime_progress_write_uint(progress, memref->value.prior); + + memref = memref->next; + } + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_memrefs(rc_runtime_progress_t* progress) +{ + 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; + + /* re-read the chunk size to determine how many memrefs are present */ + progress->offset -= 4; + entries = rc_runtime_progress_read_uint(progress) / 16; + + while (entries != 0) { + address = rc_runtime_progress_read_uint(progress); + flags = rc_runtime_progress_read_uint(progress); + value = rc_runtime_progress_read_uint(progress); + prior = rc_runtime_progress_read_uint(progress); + + size = flags & 0xFF; + + memref = first_unmatched_memref; + while (memref) { + if (memref->address == address && memref->value.size == size) { + memref->value.value = value; + memref->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + memref->value.prior = prior; + + if (memref == first_unmatched_memref) + first_unmatched_memref = memref->next; + + break; + } + + memref = memref->next; + } + + --entries; + } + + return RC_OK; +} + +static int rc_runtime_progress_is_indirect_memref(rc_operand_t* oper) +{ + switch (oper->type) + { + case RC_OPERAND_CONST: + case RC_OPERAND_FP: + case RC_OPERAND_LUA: + return 0; + + default: + return oper->value.memref->value.is_indirect; + } +} + +static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) +{ + rc_condition_t* cond; + uint32_t flags; + + rc_runtime_progress_write_uint(progress, condset->is_paused); + + cond = condset->conditions; + while (cond) { + flags = 0; + if (cond->is_true) + flags |= RC_COND_FLAG_IS_TRUE; + + if (rc_runtime_progress_is_indirect_memref(&cond->operand1)) { + flags |= RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF; + if (cond->operand1.value.memref->value.changed) + flags |= RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME; + } + + if (rc_runtime_progress_is_indirect_memref(&cond->operand2)) { + flags |= RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF; + if (cond->operand2.value.memref->value.changed) + flags |= RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME; + } + + rc_runtime_progress_write_uint(progress, cond->current_hits); + rc_runtime_progress_write_uint(progress, flags); + + if (flags & RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF) { + rc_runtime_progress_write_uint(progress, cond->operand1.value.memref->value.value); + rc_runtime_progress_write_uint(progress, cond->operand1.value.memref->value.prior); + } + + if (flags & RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF) { + rc_runtime_progress_write_uint(progress, cond->operand2.value.memref->value.value); + rc_runtime_progress_write_uint(progress, cond->operand2.value.memref->value.prior); + } + + cond = cond->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) +{ + rc_condition_t* cond; + uint32_t flags; + + condset->is_paused = (char)rc_runtime_progress_read_uint(progress); + + cond = condset->conditions; + while (cond) { + cond->current_hits = rc_runtime_progress_read_uint(progress); + flags = rc_runtime_progress_read_uint(progress); + + cond->is_true = (flags & RC_COND_FLAG_IS_TRUE) ? 1 : 0; + + if (flags & RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF) { + if (!rc_operand_is_memref(&cond->operand1)) /* this should never happen, but better safe than sorry */ + return RC_INVALID_STATE; + + cond->operand1.value.memref->value.value = rc_runtime_progress_read_uint(progress); + cond->operand1.value.memref->value.prior = rc_runtime_progress_read_uint(progress); + cond->operand1.value.memref->value.changed = (flags & RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME) ? 1 : 0; + } + + if (flags & RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF) { + if (!rc_operand_is_memref(&cond->operand2)) /* this should never happen, but better safe than sorry */ + return RC_INVALID_STATE; + + cond->operand2.value.memref->value.value = rc_runtime_progress_read_uint(progress); + cond->operand2.value.memref->value.prior = rc_runtime_progress_read_uint(progress); + cond->operand2.value.memref->value.changed = (flags & RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME) ? 1 : 0; + } + + cond = cond->next; + } + + return RC_OK; +} + +static uint32_t rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) +{ + const rc_condition_t* condition; + + /* predetermined presence of pause flag or indirect memrefs - must serialize */ + if (conditions->has_pause || conditions->has_indirect_memrefs) + return RC_VAR_FLAG_HAS_COND_DATA; + + /* if any conditions has required hits, must serialize */ + /* ASSERT: Measured with comparison and no explicit target will set hit target to 0xFFFFFFFF */ + for (condition = conditions->conditions; condition; condition = condition->next) { + if (condition->required_hits > 0) + return RC_VAR_FLAG_HAS_COND_DATA; + } + + /* can safely be reset without affecting behavior */ + return 0; +} + +static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, const rc_value_t* variable) +{ + uint32_t flags; + + flags = rc_runtime_progress_should_serialize_variable_condset(variable->conditions); + if (variable->value.changed) + flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + + rc_runtime_progress_write_uint(progress, flags); + rc_runtime_progress_write_uint(progress, variable->value.value); + rc_runtime_progress_write_uint(progress, variable->value.prior); + + if (flags & RC_VAR_FLAG_HAS_COND_DATA) { + int result = rc_runtime_progress_write_condset(progress, variable->conditions); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) +{ + uint32_t count = 0; + const rc_value_t* variable; + + for (variable = progress->runtime->variables; variable; variable = variable->next) + ++count; + if (count == 0) + return RC_OK; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_VARIABLES); + rc_runtime_progress_write_uint(progress, count); + + for (variable = progress->runtime->variables; variable; variable = variable->next) + { + uint32_t djb2 = rc_djb2(variable->name); + rc_runtime_progress_write_uint(progress, djb2); + + rc_runtime_progress_write_variable(progress, variable); + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_variable(rc_runtime_progress_t* progress, rc_value_t* variable) +{ + 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); + + if (flags & RC_VAR_FLAG_HAS_COND_DATA) { + int result = rc_runtime_progress_read_condset(progress, variable->conditions); + if (result != RC_OK) + return result; + } + else { + rc_reset_condset(variable->conditions); + } + + return RC_OK; +} + +static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) +{ + struct rc_pending_value_t + { + rc_value_t* variable; + uint32_t djb2; + }; + struct rc_pending_value_t local_pending_variables[32]; + struct rc_pending_value_t* pending_variables; + rc_value_t* variable; + uint32_t count, serialized_count; + int result; + uint32_t i; + + serialized_count = rc_runtime_progress_read_uint(progress); + if (serialized_count == 0) + return RC_OK; + + count = 0; + for (variable = progress->runtime->variables; variable; variable = variable->next) + ++count; + + if (count == 0) + return RC_OK; + + if (count <= sizeof(local_pending_variables) / sizeof(local_pending_variables[0])) { + pending_variables = local_pending_variables; + } + else { + pending_variables = (struct rc_pending_value_t*)malloc(count * sizeof(struct rc_pending_value_t)); + if (pending_variables == NULL) + return RC_OUT_OF_MEMORY; + } + + count = 0; + for (variable = progress->runtime->variables; variable; variable = variable->next) { + pending_variables[count].variable = variable; + pending_variables[count].djb2 = rc_djb2(variable->name); + ++count; + } + + result = RC_OK; + for (; serialized_count > 0 && result == RC_OK; --serialized_count) { + 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; + result = rc_runtime_progress_read_variable(progress, variable); + if (result == RC_OK) { + if (i < count - 1) + memcpy(&pending_variables[i], &pending_variables[count - 1], sizeof(struct rc_pending_value_t)); + count--; + } + break; + } + } + } + + while (count > 0) + rc_reset_value(pending_variables[--count].variable); + + if (pending_variables != local_pending_variables) + free(pending_variables); + + return result; +} + +static int rc_runtime_progress_write_trigger(rc_runtime_progress_t* progress, const rc_trigger_t* trigger) +{ + rc_condset_t* condset; + int result; + + rc_runtime_progress_write_uint(progress, trigger->state); + rc_runtime_progress_write_uint(progress, trigger->measured_value); + + if (trigger->requirement) { + result = rc_runtime_progress_write_condset(progress, trigger->requirement); + if (result != RC_OK) + return result; + } + + condset = trigger->alternative; + while (condset) { + result = rc_runtime_progress_write_condset(progress, condset); + if (result != RC_OK) + return result; + + condset = condset->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_trigger(rc_runtime_progress_t* progress, rc_trigger_t* trigger) +{ + rc_condset_t* condset; + int result; + + trigger->state = (char)rc_runtime_progress_read_uint(progress); + trigger->measured_value = rc_runtime_progress_read_uint(progress); + + if (trigger->requirement) { + result = rc_runtime_progress_read_condset(progress, trigger->requirement); + if (result != RC_OK) + return result; + } + + condset = trigger->alternative; + while (condset) { + result = rc_runtime_progress_read_condset(progress, condset); + if (result != RC_OK) + return result; + + condset = condset->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progress) +{ + uint32_t i; + int offset = 0; + int result; + + for (i = 0; i < progress->runtime->trigger_count; ++i) { + rc_runtime_trigger_t* runtime_trigger = &progress->runtime->triggers[i]; + if (!runtime_trigger->trigger) + continue; + + /* don't store state for inactive or triggered achievements */ + if (!rc_trigger_state_active(runtime_trigger->trigger->state)) + continue; + + if (!progress->buffer) { + if (runtime_trigger->serialized_size) { + progress->offset += runtime_trigger->serialized_size; + continue; + } + + offset = progress->offset; + } + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_ACHIEVEMENT); + rc_runtime_progress_write_uint(progress, runtime_trigger->id); + rc_runtime_progress_write_md5(progress, runtime_trigger->md5); + + result = rc_runtime_progress_write_trigger(progress, runtime_trigger->trigger); + if (result != RC_OK) + return result; + + rc_runtime_progress_end_chunk(progress); + + if (!progress->buffer) + runtime_trigger->serialized_size = progress->offset - offset; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) +{ + 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]; + if (runtime_trigger->id == id && runtime_trigger->trigger != NULL) { + /* ignore triggered and waiting achievements */ + if (runtime_trigger->trigger->state == RC_TRIGGER_STATE_UNUPDATED) { + /* only update state if definition hasn't changed (md5 matches) */ + if (rc_runtime_progress_match_md5(progress, runtime_trigger->md5)) + return rc_runtime_progress_read_trigger(progress, runtime_trigger->trigger); + break; + } + } + } + + return RC_OK; +} + +static int rc_runtime_progress_write_leaderboards(rc_runtime_progress_t* progress) +{ + uint32_t i; + uint32_t flags; + int offset = 0; + int result; + + for (i = 0; i < progress->runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &progress->runtime->lboards[i]; + if (!runtime_lboard->lboard) + continue; + + /* don't store state for inactive leaderboards */ + if (!rc_lboard_state_active(runtime_lboard->lboard->state)) + continue; + + if (!progress->buffer) { + if (runtime_lboard->serialized_size) { + progress->offset += runtime_lboard->serialized_size; + continue; + } + + offset = progress->offset; + } + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_LEADERBOARD); + rc_runtime_progress_write_uint(progress, runtime_lboard->id); + rc_runtime_progress_write_md5(progress, runtime_lboard->md5); + + flags = runtime_lboard->lboard->state; + rc_runtime_progress_write_uint(progress, flags); + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->start); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->submit); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->cancel); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_variable(progress, &runtime_lboard->lboard->value); + if (result != RC_OK) + return result; + + rc_runtime_progress_end_chunk(progress); + + if (!progress->buffer) + runtime_lboard->serialized_size = progress->offset - offset; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) +{ + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; + int result; + + for (i = 0; i < progress->runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &progress->runtime->lboards[i]; + if (runtime_lboard->id == id && runtime_lboard->lboard != NULL) { + /* ignore triggered and waiting achievements */ + 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)) { + uint32_t flags = rc_runtime_progress_read_uint(progress); + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->start); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->submit); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->cancel); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_variable(progress, &runtime_lboard->lboard->value); + if (result != RC_OK) + return result; + + runtime_lboard->lboard->state = (char)(flags & 0x7F); + } + break; + } + } + } + + return RC_OK; +} + +static int rc_runtime_progress_write_rich_presence(rc_runtime_progress_t* progress) +{ + const rc_richpresence_display_t* display; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + /* if there are no conditional display strings, there's nothing to capture */ + display = progress->runtime->richpresence->richpresence->first_display; + if (!display->next) + return RC_OK; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_RICHPRESENCE); + rc_runtime_progress_write_md5(progress, progress->runtime->richpresence->md5); + + for (; display->next; display = display->next) { + result = rc_runtime_progress_write_trigger(progress, &display->trigger); + if (result != RC_OK) + return result; + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_rich_presence(rc_runtime_progress_t* progress) +{ + rc_richpresence_display_t* display; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + if (!rc_runtime_progress_match_md5(progress, progress->runtime->richpresence->md5)) { + rc_reset_richpresence(progress->runtime->richpresence->richpresence); + return RC_OK; + } + + display = progress->runtime->richpresence->richpresence->first_display; + for (; display->next; display = display->next) { + result = rc_runtime_progress_read_trigger(progress, &display->trigger); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +static int rc_runtime_progress_serialize_internal(rc_runtime_progress_t* progress) +{ + md5_state_t state; + uint8_t md5[16]; + int result; + + rc_runtime_progress_write_uint(progress, RC_RUNTIME_MARKER); + + if ((result = rc_runtime_progress_write_memrefs(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_variables(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_achievements(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_leaderboards(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_rich_presence(progress)) != RC_OK) + return result; + + rc_runtime_progress_write_uint(progress, RC_RUNTIME_CHUNK_DONE); + rc_runtime_progress_write_uint(progress, 16); + + if (progress->buffer) { + md5_init(&state); + md5_append(&state, progress->buffer, progress->offset); + md5_finish(&state, md5); + } + + rc_runtime_progress_write_md5(progress, md5); + + return RC_OK; +} + +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, runtime, L); + + result = rc_runtime_progress_serialize_internal(&progress); + if (result != RC_OK) + return result; + + return progress.offset; +} + +int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L) +{ + rc_runtime_progress_t progress; + + 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 uint8_t* serialized, lua_State* L) +{ + rc_runtime_progress_t progress; + md5_state_t state; + 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 = (uint8_t*)serialized; + + if (rc_runtime_progress_read_uint(&progress) != RC_RUNTIME_MARKER) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + + for (i = 0; i < runtime->trigger_count; ++i) { + rc_runtime_trigger_t* runtime_trigger = &runtime->triggers[i]; + if (runtime_trigger->trigger) { + /* don't update state for inactive or triggered achievements */ + if (rc_trigger_state_active(runtime_trigger->trigger->state)) { + /* mark active achievements as unupdated. anything that's still unupdated + * after deserializing the progress will be reset to waiting */ + runtime_trigger->trigger->state = RC_TRIGGER_STATE_UNUPDATED; + } + } + } + + for (i = 0; i < runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &runtime->lboards[i]; + if (runtime_lboard->lboard) { + /* don't update state for inactive or triggered achievements */ + if (rc_lboard_state_active(runtime_lboard->lboard->state)) { + /* mark active achievements as unupdated. anything that's still unupdated + * after deserializing the progress will be reset to waiting */ + runtime_lboard->lboard->state = RC_TRIGGER_STATE_UNUPDATED; + } + } + } + + do { + chunk_id = rc_runtime_progress_read_uint(&progress); + chunk_size = rc_runtime_progress_read_uint(&progress); + next_chunk_offset = progress.offset + chunk_size; + + switch (chunk_id) + { + case RC_RUNTIME_CHUNK_MEMREFS: + result = rc_runtime_progress_read_memrefs(&progress); + break; + + case RC_RUNTIME_CHUNK_VARIABLES: + result = rc_runtime_progress_read_variables(&progress); + break; + + case RC_RUNTIME_CHUNK_ACHIEVEMENT: + result = rc_runtime_progress_read_achievement(&progress); + break; + + case RC_RUNTIME_CHUNK_LEADERBOARD: + result = rc_runtime_progress_read_leaderboard(&progress); + break; + + case RC_RUNTIME_CHUNK_RICHPRESENCE: + seen_rich_presence = 1; + result = rc_runtime_progress_read_rich_presence(&progress); + break; + + case RC_RUNTIME_CHUNK_DONE: + md5_init(&state); + md5_append(&state, progress.buffer, progress.offset); + md5_finish(&state, md5); + if (!rc_runtime_progress_match_md5(&progress, md5)) + result = RC_INVALID_STATE; + break; + + default: + if (chunk_size & 0xFFFF0000) + result = RC_INVALID_STATE; /* assume unknown chunk > 64KB is invalid */ + break; + } + + progress.offset = next_chunk_offset; + } while (result == RC_OK && chunk_id != RC_RUNTIME_CHUNK_DONE); + + if (result != RC_OK) { + rc_runtime_reset(runtime); + } + else { + for (i = 0; i < runtime->trigger_count; ++i) { + rc_trigger_t* trigger = runtime->triggers[i].trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_UNUPDATED) + rc_reset_trigger(trigger); + } + + for (i = 0; i < runtime->lboard_count; ++i) { + rc_lboard_t* lboard = runtime->lboards[i].lboard; + if (lboard && lboard->state == RC_TRIGGER_STATE_UNUPDATED) + rc_reset_lboard(lboard); + } + + if (!seen_rich_presence && runtime->richpresence && runtime->richpresence->richpresence) + rc_reset_richpresence(runtime->richpresence->richpresence); + } + + return result; +} diff --git a/3rdparty/rcheevos/src/rcheevos/trigger.c b/3rdparty/rcheevos/src/rcheevos/trigger.c new file mode 100644 index 0000000000..71e186c014 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/trigger.c @@ -0,0 +1,294 @@ +#include "rc_internal.h" + +#include +#include /* memset */ + +void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_t** next; + const char* aux; + + aux = *memaddr; + next = &self->alternative; + + /* reset in case multiple triggers are parsed by the same parse_state */ + parse->measured_target = 0; + parse->has_required_hits = 0; + parse->measured_as_percent = 0; + + if (*aux == 's' || *aux == 'S') { + self->requirement = 0; + } + else { + self->requirement = rc_parse_condset(&aux, parse, 0); + + if (parse->offset < 0) { + return; + } + + self->requirement->next = 0; + } + + while (*aux == 's' || *aux == 'S') { + aux++; + *next = rc_parse_condset(&aux, parse, 0); + + if (parse->offset < 0) { + return; + } + + next = &(*next)->next; + } + + *next = 0; + *memaddr = aux; + + self->measured_target = parse->measured_target; + self->measured_value = parse->measured_target ? RC_MEASURED_UNKNOWN : 0; + self->measured_as_percent = parse->measured_as_percent; + self->state = RC_TRIGGER_STATE_WAITING; + self->has_hits = 0; + self->has_required_hits = parse->has_required_hits; +} + +int rc_trigger_size(const char* memaddr) { + rc_trigger_t* self; + rc_parse_state_t parse; + rc_memref_t* memrefs; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + + self = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { + rc_trigger_t* self; + rc_parse_state_t parse; + + if (!buffer || !memaddr) + return NULL; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_trigger_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + + rc_parse_trigger_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : NULL; +} + +int rc_trigger_state_active(int state) +{ + switch (state) + { + case RC_TRIGGER_STATE_DISABLED: + case RC_TRIGGER_STATE_INACTIVE: + case RC_TRIGGER_STATE_TRIGGERED: + return 0; + + default: + return 1; + } +} + +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) { + if (condition->type == RC_CONDITION_MEASURED && condition->required_hits && + condition->current_hits == measured_value) { + return 1; + } + } + + return 0; +} + +static void rc_reset_trigger_hitcounts(rc_trigger_t* self) { + rc_condset_t* condset; + + if (self->requirement) { + rc_reset_condset(self->requirement); + } + + condset = self->alternative; + + while (condset) { + rc_reset_condset(condset); + condset = condset->next; + } +} + +int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + int ret; + char is_paused; + char is_primed; + + switch (self->state) + { + case RC_TRIGGER_STATE_TRIGGERED: + /* previously triggered. do nothing - return INACTIVE so caller doesn't think it triggered again */ + return RC_TRIGGER_STATE_INACTIVE; + + case RC_TRIGGER_STATE_DISABLED: + /* unsupported. do nothing - return INACTIVE */ + return RC_TRIGGER_STATE_INACTIVE; + + case RC_TRIGGER_STATE_INACTIVE: + /* not yet active. update the memrefs so deltas are correct when it becomes active, then return INACTIVE */ + rc_update_memref_values(self->memrefs, peek, ud); + return RC_TRIGGER_STATE_INACTIVE; + + default: + break; + } + + /* update the memory references */ + rc_update_memref_values(self->memrefs, peek, ud); + + /* process the trigger */ + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = ud; + eval_state.L = L; + + if (self->requirement != NULL) { + ret = rc_test_condset(self->requirement, &eval_state); + is_paused = self->requirement->is_paused; + is_primed = eval_state.primed; + } else { + ret = 1; + is_paused = 0; + is_primed = 1; + } + + condset = self->alternative; + if (condset) { + int sub = 0; + char sub_paused = 1; + char sub_primed = 0; + + do { + sub |= rc_test_condset(condset, &eval_state); + sub_paused &= condset->is_paused; + sub_primed |= eval_state.primed; + + condset = condset->next; + } while (condset); + + /* to trigger, the core must be true and at least one alt must be true */ + ret &= sub; + is_primed &= sub_primed; + + /* if the core is not paused, all alts must be paused to count as a paused trigger */ + is_paused |= sub_paused; + } + + /* if paused, the measured value may not be captured, keep the old value */ + if (!is_paused) { + rc_typed_value_convert(&eval_state.measured_value, RC_VALUE_TYPE_UNSIGNED); + self->measured_value = eval_state.measured_value.value.u32; + } + + /* if any ResetIf condition was true, reset the hit counts */ + if (eval_state.was_reset) { + /* if the measured value came from a hit count, reset it. do this before calling + * rc_reset_trigger_hitcounts in case we need to call rc_condset_is_measured_from_hitcount */ + if (eval_state.measured_from_hits) { + self->measured_value = 0; + } + else if (is_paused && self->measured_value) { + /* if the measured value is in a paused group, measured_from_hits won't have been set. + * attempt to determine if it should have been */ + if (self->requirement && self->requirement->is_paused && + rc_condset_is_measured_from_hitcount(self->requirement, self->measured_value)) { + self->measured_value = 0; + } + else { + for (condset = self->alternative; condset; condset = condset->next) { + if (condset->is_paused && rc_condset_is_measured_from_hitcount(condset, self->measured_value)) { + self->measured_value = 0; + break; + } + } + } + } + + rc_reset_trigger_hitcounts(self); + + /* if there were hit counts to clear, return RESET, but don't change the state */ + if (self->has_hits) { + self->has_hits = 0; + + /* cannot be PRIMED while ResetIf is true */ + if (self->state == RC_TRIGGER_STATE_PRIMED) + self->state = RC_TRIGGER_STATE_ACTIVE; + + return RC_TRIGGER_STATE_RESET; + } + + /* any hits that were tallied were just reset */ + eval_state.has_hits = 0; + is_primed = 0; + } + else if (ret) { + /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ + if (self->state == RC_TRIGGER_STATE_WAITING) { + rc_reset_trigger(self); + self->has_hits = 0; + return RC_TRIGGER_STATE_WAITING; + } + + /* trigger was triggered */ + self->state = RC_TRIGGER_STATE_TRIGGERED; + return RC_TRIGGER_STATE_TRIGGERED; + } + + /* did not trigger this frame - update the information we'll need for next time */ + self->has_hits = eval_state.has_hits; + + if (is_paused) { + self->state = RC_TRIGGER_STATE_PAUSED; + } + else if (is_primed) { + self->state = RC_TRIGGER_STATE_PRIMED; + } + else { + self->state = RC_TRIGGER_STATE_ACTIVE; + } + + /* if an individual condition was reset, notify the caller */ + if (eval_state.was_cond_reset) + return RC_TRIGGER_STATE_RESET; + + /* otherwise, just return the current state */ + return self->state; +} + +int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) { + /* for backwards compatibilty, rc_test_trigger always assumes the achievement is active */ + self->state = RC_TRIGGER_STATE_ACTIVE; + + return (rc_evaluate_trigger(self, peek, ud, L) == RC_TRIGGER_STATE_TRIGGERED); +} + +void rc_reset_trigger(rc_trigger_t* self) { + if (!self) + return; + + rc_reset_trigger_hitcounts(self); + + self->state = RC_TRIGGER_STATE_WAITING; + + if (self->measured_target) + self->measured_value = RC_MEASURED_UNKNOWN; + + self->has_hits = 0; +} diff --git a/3rdparty/rcheevos/src/rcheevos/value.c b/3rdparty/rcheevos/src/rcheevos/value.c new file mode 100644 index 0000000000..3662fc3649 --- /dev/null +++ b/3rdparty/rcheevos/src/rcheevos/value.c @@ -0,0 +1,719 @@ +#include "rc_internal.h" + +#include /* memset */ +#include /* isdigit */ +#include /* FLT_EPSILON */ + +static void rc_parse_cond_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_t** next_clause; + + next_clause = &self->conditions; + + do + { + parse->measured_target = 0; /* passing is_value=1 should prevent any conflicts, but clear it out anyway */ + *next_clause = rc_parse_condset(memaddr, parse, 1); + if (parse->offset < 0) { + return; + } + + if (**memaddr == 'S' || **memaddr == 's') { + /* alt groups not supported */ + parse->offset = RC_INVALID_VALUE_FLAG; + } + else if (parse->measured_target == 0) { + parse->offset = RC_MISSING_VALUE_MEASURED; + } + else if (**memaddr == '$') { + /* maximum of */ + ++(*memaddr); + next_clause = &(*next_clause)->next; + continue; + } + + break; + } while (1); + + (*next_clause)->next = 0; +} + +void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condition_t** next; + rc_condset_t** next_clause; + rc_condition_t* cond; + char buffer[64] = "A:"; + const char* buffer_ptr; + char* ptr; + + /* convert legacy format into condset */ + self->conditions = RC_ALLOC(rc_condset_t, parse); + memset(self->conditions, 0, sizeof(rc_condset_t)); + + next = &self->conditions->conditions; + next_clause = &self->conditions->next; + + for (;; ++(*memaddr)) { + buffer[0] = 'A'; /* reset to AddSource */ + ptr = &buffer[2]; + + /* extract the next clause */ + for (;; ++(*memaddr)) { + switch (**memaddr) { + case '_': /* add next */ + case '$': /* maximum of */ + case '\0': /* end of string */ + case ':': /* end of leaderboard clause */ + case ')': /* end of rich presence macro */ + *ptr = '\0'; + break; + + case '*': + *ptr++ = '*'; + + buffer_ptr = *memaddr + 1; + if (*buffer_ptr == '-') { + buffer[0] = 'B'; /* change to SubSource */ + ++(*memaddr); /* don't copy sign */ + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + else if (*buffer_ptr == '+') { + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + + /* if it looks like a floating point number, add the 'f' prefix */ + while (isdigit((unsigned char)*buffer_ptr)) + ++buffer_ptr; + if (*buffer_ptr == '.') + *ptr++ = 'f'; + continue; + + default: + *ptr++ = **memaddr; + continue; + } + + break; + } + + /* process the clause */ + buffer_ptr = buffer; + cond = rc_parse_condition(&buffer_ptr, parse, 0); + if (parse->offset < 0) + return; + + if (*buffer_ptr) { + /* whatever we copied as a single condition was not fully consumed */ + parse->offset = RC_INVALID_COMPARISON; + return; + } + + switch (cond->oper) { + case RC_OPERATOR_MULT: + case RC_OPERATOR_DIV: + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_NONE: + break; + + default: + parse->offset = RC_INVALID_OPERATOR; + return; + } + + *next = cond; + + if (**memaddr == '_') { + /* add next */ + next = &cond->next; + continue; + } + + if (cond->type == RC_CONDITION_SUB_SOURCE) { + /* cannot change SubSource to Measured. add a dummy condition */ + next = &cond->next; + buffer_ptr = "A:0"; + cond = rc_parse_condition(&buffer_ptr, parse, 0); + *next = cond; + } + + /* convert final AddSource condition to Measured */ + cond->type = RC_CONDITION_MEASURED; + cond->next = 0; + + if (**memaddr != '$') { + /* end of valid string */ + *next_clause = 0; + break; + } + + /* max of ($), start a new clause */ + *next_clause = RC_ALLOC(rc_condset_t, parse); + + if (parse->buffer) /* don't clear in sizing mode or pointer will break */ + memset(*next_clause, 0, sizeof(rc_condset_t)); + + next = &(*next_clause)->conditions; + next_clause = &(*next_clause)->next; + } +} + +void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + /* if it starts with a condition flag (M: A: B: C:), parse the conditions */ + if ((*memaddr)[1] == ':') { + rc_parse_cond_value(self, memaddr, parse); + } + else { + rc_parse_legacy_value(self, memaddr, parse); + } + + self->name = "(unnamed)"; + self->value.value = self->value.prior = 0; + self->value.changed = 0; + self->next = 0; +} + +int rc_value_size(const char* memaddr) { + rc_value_t* self; + rc_parse_state_t parse; + rc_memref_t* first_memref; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &first_memref); + + self = RC_ALLOC(rc_value_t, &parse); + rc_parse_value_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { + rc_value_t* self; + rc_parse_state_t parse; + + if (!buffer || !memaddr) + return NULL; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_value_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + + rc_parse_value_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : NULL; +} + +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + int valid = 0; + + rc_update_memref_values(self->memrefs, peek, ud); + + value->value.i32 = 0; + value->type = RC_VALUE_TYPE_SIGNED; + + for (condset = self->conditions; condset != NULL; condset = condset->next) { + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = ud; + eval_state.L = L; + + rc_test_condset(condset, &eval_state); + + if (condset->is_paused) + continue; + + if (eval_state.was_reset) { + /* if any ResetIf condition was true, reset the hit counts + * NOTE: ResetIf only affects the current condset when used in values! + */ + rc_reset_condset(condset); + + /* if the measured value came from a hit count, reset it too */ + if (eval_state.measured_from_hits) { + eval_state.measured_value.value.u32 = 0; + eval_state.measured_value.type = RC_VALUE_TYPE_UNSIGNED; + } + } + + if (!valid) { + /* capture the first valid measurement */ + memcpy(value, &eval_state.measured_value, sizeof(*value)); + valid = 1; + } + else { + /* multiple condsets are currently only used for the MAX_OF operation. + * only keep the condset's value if it's higher than the current highest value. + */ + if (rc_typed_value_compare(&eval_state.measured_value, value, RC_OPERATOR_GT)) + memcpy(value, &eval_state.measured_value, sizeof(*value)); + } + } + + return valid; +} + +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); + + if (valid) { + /* if not paused, store the value so that it's available when paused. */ + rc_typed_value_convert(&result, RC_VALUE_TYPE_UNSIGNED); + rc_update_memref_value(&self->value, result.value.u32); + } + else { + /* when paused, the Measured value will not be captured, use the last captured value. */ + result.value.u32 = self->value.value; + result.type = RC_VALUE_TYPE_UNSIGNED; + } + + rc_typed_value_convert(&result, RC_VALUE_TYPE_SIGNED); + return result.value.i32; +} + +void rc_reset_value(rc_value_t* self) { + rc_condset_t* condset = self->conditions; + while (condset != NULL) { + rc_reset_condset(condset); + condset = condset->next; + } + + self->value.value = self->value.prior = 0; + 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, size_t memaddr_len, rc_parse_state_t* parse) +{ + rc_value_t** variables = parse->variables; + rc_value_t* value; + const char* name; + uint32_t measured_target; + + while ((value = *variables) != NULL) { + if (strncmp(value->name, memaddr, memaddr_len) == 0 && value->name[memaddr_len] == 0) + return value; + + variables = &value->next; + } + + value = RC_ALLOC_SCRATCH(rc_value_t, parse); + memset(&value->value, 0, sizeof(value->value)); + value->value.size = RC_MEMSIZE_VARIABLE; + value->memrefs = NULL; + + /* capture name before calling parse as parse will update memaddr pointer */ + name = rc_alloc_str(parse, memaddr, memaddr_len); + if (!name) + return NULL; + + /* the helper variable likely has a Measured condition. capture the current measured_target so we can restore it + * after generating the variable so the variable's Measured target doesn't conflict with the rest of the trigger. */ + measured_target = parse->measured_target; + + /* disable variable resolution when defining a variable to prevent infinite recursion */ + variables = parse->variables; + parse->variables = NULL; + rc_parse_value_internal(value, &memaddr, parse); + parse->variables = variables; + + /* restore the measured target */ + parse->measured_target = measured_target; + + /* store name after calling parse as parse will set name to (unnamed) */ + value->name = name; + + /* append the new variable to the end of the list (have to re-evaluate in case any others were added) */ + while (*variables != NULL) + variables = &(*variables)->next; + *variables = value; + + return value; +} + +void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L) { + rc_typed_value_t result; + + while (variable) { + if (rc_evaluate_value_typed(variable, &result, peek, ud, L)) { + /* store the raw bytes and type to be restored by rc_typed_value_from_memref_value */ + rc_update_memref_value(&variable->value, result.value.u32); + variable->value.type = result.type; + } + + variable = variable->next; + } +} + +void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref) { + value->value.u32 = memref->value; + + if (memref->size == RC_MEMSIZE_VARIABLE) { + /* a variable can be any of the supported types, but the raw data was copied into u32 */ + value->type = memref->type; + } + else { + /* not a variable, only u32 is supported */ + value->type = RC_VALUE_TYPE_UNSIGNED; + } +} + +void rc_typed_value_convert(rc_typed_value_t* value, char new_type) { + switch (new_type) { + case RC_VALUE_TYPE_UNSIGNED: + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: + return; + case RC_VALUE_TYPE_SIGNED: + value->value.u32 = (unsigned)value->value.i32; + break; + case RC_VALUE_TYPE_FLOAT: + value->value.u32 = (unsigned)value->value.f32; + break; + default: + value->value.u32 = 0; + break; + } + break; + + case RC_VALUE_TYPE_SIGNED: + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: + return; + case RC_VALUE_TYPE_UNSIGNED: + value->value.i32 = (int)value->value.u32; + break; + case RC_VALUE_TYPE_FLOAT: + value->value.i32 = (int)value->value.f32; + break; + default: + value->value.i32 = 0; + break; + } + break; + + case RC_VALUE_TYPE_FLOAT: + switch (value->type) { + case RC_VALUE_TYPE_FLOAT: + return; + case RC_VALUE_TYPE_UNSIGNED: + value->value.f32 = (float)value->value.u32; + break; + case RC_VALUE_TYPE_SIGNED: + value->value.f32 = (float)value->value.i32; + break; + default: + value->value.f32 = 0.0; + break; + } + break; + + default: + break; + } + + value->type = new_type; +} + +static rc_typed_value_t* rc_typed_value_convert_into(rc_typed_value_t* dest, const rc_typed_value_t* source, char new_type) { + memcpy(dest, source, sizeof(rc_typed_value_t)); + rc_typed_value_convert(dest, new_type); + return dest; +} + +void rc_typed_value_negate(rc_typed_value_t* value) { + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + rc_typed_value_convert(value, RC_VALUE_TYPE_SIGNED); + /* fallthrough */ /* to RC_VALUE_TYPE_SIGNED */ + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = -(value->value.i32); + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = -(value->value.f32); + break; + + default: + break; + } +} + +void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + if (amount->type != value->type && value->type != RC_VALUE_TYPE_NONE) + amount = rc_typed_value_convert_into(&converted, amount, value->type); + + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + value->value.u32 += amount->value.u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 += amount->value.i32; + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 += amount->value.f32; + break; + + case RC_VALUE_TYPE_NONE: + memcpy(value, amount, sizeof(rc_typed_value_t)); + break; + + default: + break; + } +} + +void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + /* the c standard for unsigned multiplication is well defined as non-overflowing truncation + * to the type's size. this allows negative multiplication through twos-complements. i.e. + * 1 * -1 (0xFFFFFFFF) = 0xFFFFFFFF = -1 + * 3 * -2 (0xFFFFFFFE) = 0x2FFFFFFFA & 0xFFFFFFFF = 0xFFFFFFFA = -6 + * 10 * -5 (0xFFFFFFFB) = 0x9FFFFFFCE & 0xFFFFFFFF = 0xFFFFFFCE = -50 + */ + value->value.u32 *= amount->value.u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.u32 *= (unsigned)amount->value.i32; + break; + + case RC_VALUE_TYPE_FLOAT: + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } + break; + + case RC_VALUE_TYPE_SIGNED: + switch (amount->type) + { + case RC_VALUE_TYPE_SIGNED: + value->value.i32 *= amount->value.i32; + break; + + case RC_VALUE_TYPE_UNSIGNED: + value->value.i32 *= (int)amount->value.u32; + break; + + case RC_VALUE_TYPE_FLOAT: + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } + break; + + case RC_VALUE_TYPE_FLOAT: + if (amount->type == RC_VALUE_TYPE_NONE) { + value->type = RC_VALUE_TYPE_NONE; + } + else { + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + } + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } +} + +void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + if (amount->value.u32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 /= amount->value.u32; + return; + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 /= (int)amount->value.u32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_SIGNED: + if (amount->value.i32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 /= amount->value.i32; + return; + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 /= (unsigned)amount->value.i32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_FLOAT: + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + + if (amount->value.f32 == 0.0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 /= amount->value.f32; +} + +static int rc_typed_value_compare_floats(float f1, float f2, char oper) { + if (f1 == f2) { + /* exactly equal */ + } + else { + /* attempt to match 7 significant digits (24-bit mantissa supports just over 7 significant decimal digits) */ + /* https://stackoverflow.com/questions/17333/what-is-the-most-effective-way-for-float-and-double-comparison */ + const float abs1 = (f1 < 0) ? -f1 : f1; + const float abs2 = (f2 < 0) ? -f2 : f2; + const float threshold = ((abs1 < abs2) ? abs1 : abs2) * FLT_EPSILON; + const float diff = f1 - f2; + const float abs_diff = (diff < 0) ? -diff : diff; + + if (abs_diff <= threshold) { + /* approximately equal */ + } + else if (diff > threshold) { + /* greater */ + switch (oper) { + case RC_OPERATOR_NE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + return 1; + + default: + return 0; + } + } + else { + /* lesser */ + switch (oper) { + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } + } + } + + /* exactly or approximately equal */ + switch (oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper) { + rc_typed_value_t converted_value2; + if (value2->type != value1->type) + value2 = rc_typed_value_convert_into(&converted_value2, value2, value1->type); + + switch (value1->type) { + case RC_VALUE_TYPE_UNSIGNED: + switch (oper) { + case RC_OPERATOR_EQ: return value1->value.u32 == value2->value.u32; + case RC_OPERATOR_NE: return value1->value.u32 != value2->value.u32; + case RC_OPERATOR_LT: return value1->value.u32 < value2->value.u32; + case RC_OPERATOR_LE: return value1->value.u32 <= value2->value.u32; + case RC_OPERATOR_GT: return value1->value.u32 > value2->value.u32; + case RC_OPERATOR_GE: return value1->value.u32 >= value2->value.u32; + default: return 1; + } + + case RC_VALUE_TYPE_SIGNED: + switch (oper) { + case RC_OPERATOR_EQ: return value1->value.i32 == value2->value.i32; + case RC_OPERATOR_NE: return value1->value.i32 != value2->value.i32; + case RC_OPERATOR_LT: return value1->value.i32 < value2->value.i32; + case RC_OPERATOR_LE: return value1->value.i32 <= value2->value.i32; + case RC_OPERATOR_GT: return value1->value.i32 > value2->value.i32; + case RC_OPERATOR_GE: return value1->value.i32 >= value2->value.i32; + default: return 1; + } + + case RC_VALUE_TYPE_FLOAT: + return rc_typed_value_compare_floats(value1->value.f32, value2->value.f32, oper); + + default: + return 1; + } +} diff --git a/3rdparty/rcheevos/src/rhash/cdreader.c b/3rdparty/rcheevos/src/rhash/cdreader.c new file mode 100644 index 0000000000..9ff9cb7396 --- /dev/null +++ b/3rdparty/rcheevos/src/rhash/cdreader.c @@ -0,0 +1,879 @@ +#include "rc_hash.h" + +#include "../rc_compat.h" + +#include +#include +#include + +/* internal helper functions in hash.c */ +extern void* rc_file_open(const char* path); +extern void rc_file_seek(void* file_handle, int64_t offset, int origin); +extern int64_t rc_file_tell(void* file_handle); +extern size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes); +extern void rc_file_close(void* file_handle); +extern int rc_hash_error(const char* message); +extern const char* rc_path_get_filename(const char* path); +extern int rc_path_compare_extension(const char* path, const char* ext); +extern rc_hash_message_callback verbose_message_callback; + +struct cdrom_t +{ + void* file_handle; /* the file handle for reading the track data */ + int sector_size; /* the size of each sector in the track data */ + int sector_header_size; /* the offset to the raw data within a sector block */ + int raw_data_size; /* the amount of raw data within a sector block */ + int64_t file_track_offset;/* the offset of the track data within the file */ + int track_first_sector; /* the first absolute sector associated to the track (includes pregap) */ + int track_pregap_sectors; /* the number of pregap sectors */ +#ifndef NDEBUG + uint32_t track_id; /* the index of the track */ +#endif +}; + +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); + int frames = (header[14] >> 4) * 10 + (header[14] & 0x0F); + + /* convert the MSF value to a sector index, and subtract 150 (2 seconds) per: + * For data and mixed mode media (those conforming to ISO/IEC 10149), logical block address + * zero shall be assigned to the block at MSF address 00/02/00 */ + return ((minutes * 60) + seconds) * 75 + frames - 150; +} + +static void cdreader_determine_sector_size(struct cdrom_t* cdrom) +{ + /* Attempt to determine the sector and header sizes. The CUE file may be lying. + * Look for the sync pattern using each of the supported sector sizes. + * 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 uint8_t sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + }; + + uint8_t header[32]; + const int64_t toc_sector = 16 + cdrom->track_pregap_sectors; + + cdrom->sector_size = 0; + cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2048; + + rc_file_seek(cdrom->file_handle, toc_sector * 2352 + cdrom->file_track_offset, SEEK_SET); + if (rc_file_read(cdrom->file_handle, header, sizeof(header)) < sizeof(header)) + return; + + if (memcmp(header, sync_pattern, 12) == 0) + { + cdrom->sector_size = 2352; + + if (memcmp(&header[25], "CD001", 5) == 0) + cdrom->sector_header_size = 24; + else + cdrom->sector_header_size = 16; + + cdrom->track_first_sector = cdreader_get_sector(header) - (int)toc_sector; + } + else + { + rc_file_seek(cdrom->file_handle, toc_sector * 2336 + cdrom->file_track_offset, SEEK_SET); + rc_file_read(cdrom->file_handle, header, sizeof(header)); + + if (memcmp(header, sync_pattern, 12) == 0) + { + cdrom->sector_size = 2336; + + if (memcmp(&header[25], "CD001", 5) == 0) + cdrom->sector_header_size = 24; + else + cdrom->sector_header_size = 16; + + cdrom->track_first_sector = cdreader_get_sector(header) - (int)toc_sector; + } + else + { + rc_file_seek(cdrom->file_handle, toc_sector * 2048 + cdrom->file_track_offset, SEEK_SET); + rc_file_read(cdrom->file_handle, header, sizeof(header)); + + if (memcmp(&header[1], "CD001", 5) == 0) + { + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + } + } +} + +static void* cdreader_open_bin_track(const char* path, uint32_t track) +{ + void* file_handle; + struct cdrom_t* cdrom; + + if (track > 1) + { + if (verbose_message_callback) + verbose_message_callback("Cannot locate secondary tracks without a cue sheet"); + + return NULL; + } + + file_handle = rc_file_open(path); + if (!file_handle) + return NULL; + + cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + return NULL; + cdrom->file_handle = file_handle; +#ifndef NDEBUG + cdrom->track_id = track; +#endif + + cdreader_determine_sector_size(cdrom); + + if (cdrom->sector_size == 0) + { + int64_t size; + + rc_file_seek(cdrom->file_handle, 0, SEEK_END); + size = rc_file_tell(cdrom->file_handle); + + if ((size % 2352) == 0) + { + /* raw tracks use all 2352 bytes and have a 24 byte header */ + cdrom->sector_size = 2352; + cdrom->sector_header_size = 24; + } + else if ((size % 2048) == 0) + { + /* cooked tracks eliminate all header/footer data */ + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + else if ((size % 2336) == 0) + { + /* MODE 2 format without 16-byte sync data */ + cdrom->sector_size = 2336; + cdrom->sector_header_size = 8; + } + else + { + free(cdrom); + + if (verbose_message_callback) + verbose_message_callback("Could not determine sector size"); + + return NULL; + } + } + + return cdrom; +} + +static int cdreader_open_bin(struct cdrom_t* cdrom, const char* path, const char* mode) +{ + cdrom->file_handle = rc_file_open(path); + if (!cdrom->file_handle) + return 0; + + /* determine sector size */ + cdreader_determine_sector_size(cdrom); + + /* could not determine, which means we'll probably have more issues later + * but use the CUE provided information anyway + */ + if (cdrom->sector_size == 0) + { + /* All of these modes have 2048 byte payloads. In MODE1/2352 and MODE2/2352 + * modes, the mode can actually be specified per sector to change the payload + * size, but that reduces the ability to recover from errors when the disc + * is damaged, so it's seldomly used, and when it is, it's mostly for audio + * or video data where a blip or two probably won't be noticed by the user. + * So, while we techincally support all of the following modes, we only do + * so with 2048 byte payloads. + * http://totalsonicmastering.com/cuesheetsyntax.htm + * MODE1/2048 ? CDROM Mode1 Data (cooked) [no header, no footer] + * MODE1/2352 ? CDROM Mode1 Data (raw) [16 byte header, 288 byte footer] + * MODE2/2336 ? CDROM-XA Mode2 Data [8 byte header, 280 byte footer] + * MODE2/2352 ? CDROM-XA Mode2 Data [24 byte header, 280 byte footer] + */ + if (memcmp(mode, "MODE2/2352", 10) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 24; + } + else if (memcmp(mode, "MODE1/2048", 10) == 0) + { + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + else if (memcmp(mode, "MODE2/2336", 10) == 0) + { + cdrom->sector_size = 2336; + cdrom->sector_header_size = 8; + } + else if (memcmp(mode, "MODE1/2352", 10) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 16; + } + else if (memcmp(mode, "AUDIO", 5) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2352; /* no header or footer data on audio tracks */ + } + } + + return (cdrom->sector_size != 0); +} + +static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name) +{ + const char* filename = rc_path_get_filename(cue_path); + const size_t bin_name_len = strlen(bin_name); + const size_t cue_path_len = filename - cue_path; + const size_t needed = cue_path_len + bin_name_len + 1; + + char* bin_filename = (char*)malloc(needed); + if (!bin_filename) + { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)needed); + rc_hash_error((const char*)buffer); + } + else + { + memcpy(bin_filename, cue_path, cue_path_len); + memcpy(bin_filename + cue_path_len, bin_name, bin_name_len + 1); + } + + return bin_filename; +} + +static int64_t cdreader_get_bin_size(const char* cue_path, const char* bin_name) +{ + int64_t size = 0; + char* bin_filename = cdreader_get_bin_path(cue_path, bin_name); + if (bin_filename) + { + /* disable verbose messaging while getting file size */ + rc_hash_message_callback old_verbose_message_callback = verbose_message_callback; + void* file_handle; + verbose_message_callback = NULL; + + file_handle = rc_file_open(bin_filename); + if (file_handle) + { + rc_file_seek(file_handle, 0, SEEK_END); + size = rc_file_tell(file_handle); + rc_file_close(file_handle); + } + + verbose_message_callback = old_verbose_message_callback; + free(bin_filename); + } + + return size; +} + +static void* cdreader_open_cue_track(const char* path, uint32_t track) +{ + void* cue_handle; + int64_t cue_offset = 0; + char buffer[1024]; + char* bin_filename = NULL; + char *ptr, *ptr2, *end; + int done = 0; + int session = 1; + size_t num_read = 0; + struct cdrom_t* cdrom = NULL; + + struct track_t + { + uint32_t id; + int sector_size; + int sector_count; + int first_sector; + int pregap_sectors; + int is_data; + int file_track_offset; + int file_first_sector; + char mode[16]; + char filename[256]; + } current_track, previous_track, largest_track; + + cue_handle = rc_file_open(path); + if (!cue_handle) + return NULL; + + memset(¤t_track, 0, sizeof(current_track)); + memset(&previous_track, 0, sizeof(previous_track)); + memset(&largest_track, 0, sizeof(largest_track)); + + do + { + num_read = rc_file_read(cue_handle, buffer, sizeof(buffer) - 1); + if (num_read == 0) + break; + + buffer[num_read] = 0; + if (num_read == sizeof(buffer) - 1) + end = buffer + sizeof(buffer) * 3 / 4; + else + end = buffer + num_read; + + for (ptr = buffer; ptr < end; ++ptr) + { + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "INDEX ", 6) == 0) + { + int m = 0, s = 0, f = 0; + int index; + int sector_offset; + + ptr += 6; + index = atoi(ptr); + + while (*ptr != ' ' && *ptr != '\n') + ++ptr; + while (*ptr == ' ') + ++ptr; + + /* convert mm:ss:ff to sector count */ + sscanf_s(ptr, "%d:%d:%d", &m, &s, &f); + sector_offset = ((m * 60) + s) * 75 + f; + + if (current_track.first_sector == -1) + { + current_track.first_sector = sector_offset; + if (strcmp(current_track.filename, previous_track.filename) == 0) + { + previous_track.sector_count = current_track.first_sector - previous_track.first_sector; + current_track.file_track_offset += previous_track.sector_count * previous_track.sector_size; + } + + /* if looking for the largest data track, determine previous track size */ + if (track == RC_HASH_CDTRACK_LARGEST && previous_track.sector_count > largest_track.sector_count && + previous_track.is_data) + { + memcpy(&largest_track, &previous_track, sizeof(largest_track)); + } + } + + if (index == 1) + { + current_track.pregap_sectors = (sector_offset - current_track.first_sector); + + if (verbose_message_callback) + { + char message[128]; + char* scan = current_track.mode; + while (*scan && !isspace((unsigned char)*scan)) + ++scan; + *scan = '\0'; + + /* it's undesirable to truncate offset to 32-bits, but %lld isn't defined in c89. */ + snprintf(message, sizeof(message), "Found %s track %d (first sector %d, sector size %d, %d pregap sectors)", + current_track.mode, current_track.id, current_track.first_sector, current_track.sector_size, current_track.pregap_sectors); + verbose_message_callback(message); + } + + if (current_track.id == track) + { + done = 1; + break; + } + + if (track == RC_HASH_CDTRACK_FIRST_DATA && current_track.is_data) + { + track = current_track.id; + done = 1; + break; + } + + if (track == RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION && session == 2) + { + track = current_track.id; + done = 1; + break; + } + } + } + else if (strncasecmp(ptr, "TRACK ", 6) == 0) + { + if (current_track.sector_size) + memcpy(&previous_track, ¤t_track, sizeof(current_track)); + + ptr += 6; + current_track.id = atoi(ptr); + + current_track.pregap_sectors = -1; + current_track.first_sector = -1; + + while (*ptr != ' ') + ++ptr; + while (*ptr == ' ') + ++ptr; + memcpy(current_track.mode, ptr, sizeof(current_track.mode)); + current_track.is_data = (memcmp(current_track.mode, "MODE", 4) == 0); + + if (current_track.is_data) + { + current_track.sector_size = atoi(ptr + 6); + } + else + { + /* assume AUDIO */ + current_track.sector_size = 2352; + } + } + else if (strncasecmp(ptr, "FILE ", 5) == 0) + { + if (current_track.sector_size) + { + memcpy(&previous_track, ¤t_track, sizeof(previous_track)); + + if (previous_track.sector_count == 0) + { + const uint32_t file_sector_count = (uint32_t)cdreader_get_bin_size(path, previous_track.filename) / previous_track.sector_size; + previous_track.sector_count = file_sector_count - previous_track.first_sector; + } + + /* if looking for the largest data track, check to see if this one is larger */ + if (track == RC_HASH_CDTRACK_LARGEST && previous_track.is_data && + previous_track.sector_count > largest_track.sector_count) + { + memcpy(&largest_track, &previous_track, sizeof(largest_track)); + } + } + + memset(¤t_track, 0, sizeof(current_track)); + + current_track.file_first_sector = previous_track.file_first_sector + + previous_track.first_sector + previous_track.sector_count; + + ptr += 5; + ptr2 = ptr; + if (*ptr == '"') + { + ++ptr; + do + { + ++ptr2; + } while (*ptr2 && *ptr2 != '\n' && *ptr2 != '"'); + } + else + { + do + { + ++ptr2; + } while (*ptr2 && *ptr2 != '\n' && *ptr2 != ' '); + } + + if (ptr2 - ptr < (int)sizeof(current_track.filename)) + memcpy(current_track.filename, ptr, ptr2 - ptr); + } + else if (strncasecmp(ptr, "REM ", 4) == 0) + { + ptr += 4; + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "SESSION ", 8) == 0) + { + ptr += 8; + while (*ptr == ' ') + ++ptr; + session = atoi(ptr); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + } + + if (done) + break; + + cue_offset += (ptr - buffer); + rc_file_seek(cue_handle, cue_offset, SEEK_SET); + + } while (1); + + rc_file_close(cue_handle); + + if (track == RC_HASH_CDTRACK_LARGEST) + { + if (current_track.sector_size && current_track.is_data) + { + const uint32_t file_sector_count = (uint32_t)cdreader_get_bin_size(path, current_track.filename) / current_track.sector_size; + current_track.sector_count = file_sector_count - current_track.first_sector; + + if (largest_track.sector_count > current_track.sector_count) + memcpy(¤t_track, &largest_track, sizeof(current_track)); + } + else + { + memcpy(¤t_track, &largest_track, sizeof(current_track)); + } + + track = current_track.id; + } + else if (track == RC_HASH_CDTRACK_LAST && !done) + { + track = current_track.id; + } + + if (current_track.id == track) + { + cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + { + snprintf((char*)buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); + rc_hash_error((const char*)buffer); + return NULL; + } + + cdrom->file_track_offset = current_track.file_track_offset; + cdrom->track_pregap_sectors = current_track.pregap_sectors; + cdrom->track_first_sector = current_track.file_first_sector + current_track.first_sector; +#ifndef NDEBUG + cdrom->track_id = current_track.id; +#endif + + /* verify existance of bin file */ + bin_filename = cdreader_get_bin_path(path, current_track.filename); + if (bin_filename) + { + if (cdreader_open_bin(cdrom, bin_filename, current_track.mode)) + { + if (verbose_message_callback) + { + if (cdrom->track_pregap_sectors) + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d, %d pregap sectors)", + track, cdrom->sector_size, cdrom->track_pregap_sectors); + else + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", track, cdrom->sector_size); + + verbose_message_callback((const char*)buffer); + } + } + else + { + if (cdrom->file_handle) + { + rc_file_close(cdrom->file_handle); + snprintf((char*)buffer, sizeof(buffer), "Could not determine sector size for %s track", current_track.mode); + } + else + { + snprintf((char*)buffer, sizeof(buffer), "Could not open %s", bin_filename); + } + + rc_hash_error((const char*)buffer); + + free(cdrom); + cdrom = NULL; + } + + free(bin_filename); + } + } + + return cdrom; +} + +static void* cdreader_open_gdi_track(const char* path, uint32_t track) +{ + void* file_handle; + char buffer[1024]; + char mode[16] = "MODE1/"; + char sector_size[16]; + char file[256]; + int64_t track_size; + int track_type; + char* bin_path = NULL; + uint32_t current_track = 0; + char* ptr, *ptr2, *end; + int lba = 0; + + uint32_t largest_track = 0; + int64_t largest_track_size = 0; + char largest_track_file[256]; + char largest_track_sector_size[16]; + int largest_track_lba = 0; + + int found = 0; + size_t num_read = 0; + int64_t file_offset = 0; + struct cdrom_t* cdrom = NULL; + + file_handle = rc_file_open(path); + if (!file_handle) + return NULL; + + file[0] = '\0'; + do + { + num_read = rc_file_read(file_handle, buffer, sizeof(buffer) - 1); + if (num_read == 0) + break; + + buffer[num_read] = 0; + if (num_read == sizeof(buffer) - 1) + end = buffer + sizeof(buffer) * 3 / 4; + else + end = buffer + num_read; + + ptr = buffer; + + /* the first line contains the number of tracks, so we can get the last track index from it */ + if (track == RC_HASH_CDTRACK_LAST) + track = atoi(ptr); + + /* first line contains the number of tracks and will be skipped */ + while (ptr < end) + { + /* skip until next newline */ + while (*ptr != '\n' && ptr < end) + ++ptr; + + /* skip newlines */ + while ((*ptr == '\n' || *ptr == '\r') && ptr < end) + ++ptr; + + /* line format: [trackid] [lba] [type] [sectorsize] [file] [?] */ + while (isspace((unsigned char)*ptr)) + ++ptr; + + current_track = (uint32_t)atoi(ptr); + if (track && current_track != track && track != RC_HASH_CDTRACK_FIRST_DATA) + continue; + + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + lba = atoi(ptr); + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + track_type = atoi(ptr); + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + ptr2 = sector_size; + while (isdigit((unsigned char)*ptr)) + *ptr2++ = *ptr++; + *ptr2 = '\0'; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + ptr2 = file; + if (*ptr == '\"') + { + ++ptr; + while (*ptr != '\"') + *ptr2++ = *ptr++; + ++ptr; + } + else + { + while (*ptr != ' ') + *ptr2++ = *ptr++; + } + *ptr2 = '\0'; + + if (track == current_track) + { + found = 1; + break; + } + else if (track == RC_HASH_CDTRACK_FIRST_DATA && track_type == 4) + { + track = current_track; + found = 1; + break; + } + else if (track == RC_HASH_CDTRACK_LARGEST && track_type == 4) + { + track_size = cdreader_get_bin_size(path, file); + if (track_size > largest_track_size) + { + largest_track_size = track_size; + largest_track = current_track; + largest_track_lba = lba; + strcpy_s(largest_track_file, sizeof(largest_track_file), file); + strcpy_s(largest_track_sector_size, sizeof(largest_track_sector_size), sector_size); + } + } + } + + if (found) + break; + + file_offset += (ptr - buffer); + rc_file_seek(file_handle, file_offset, SEEK_SET); + + } while (1); + + rc_file_close(file_handle); + + cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + { + snprintf((char*)buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); + rc_hash_error((const char*)buffer); + return NULL; + } + + /* if we were tracking the largest track, make it the current track. + * otherwise, current_track will be the requested track, or last track. */ + if (largest_track != 0 && largest_track != current_track) + { + current_track = largest_track; + strcpy_s(file, sizeof(file), largest_track_file); + strcpy_s(sector_size, sizeof(sector_size), largest_track_sector_size); + lba = largest_track_lba; + } + + /* open the bin file for the track - construct mode parameter from sector_size */ + ptr = &mode[6]; + ptr2 = sector_size; + while (*ptr2 && *ptr2 != '\"') + *ptr++ = *ptr2++; + *ptr = '\0'; + + bin_path = cdreader_get_bin_path(path, file); + if (cdreader_open_bin(cdrom, bin_path, mode)) + { + cdrom->track_pregap_sectors = 0; + cdrom->track_first_sector = lba; +#ifndef NDEBUG + cdrom->track_id = current_track; +#endif + + if (verbose_message_callback) + { + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", current_track, cdrom->sector_size); + verbose_message_callback((const char*)buffer); + } + } + else + { + snprintf((char*)buffer, sizeof(buffer), "Could not open %s", bin_path); + rc_hash_error((const char*)buffer); + + free(cdrom); + cdrom = NULL; + } + + free(bin_path); + + return cdrom; +} + +static void* cdreader_open_track(const char* path, uint32_t track) +{ + /* backwards compatibility - 0 used to mean largest */ + if (track == 0) + track = RC_HASH_CDTRACK_LARGEST; + + if (rc_path_compare_extension(path, "cue")) + return cdreader_open_cue_track(path, track); + if (rc_path_compare_extension(path, "gdi")) + return cdreader_open_gdi_track(path, track); + + return cdreader_open_bin_track(path, track); +} + +static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + int64_t sector_start; + size_t num_read, total_read = 0; + uint8_t* buffer_ptr = (uint8_t*)buffer; + + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (!cdrom) + return 0; + + if (sector < (uint32_t)cdrom->track_first_sector) + return 0; + + sector_start = (int64_t)(sector - cdrom->track_first_sector) * cdrom->sector_size + + cdrom->sector_header_size + cdrom->file_track_offset; + + while (requested_bytes > (size_t)cdrom->raw_data_size) + { + rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, cdrom->raw_data_size); + total_read += num_read; + + if (num_read < (size_t)cdrom->raw_data_size) + return total_read; + + buffer_ptr += cdrom->raw_data_size; + sector_start += cdrom->sector_size; + requested_bytes -= cdrom->raw_data_size; + } + + rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, (int)requested_bytes); + total_read += num_read; + + return total_read; +} + +static void cdreader_close_track(void* track_handle) +{ + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (cdrom) + { + if (cdrom->file_handle) + rc_file_close(cdrom->file_handle); + + free(track_handle); + } +} + +static uint32_t cdreader_first_track_sector(void* track_handle) +{ + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (cdrom) + return cdrom->track_first_sector + cdrom->track_pregap_sectors; + + return 0; +} + +void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader) +{ + cdreader->open_track = cdreader_open_track; + cdreader->read_sector = cdreader_read_sector; + cdreader->close_track = cdreader_close_track; + cdreader->first_track_sector = cdreader_first_track_sector; +} + +void rc_hash_init_default_cdreader(void) +{ + struct rc_hash_cdreader cdreader; + rc_hash_get_default_cdreader(&cdreader); + rc_hash_init_custom_cdreader(&cdreader); +} diff --git a/3rdparty/rcheevos/src/rhash/hash.c b/3rdparty/rcheevos/src/rhash/hash.c new file mode 100644 index 0000000000..5d65512b68 --- /dev/null +++ b/3rdparty/rcheevos/src/rhash/hash.c @@ -0,0 +1,3306 @@ +#include "rc_hash.h" + +#include "../rc_compat.h" + +#include "md5.h" + +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#endif + +/* arbitrary limit to prevent allocating and hashing large files */ +#define MAX_BUFFER_SIZE 64 * 1024 * 1024 + +const char* rc_path_get_filename(const char* path); +static int rc_hash_whole_file(char hash[33], const char* path); + +/* ===================================================== */ + +static rc_hash_message_callback error_message_callback = NULL; +rc_hash_message_callback verbose_message_callback = NULL; + +void rc_hash_init_error_message_callback(rc_hash_message_callback callback) +{ + error_message_callback = callback; +} + +int rc_hash_error(const char* message) +{ + if (error_message_callback) + error_message_callback(message); + + return 0; +} + +void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback) +{ + verbose_message_callback = callback; +} + +static void rc_hash_verbose(const char* message) +{ + if (verbose_message_callback) + verbose_message_callback(message); +} + +/* ===================================================== */ + +static struct rc_hash_filereader filereader_funcs; +static struct rc_hash_filereader* filereader = NULL; + +static void* filereader_open(const char* path) +{ +#if defined(WINVER) && WINVER >= 0x0500 + /* Windows requires using wchar APIs for Unicode paths */ + /* Note that MultiByteToWideChar will only be defined for >= Windows 2000 */ + wchar_t* wpath; + int wpath_length; + FILE* fp; + + /* Calculate wpath length from path */ + wpath_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, NULL, 0); + if (wpath_length == 0) /* 0 indicates error (this is likely from invalid UTF-8) */ + return NULL; + + wpath = (wchar_t*)malloc(wpath_length * sizeof(wchar_t)); + if (!wpath) + return NULL; + + if (MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, wpath_length) == 0) + { + free(wpath); + return NULL; + } +#if defined(__STDC_WANT_SECURE_LIB__) + _wfopen_s(&fp, wpath, L"rb"); +#else + fp = _wfopen(wpath, L"rb"); +#endif + free(wpath); + return fp; +#elif defined(__STDC_WANT_SECURE_LIB__) + FILE* fp; + fopen_s(&fp, path, "rb"); + return fp; +#else + return fopen(path, "rb"); +#endif +} + +static void filereader_seek(void* file_handle, int64_t offset, int origin) +{ +#if defined(_WIN32) + _fseeki64((FILE*)file_handle, offset, origin); +#elif defined(_LARGEFILE64_SOURCE) + fseeko64((FILE*)file_handle, offset, origin); +#else + fseek((FILE*)file_handle, offset, origin); +#endif +} + +static int64_t filereader_tell(void* file_handle) +{ +#if defined(_WIN32) + return _ftelli64((FILE*)file_handle); +#elif defined(_LARGEFILE64_SOURCE) + return ftello64((FILE*)file_handle); +#else + return ftell((FILE*)file_handle); +#endif +} + +static size_t filereader_read(void* file_handle, void* buffer, size_t requested_bytes) +{ + return fread(buffer, 1, requested_bytes, (FILE*)file_handle); +} + +static void filereader_close(void* file_handle) +{ + fclose((FILE*)file_handle); +} + +/* for unit tests - normally would call rc_hash_init_custom_filereader(NULL) */ +void rc_hash_reset_filereader(void) +{ + filereader = NULL; +} + +void rc_hash_init_custom_filereader(struct rc_hash_filereader* reader) +{ + /* initialize with defaults first */ + filereader_funcs.open = filereader_open; + filereader_funcs.seek = filereader_seek; + filereader_funcs.tell = filereader_tell; + filereader_funcs.read = filereader_read; + filereader_funcs.close = filereader_close; + + /* hook up any provided custom handlers */ + if (reader) { + if (reader->open) + filereader_funcs.open = reader->open; + + if (reader->seek) + filereader_funcs.seek = reader->seek; + + if (reader->tell) + filereader_funcs.tell = reader->tell; + + if (reader->read) + filereader_funcs.read = reader->read; + + if (reader->close) + filereader_funcs.close = reader->close; + } + + filereader = &filereader_funcs; +} + +void* rc_file_open(const char* path) +{ + void* handle; + + if (!filereader) + { + rc_hash_init_custom_filereader(NULL); + if (!filereader) + return NULL; + } + + handle = filereader->open(path); + if (handle && verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "Opened %s", rc_path_get_filename(path)); + verbose_message_callback(message); + } + + return handle; +} + +void rc_file_seek(void* file_handle, int64_t offset, int origin) +{ + if (filereader) + filereader->seek(file_handle, offset, origin); +} + +int64_t rc_file_tell(void* file_handle) +{ + return (filereader) ? filereader->tell(file_handle) : 0; +} + +size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes) +{ + return (filereader) ? filereader->read(file_handle, buffer, requested_bytes) : 0; +} + +void rc_file_close(void* file_handle) +{ + if (filereader) + filereader->close(file_handle); +} + +/* ===================================================== */ + +static struct rc_hash_cdreader cdreader_funcs; +struct rc_hash_cdreader* cdreader = NULL; + +void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader) +{ + if (reader) + { + memcpy(&cdreader_funcs, reader, sizeof(cdreader_funcs)); + cdreader = &cdreader_funcs; + } + else + { + cdreader = NULL; + } +} + +static void* rc_cd_open_track(const char* path, uint32_t track) +{ + if (cdreader && cdreader->open_track) + return cdreader->open_track(path, track); + + rc_hash_error("no hook registered for cdreader_open_track"); + return NULL; +} + +static size_t rc_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + if (cdreader && cdreader->read_sector) + return cdreader->read_sector(track_handle, sector, buffer, requested_bytes); + + rc_hash_error("no hook registered for cdreader_read_sector"); + return 0; +} + +static uint32_t rc_cd_first_track_sector(void* track_handle) +{ + if (cdreader && cdreader->first_track_sector) + return cdreader->first_track_sector(track_handle); + + rc_hash_error("no hook registered for cdreader_first_track_sector"); + return 0; +} + +static void rc_cd_close_track(void* track_handle) +{ + if (cdreader && cdreader->close_track) + { + cdreader->close_track(track_handle); + return; + } + + rc_hash_error("no hook registered for cdreader_close_track"); +} + +static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uint32_t* size) +{ + uint8_t buffer[2048], *tmp; + int sector; + uint32_t num_sectors = 0; + size_t filename_length; + const char* slash; + + if (!track_handle) + return 0; + + /* we start at the root. don't need to explicitly find it */ + if (*path == '\\') + ++path; + + filename_length = strlen(path); + slash = strrchr(path, '\\'); + if (slash) + { + /* find the directory record for the first part of the path */ + memcpy(buffer, path, slash - path); + buffer[slash - path] = '\0'; + + sector = rc_cd_find_file_sector(track_handle, (const char *)buffer, NULL); + if (!sector) + return 0; + + ++slash; + filename_length -= (slash - path); + path = slash; + } + else + { + 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)) + return 0; + + /* the directory_record starts at 156, the sector containing the table of contents is 2 bytes into that. + * https://www.cdroller.com/htm/readdata.html + */ + sector = buffer[156 + 2] | (buffer[156 + 3] << 8) | (buffer[156 + 4] << 16); + + /* if the table of contents spans more than one sector, it's length of section will exceed it's logical block size */ + logical_block_size = (buffer[128] | (buffer[128 + 1] << 8)); /* logical block size */ + if (logical_block_size == 0) { + num_sectors = 1; + } else { + num_sectors = (buffer[156 + 10] | (buffer[156 + 11] << 8) | (buffer[156 + 12] << 16) | (buffer[156 + 13] << 24)); /* length of section */ + num_sectors /= logical_block_size; + } + } + + /* fetch and process the directory record */ + if (!rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) + return 0; + + tmp = buffer; + do + { + if (tmp >= buffer + sizeof(buffer) || !*tmp) + { + /* end of this path table block. if the path table spans multiple sectors, keep scanning */ + if (num_sectors > 1) + { + --num_sectors; + if (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer))) + { + tmp = buffer; + continue; + } + } + break; + } + + /* filename is 33 bytes into the record and the format is "FILENAME;version" or "DIRECTORY" */ + if ((tmp[32] == filename_length || tmp[33 + filename_length] == ';') && + strncasecmp((const char*)(tmp + 33), path, filename_length) == 0) + { + sector = tmp[2] | (tmp[3] << 8) | (tmp[4] << 16); + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Found %s at sector %d", path, sector); + verbose_message_callback(message); + } + + if (size) + *size = tmp[10] | (tmp[11] << 8) | (tmp[12] << 16) | (tmp[13] << 24); + + return sector; + } + + /* the first byte of the record is the length of the record */ + tmp += *tmp; + } while (1); + + return 0; +} + +/* ===================================================== */ + +const char* rc_path_get_filename(const char* path) +{ + const char* ptr = path + strlen(path); + do + { + if (ptr[-1] == '/' || ptr[-1] == '\\') + break; + + --ptr; + } while (ptr > path); + + return ptr; +} + +static const char* rc_path_get_extension(const char* path) +{ + const char* ptr = path + strlen(path); + do + { + if (ptr[-1] == '.') + return ptr; + + --ptr; + } while (ptr > path); + + return path + strlen(path); +} + +int rc_path_compare_extension(const char* path, const char* ext) +{ + size_t path_len = strlen(path); + size_t ext_len = strlen(ext); + const char* ptr = path + path_len - ext_len; + if (ptr[-1] != '.') + return 0; + + if (memcmp(ptr, ext, ext_len) == 0) + return 1; + + do + { + if (tolower(*ptr) != *ext) + return 0; + + ++ext; + ++ptr; + } while (*ptr); + + return 1; +} + +/* ===================================================== */ + +static void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF00FF00) >> 8 | + (temp & 0x00FF00FF) << 8; + *ptr++ = temp; + } +} + +static void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF000000) >> 24 | + (temp & 0x00FF0000) >> 8 | + (temp & 0x0000FF00) << 8 | + (temp & 0x000000FF) << 24; + *ptr++ = temp; + } +} + +static int rc_hash_finalize(md5_state_t* md5, char hash[33]) +{ + md5_byte_t digest[16]; + + md5_finish(md5, digest); + + /* NOTE: sizeof(hash) is 4 because it's still treated like a pointer, despite specifying a size */ + snprintf(hash, 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] + ); + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Generated hash %s", hash); + verbose_message_callback(message); + } + + return 1; +} + +static int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + md5_state_t md5; + md5_init(&md5); + + if (buffer_size > MAX_BUFFER_SIZE) + buffer_size = MAX_BUFFER_SIZE; + + md5_append(&md5, buffer, (int)buffer_size); + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %u byte buffer", (unsigned)buffer_size); + verbose_message_callback(message); + } + + 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, uint32_t size, const char* description) +{ + uint8_t buffer[2048]; + size_t num_read; + + if ((num_read = rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) < sizeof(buffer)) + { + char message[128]; + snprintf(message, sizeof(message), "Could not read %s", description); + return rc_hash_error(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + if (verbose_message_callback) + { + char message[128]; + if (name) + snprintf(message, sizeof(message), "Hashing %s title (%u bytes) and contents (%u bytes) ", name, (unsigned)strlen(name), size); + else + snprintf(message, sizeof(message), "Hashing %s contents (%u bytes @ sector %u)", description, size, sector); + + verbose_message_callback(message); + } + + if (size < (unsigned)num_read) /* we read a whole sector - only hash the part containing file data */ + num_read = (size_t)size; + + do + { + md5_append(md5, buffer, (int)num_read); + + if (size <= (unsigned)num_read) + break; + size -= (unsigned)num_read; + + ++sector; + if (size >= sizeof(buffer)) + num_read = rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + else + num_read = rc_cd_read_sector(track_handle, sector, buffer, size); + } while (num_read > 0); + + return 1; +} + +static int rc_hash_3do(char hash[33], const char* path) +{ + uint8_t buffer[2048]; + const uint8_t operafs_identifier[7] = { 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01 }; + void* track_handle; + md5_state_t md5; + int sector; + int block_size, block_location; + int offset, stop; + size_t size = 0; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* the Opera filesystem stores the volume information in the first 132 bytes of sector 0 + * https://github.com/barbeque/3dodump/blob/master/OperaFS-Format.md + */ + rc_cd_read_sector(track_handle, 0, buffer, 132); + + if (memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) + { + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Found 3DO CD, title=%.32s", &buffer[0x28]); + verbose_message_callback(message); + } + + /* include the volume header in the hash */ + md5_init(&md5); + md5_append(&md5, buffer, 132); + + /* the block size is at offset 0x4C (assume 0x4C is always 0) */ + block_size = buffer[0x4D] * 65536 + buffer[0x4E] * 256 + buffer[0x4F]; + + /* the root directory block location is at offset 0x64 (and duplicated several + * times, but we just look at the primary record) (assume 0x64 is always 0)*/ + block_location = buffer[0x65] * 65536 + buffer[0x66] * 256 + buffer[0x67]; + + /* multiply the block index by the block size to get the real address */ + block_location *= block_size; + + /* convert that to a sector and read it */ + sector = block_location / 2048; + + do + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + + /* offset to start of entries is at offset 0x10 (assume 0x10 and 0x11 are always 0) */ + offset = buffer[0x12] * 256 + buffer[0x13]; + + /* offset to end of entries is at offset 0x0C (assume 0x0C is always 0) */ + stop = buffer[0x0D] * 65536 + buffer[0x0E] * 256 + buffer[0x0F]; + + while (offset < stop) + { + if (buffer[offset + 0x03] == 0x02) /* file */ + { + if (strcasecmp((const char*)&buffer[offset + 0x20], "LaunchMe") == 0) + { + /* the block size is at offset 0x0C (assume 0x0C is always 0) */ + block_size = buffer[offset + 0x0D] * 65536 + buffer[offset + 0x0E] * 256 + buffer[offset + 0x0F]; + + /* the block location is at offset 0x44 (assume 0x44 is always 0) */ + block_location = buffer[offset + 0x45] * 65536 + buffer[offset + 0x46] * 256 + buffer[offset + 0x47]; + block_location *= block_size; + + /* the file size is at offset 0x10 (assume 0x10 is always 0) */ + size = (size_t)buffer[offset + 0x11] * 65536 + buffer[offset + 0x12] * 256 + buffer[offset + 0x13]; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing header (%u bytes) and %.32s (%u bytes) ", 132, &buffer[offset + 0x20], (unsigned)size); + verbose_message_callback(message); + } + + break; + } + } + + /* the number of extra copies of the file is at offset 0x40 (assume 0x40-0x42 are always 0) */ + offset += 0x48 + buffer[offset + 0x43] * 4; + } + + if (size != 0) + break; + + /* did not find the file, see if the directory listing is continued in another sector */ + offset = buffer[0x02] * 256 + buffer[0x03]; + + /* no more sectors to search*/ + if (offset == 0xFFFF) + break; + + /* get next sector */ + offset *= block_size; + sector = (block_location + offset) / 2048; + } while (1); + + if (size == 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Could not find LaunchMe"); + } + + sector = block_location / 2048; + + while (size > 2048) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= 2048; + } + + rc_cd_read_sector(track_handle, sector, buffer, size); + md5_append(&md5, buffer, (int)size); + } + else + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a 3DO CD"); + } + + rc_cd_close_track(track_handle); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_7800(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&buffer[1], "ATARI7800", 9) == 0) + { + rc_hash_verbose("Ignoring 7800 header"); + + buffer += 128; + buffer_size -= 128; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +struct rc_hash_zip_idx +{ + size_t length; + uint8_t* data; +}; + +static int rc_hash_zip_idx_sort(const void* a, const void* b) +{ + struct rc_hash_zip_idx *A = (struct rc_hash_zip_idx*)a, *B = (struct rc_hash_zip_idx*)b; + size_t len = (A->length < B->length ? A->length : B->length); + return memcmp(A->data, B->data, len); +} + +static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) +{ + uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size; + uint32_t cdir_entry_len; + size_t sizeof_idx, indices_offset, alloc_size; + int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs; + struct rc_hash_zip_idx* hashindices, *hashindex; + + rc_file_seek(file_handle, 0, SEEK_END); + archive_size = rc_file_tell(file_handle); + + /* Basic sanity checks - reject files which are too small */ + eocdirhdr_size = 22; /* the 'end of central directory header' is 22 bytes */ + if (archive_size < eocdirhdr_size) + return rc_hash_error("ZIP is too small"); + + /* Macros used for reading ZIP and writing to a buffer for hashing (undefined again at the end of the function) */ + #define RC_ZIP_READ_LE16(p) ((uint16_t)(((const uint8_t*)(p))[0]) | ((uint16_t)(((const uint8_t*)(p))[1]) << 8U)) + #define RC_ZIP_READ_LE32(p) ((uint32_t)(((const uint8_t*)(p))[0]) | ((uint32_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint32_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint32_t)(((const uint8_t*)(p))[3]) << 24U)) + #define RC_ZIP_READ_LE64(p) ((uint64_t)(((const uint8_t*)(p))[0]) | ((uint64_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint64_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint64_t)(((const uint8_t*)(p))[3]) << 24U) | ((uint64_t)(((const uint8_t*)(p))[4]) << 32U) | ((uint64_t)(((const uint8_t*)(p))[5]) << 40U) | ((uint64_t)(((const uint8_t*)(p))[6]) << 48U) | ((uint64_t)(((const uint8_t*)(p))[7]) << 56U)) + #define RC_ZIP_WRITE_LE32(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint32_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint32_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint32_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)((uint32_t)(v) >> 24); } + #define RC_ZIP_WRITE_LE64(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint64_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint64_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint64_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)(((uint64_t)(v) >> 24) & 0xFF); ((uint8_t*)(p))[4] = (uint8_t)(((uint64_t)(v) >> 32) & 0xFF); ((uint8_t*)(p))[5] = (uint8_t)(((uint64_t)(v) >> 40) & 0xFF); ((uint8_t*)(p))[6] = (uint8_t)(((uint64_t)(v) >> 48) & 0xFF); ((uint8_t*)(p))[7] = (uint8_t)((uint64_t)(v) >> 56); } + + /* Find the end of central directory record by scanning the file from the end towards the beginning */ + for (ecdh_ofs = archive_size - sizeof(buf); ; ecdh_ofs -= (sizeof(buf) - 3)) + { + int i, n = sizeof(buf); + if (ecdh_ofs < 0) + ecdh_ofs = 0; + if (n > archive_size) + n = (int)archive_size; + rc_file_seek(file_handle, ecdh_ofs, SEEK_SET); + if (rc_file_read(file_handle, buf, n) != (size_t)n) + return rc_hash_error("ZIP read error"); + for (i = n - 4; i >= 0; --i) + if (RC_ZIP_READ_LE32(buf + i) == 0x06054b50) /* end of central directory header signature */ + break; + if (i >= 0) + { + ecdh_ofs += i; + break; + } + if (!ecdh_ofs || (archive_size - ecdh_ofs) >= (0xFFFF + eocdirhdr_size)) + return rc_hash_error("Failed to find ZIP central directory"); + } + + /* Read and verify the end of central directory record. */ + rc_file_seek(file_handle, ecdh_ofs, SEEK_SET); + if (rc_file_read(file_handle, buf, eocdirhdr_size) != eocdirhdr_size) + return rc_hash_error("Failed to read ZIP central directory"); + + /* Read central dir information from end of central directory header */ + total_files = RC_ZIP_READ_LE16(buf + 0x0A); + cdir_size = RC_ZIP_READ_LE32(buf + 0x0C); + cdir_ofs = RC_ZIP_READ_LE32(buf + 0x10); + + /* Check if this is a Zip64 file. In the block of code below: + * - 20 is the size of the ZIP64 end of central directory locator + * - 56 is the size of the ZIP64 end of central directory header + */ + if ((cdir_ofs == 0xFFFFFFFF || cdir_size == 0xFFFFFFFF || total_files == 0xFFFF) && ecdh_ofs >= (20 + 56)) + { + /* Read the ZIP64 end of central directory locator if it actually exists */ + rc_file_seek(file_handle, ecdh_ofs - 20, SEEK_SET); + if (rc_file_read(file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) /* locator signature */ + { + /* Found the locator, now read the actual ZIP64 end of central directory header */ + int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08); + if (ecdh64_ofs <= (archive_size - 56)) + { + rc_file_seek(file_handle, ecdh64_ofs, SEEK_SET); + if (rc_file_read(file_handle, buf, 56) == 56 && RC_ZIP_READ_LE32(buf) == 0x06064b50) /* header signature */ + { + total_files = RC_ZIP_READ_LE64(buf + 0x20); + cdir_size = RC_ZIP_READ_LE64(buf + 0x28); + cdir_ofs = RC_ZIP_READ_LE64(buf + 0x30); + } + } + } + } + + /* Basic verificaton of central directory (limit to a 256MB content directory) */ + cdirhdr_size = 46; /* the 'central directory header' is 46 bytes */ + if ((cdir_size >= 0x10000000) || (cdir_size < total_files * cdirhdr_size) || ((cdir_ofs + cdir_size) > archive_size)) + return rc_hash_error("Central directory of ZIP file is invalid"); + + /* Allocate once for both directory and our temporary sort index (memory aligned to sizeof(rc_hash_zip_idx)) */ + sizeof_idx = sizeof(struct rc_hash_zip_idx); + indices_offset = (size_t)((cdir_size + sizeof_idx - 1) / sizeof_idx * sizeof_idx); + alloc_size = (size_t)(indices_offset + total_files * sizeof_idx); + alloc_buf = (uint8_t*)malloc(alloc_size); + + /* Read entire central directory to a buffer */ + if (!alloc_buf) + return rc_hash_error("Could not allocate temporary buffer"); + rc_file_seek(file_handle, cdir_ofs, SEEK_SET); + if ((int64_t)rc_file_read(file_handle, alloc_buf, (int)cdir_size) != cdir_size) + { + free(alloc_buf); + return rc_hash_error("Failed to read central directory of ZIP file"); + } + + cdir_start = alloc_buf; + cdir_max = cdir_start + cdir_size - cdirhdr_size; + cdir = cdir_start; + + /* Write our temporary hash data to the same buffer we read the central directory from. + * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record. + */ + hashdata = alloc_buf; + hashindices = (struct rc_hash_zip_idx*)(alloc_buf + indices_offset); + hashindex = hashindices; + + /* Now process the central directory file records */ + for (i_file = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) + { + const uint8_t *name, *name_end; + uint32_t signature = RC_ZIP_READ_LE32(cdir + 0x00); + uint32_t method = RC_ZIP_READ_LE16(cdir + 0x0A); + uint32_t crc32 = RC_ZIP_READ_LE32(cdir + 0x10); + uint64_t comp_size = RC_ZIP_READ_LE32(cdir + 0x14); + uint64_t decomp_size = RC_ZIP_READ_LE32(cdir + 0x18); + uint32_t filename_len = RC_ZIP_READ_LE16(cdir + 0x1C); + int32_t extra_len = RC_ZIP_READ_LE16(cdir + 0x1E); + int32_t comment_len = RC_ZIP_READ_LE16(cdir + 0x20); + uint64_t local_hdr_ofs = RC_ZIP_READ_LE32(cdir + 0x2A); + cdir_entry_len = cdirhdr_size + filename_len + extra_len + comment_len; + + if (signature != 0x02014b50) /* expected central directory entry signature */ + break; + + /* Handle Zip64 fields */ + if (decomp_size == 0xFFFFFFFF || comp_size == 0xFFFFFFFF || local_hdr_ofs == 0xFFFFFFFF) + { + int invalid = 0; + const uint8_t *x = cdir + cdirhdr_size + filename_len, *xEnd, *field, *fieldEnd; + for (xEnd = x + extra_len; (x + (sizeof(uint16_t) * 2)) < xEnd; x = fieldEnd) + { + field = x + (sizeof(uint16_t) * 2); + fieldEnd = field + RC_ZIP_READ_LE16(x + 2); + if (RC_ZIP_READ_LE16(x) != 0x0001 || fieldEnd > xEnd) + continue; /* Not the Zip64 extended information extra field */ + + if (decomp_size == 0xFFFFFFFF) + { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; } + decomp_size = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + if (comp_size == 0xFFFFFFFF) + { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; } + comp_size = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + if (local_hdr_ofs == 0xFFFFFFFF) + { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; } + local_hdr_ofs = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + break; + } + if (invalid) + { + free(alloc_buf); + return rc_hash_error("Encountered invalid Zip64 file"); + } + } + + /* Basic sanity check on file record */ + /* 30 is the length of the local directory header preceeding the compressed data */ + if ((!method && decomp_size != comp_size) || (decomp_size && !comp_size) || ((local_hdr_ofs + 30 + comp_size) > (uint64_t)archive_size)) + { + free(alloc_buf); + return rc_hash_error("Encountered invalid entry in ZIP central directory"); + } + + /* Write the pointer and length of the data we record about this file */ + hashindex->data = hashdata; + hashindex->length = filename_len + 1 + 4 + 8; + hashindex++; + + /* Convert and store the file name in the hash data buffer */ + for (name = (cdir + cdirhdr_size), name_end = name + filename_len; name != name_end; name++) + { + *(hashdata++) = + (*name == '\\' ? '/' : /* convert back-slashes to regular slashes */ + (*name >= 'A' && *name <= 'Z') ? (*name | 0x20) : /* convert upper case letters to lower case */ + *name); /* else use the byte as-is */ + } + + /* Add zero terminator, CRC32 and decompressed size to the hash data buffer */ + *(hashdata++) = '\0'; + RC_ZIP_WRITE_LE32(hashdata, crc32); + hashdata += 4; + RC_ZIP_WRITE_LE64(hashdata, decomp_size); + hashdata += 8; + + if (verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "File in ZIP: %.*s (%u bytes, CRC32 = %08X)", filename_len, (const char*)(cdir + cdirhdr_size), (unsigned)decomp_size, crc32); + verbose_message_callback(message); + } + } + + if (verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "Hashing %u files in ZIP archive", (unsigned)(hashindex - hashindices)); + verbose_message_callback(message); + } + + /* Sort the file list indices */ + qsort(hashindices, (hashindex - hashindices), sizeof(struct rc_hash_zip_idx), rc_hash_zip_idx_sort); + + /* Hash the data in the order of the now sorted indices */ + for (; hashindices != hashindex; hashindices++) + md5_append(md5, hashindices->data, (int)hashindices->length); + + free(alloc_buf); + return 1; + + #undef RC_ZIP_READ_LE16 + #undef RC_ZIP_READ_LE32 + #undef RC_ZIP_READ_LE64 + #undef RC_ZIP_WRITE_LE32 + #undef RC_ZIP_WRITE_LE64 +} + +static int rc_hash_ms_dos(char hash[33], const char* path) +{ + md5_state_t md5; + size_t path_len; + int res; + + void* file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + /* hash the main content zip file first */ + md5_init(&md5); + res = rc_hash_zip_file(&md5, file_handle); + rc_file_close(file_handle); + + if (!res) + return 0; + + /* if this is a .dosz file, check if an associated .dosc file exists */ + path_len = strlen(path); + if (path[path_len-1] == 'z' || path[path_len-1] == 'Z') + { + char *dosc_path = strdup(path); + if (!dosc_path) + return rc_hash_error("Could not allocate temporary buffer"); + + /* swap the z to c and use the same capitalization, hash the file if it exists*/ + dosc_path[path_len-1] = (path[path_len-1] == 'z' ? 'c' : 'C'); + file_handle = rc_file_open(dosc_path); + free((void*)dosc_path); + if (file_handle) + { + res = rc_hash_zip_file(&md5, file_handle); + rc_file_close(file_handle); + + if (!res) + return 0; + } + } + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_arcade(char hash[33], const char* path) +{ + /* arcade hash is just the hash of the filename (no extension) - the cores are pretty stringent about having the right ROM data */ + const char* filename = rc_path_get_filename(path); + const char* ext = rc_path_get_extension(filename); + size_t filename_length = ext - filename - 1; + + /* fbneo supports loading subsystems by using specific folder names. + * if one is found, include it in the hash. + * https://github.com/libretro/FBNeo/blob/master/src/burner/libretro/README.md#emulating-consoles + */ + if (filename > path + 1) + { + int include_folder = 0; + const char* folder = filename - 1; + size_t parent_folder_length = 0; + + do + { + if (folder[-1] == '/' || folder[-1] == '\\') + break; + + --folder; + } while (folder > path); + + parent_folder_length = filename - folder - 1; + switch (parent_folder_length) + { + case 3: + if (memcmp(folder, "nes", 3) == 0 || + memcmp(folder, "fds", 3) == 0 || + memcmp(folder, "sms", 3) == 0 || + memcmp(folder, "msx", 3) == 0 || + memcmp(folder, "ngp", 3) == 0 || + memcmp(folder, "pce", 3) == 0 || + memcmp(folder, "sgx", 3) == 0) + include_folder = 1; + break; + case 4: + if (memcmp(folder, "tg16", 4) == 0) + include_folder = 1; + break; + case 6: + if (memcmp(folder, "coleco", 6) == 0 || + memcmp(folder, "sg1000", 6) == 0) + include_folder = 1; + break; + case 8: + if (memcmp(folder, "gamegear", 8) == 0 || + memcmp(folder, "megadriv", 8) == 0 || + memcmp(folder, "spectrum", 8) == 0) + include_folder = 1; + break; + default: + break; + } + + if (include_folder) + { + char buffer[128]; /* realistically, this should never need more than ~20 characters */ + if (parent_folder_length + filename_length + 1 < sizeof(buffer)) + { + memcpy(&buffer[0], folder, parent_folder_length); + buffer[parent_folder_length] = '_'; + memcpy(&buffer[parent_folder_length + 1], filename, filename_length); + return rc_hash_buffer(hash, (uint8_t*)&buffer[0], parent_folder_length + filename_length + 1); + } + } + } + + return rc_hash_buffer(hash, (uint8_t*)filename, filename_length); +} + +static int rc_hash_text(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + md5_state_t md5; + const uint8_t* scan = buffer; + const uint8_t* stop = buffer + buffer_size; + + md5_init(&md5); + + do { + /* find end of line */ + while (scan < stop && *scan != '\r' && *scan != '\n') + ++scan; + + md5_append(&md5, buffer, (int)(scan - buffer)); + + /* include a normalized line ending */ + /* NOTE: this causes a line ending to be hashed at the end of the file, even if one was not present */ + md5_append(&md5, (const uint8_t*)"\n", 1); + + /* skip newline */ + if (scan < stop && *scan == '\r') + ++scan; + if (scan < stop && *scan == '\n') + ++scan; + + buffer = scan; + } while (scan < stop); + + return rc_hash_finalize(&md5, hash); +} + +/* helper variable only used for testing */ +const char* _rc_hash_jaguar_cd_homebrew_hash = NULL; + +static int rc_hash_jaguar_cd(char hash[33], const char* path) +{ + uint8_t buffer[2352]; + char message[128]; + void* track_handle; + md5_state_t md5; + int byteswapped = 0; + 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 */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* The header is an unspecified distance into the first sector, but usually two bytes in. + * It consists of 64 bytes of "TAIR" or "ATRI" repeating, depending on whether or not the data + * is byteswapped. Then another 32 byte that reads "ATARI APPROVED DATA HEADER ATRI " + * (possibly byteswapped). Then a big-endian 32-bit value for the address where the boot code + * should be loaded, and a second big-endian 32-bit value for the size of the boot code. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + + for (i = 64; i < sizeof(buffer) - 32 - 4 * 3; i++) + { + if (memcmp(&buffer[i], "TARA IPARPVODED TA AEHDAREA RT I", 32) == 0) + { + byteswapped = 1; + offset = i + 32 + 4; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + break; + } + else if (memcmp(&buffer[i], "ATARI APPROVED DATA HEADER ATRI ", 32) == 0) + { + byteswapped = 0; + offset = i + 32 + 4; + size = (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 3]); + break; + } + } + + if (size == 0) /* did not see ATARI APPROVED DATA HEADER */ + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Jaguar CD"); + } + + i = 0; /* only loop once */ + do + { + md5_init(&md5); + + offset += 4; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Hashing boot executable (%u bytes starting at %u bytes into sector %u)", size, offset, sector); + rc_hash_verbose(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + do + { + if (byteswapped) + rc_hash_byteswap16(buffer, &buffer[sizeof(buffer)]); + + remaining = sizeof(buffer) - offset; + if (remaining >= size) + { + md5_append(&md5, &buffer[offset], size); + size = 0; + break; + } + + md5_append(&md5, &buffer[offset], remaining); + size -= remaining; + offset = 0; + } while (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer)) == sizeof(buffer)); + + rc_cd_close_track(track_handle); + + if (size > 0) + return rc_hash_error("Not enough data"); + + rc_hash_finalize(&md5, hash); + + /* homebrew games all seem to have the same boot executable and store the actual game code in track 2. + * if we generated something other than the homebrew hash, return it. assume all homebrews are byteswapped. */ + if (strcmp(hash, "254487b59ab21bc005338e85cbf9fd2f") != 0 || !byteswapped) { + if (_rc_hash_jaguar_cd_homebrew_hash == NULL || strcmp(hash, _rc_hash_jaguar_cd_homebrew_hash) != 0) + return 1; + } + + /* if we've already been through the loop a second time, just return the hash */ + if (i == 1) + return 1; + ++i; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Potential homebrew at sector %u, checking for KART data in track 2", sector); + rc_hash_verbose(message); + } + + track_handle = rc_cd_open_track(path, 2); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* track 2 of the homebrew code has the 64 bytes or ATRI followed by 32 bytes of "ATARI APPROVED DATA HEADER ATRI!", + * then 64 bytes of KART repeating. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + if (memcmp(&buffer[0x5E], "RT!IRTKA", 8) != 0) + return rc_hash_error("Homebrew executable not found in track 2"); + + /* found KART data*/ + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Found KART data in track 2"); + rc_hash_verbose(message); + } + + offset = 0xA6; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + } while (1); +} + +static int rc_hash_lynx(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + if (buffer[0] == 'L' && buffer[1] == 'Y' && buffer[2] == 'N' && buffer[3] == 'X' && buffer[4] == 0) + { + rc_hash_verbose("Ignoring LYNX header"); + + buffer += 64; + buffer_size -= 64; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_neogeo_cd(char hash[33], const char* path) +{ + char buffer[1024], *ptr; + void* track_handle; + uint32_t sector; + uint32_t size; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* https://wiki.neogeodev.org/index.php?title=IPL_file, https://wiki.neogeodev.org/index.php?title=PRG_file + * IPL file specifies data to be loaded before the game starts. PRG files are the executable code + */ + sector = rc_cd_find_file_sector(track_handle, "IPL.TXT", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a NeoGeo CD game disc"); + } + + if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) == 0) + { + rc_cd_close_track(track_handle); + return 0; + } + + md5_init(&md5); + + buffer[sizeof(buffer) - 1] = '\0'; + ptr = &buffer[0]; + do + { + char* start = ptr; + while (*ptr && *ptr != '.') + ++ptr; + + if (strncasecmp(ptr, ".PRG", 4) == 0) + { + ptr += 4; + *ptr++ = '\0'; + + sector = rc_cd_find_file_sector(track_handle, start, &size); + if (!sector || !rc_hash_cd_file(&md5, track_handle, sector, NULL, size, start)) + { + char error[128]; + rc_cd_close_track(track_handle); + snprintf(error, sizeof(error), "Could not read %.16s", start); + return rc_hash_error(error); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr != '\n') + break; + ++ptr; + } while (*ptr != '\0' && *ptr != '\x1a'); + + rc_cd_close_track(track_handle); + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_nes(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + if (buffer[0] == 'N' && buffer[1] == 'E' && buffer[2] == 'S' && buffer[3] == 0x1A) + { + rc_hash_verbose("Ignoring NES header"); + + buffer += 16; + buffer_size -= 16; + } + else if (buffer[0] == 'F' && buffer[1] == 'D' && buffer[2] == 'S' && buffer[3] == 0x1A) + { + rc_hash_verbose("Ignoring FDS header"); + + buffer += 16; + buffer_size -= 16; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_n64(char hash[33], const char* path) +{ + uint8_t* buffer; + uint8_t* stop; + const size_t buffer_size = 65536; + md5_state_t md5; + size_t remaining; + void* file_handle; + int is_v64 = 0; + int is_n64 = 0; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + buffer = (uint8_t*)malloc(buffer_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + stop = buffer + buffer_size; + + /* read first byte so we can detect endianness */ + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, 1); + + if (buffer[0] == 0x80) /* z64 format (big endian [native]) */ + { + } + else if (buffer[0] == 0x37) /* v64 format (byteswapped) */ + { + rc_hash_verbose("converting v64 to z64"); + is_v64 = 1; + } + else if (buffer[0] == 0x40) /* n64 format (little endian) */ + { + rc_hash_verbose("converting n64 to z64"); + is_n64 = 1; + } + else if (buffer[0] == 0xE8 || buffer[0] == 0x22) /* ndd format (don't byteswap) */ + { + } + else + { + free(buffer); + rc_file_close(file_handle); + + rc_hash_verbose("Not a Nintendo 64 ROM"); + return 0; + } + + /* calculate total file size */ + rc_file_seek(file_handle, 0, SEEK_END); + remaining = (size_t)rc_file_tell(file_handle); + if (remaining > MAX_BUFFER_SIZE) + remaining = MAX_BUFFER_SIZE; + + if (verbose_message_callback) + { + char message[64]; + snprintf(message, sizeof(message), "Hashing %u bytes", (unsigned)remaining); + verbose_message_callback(message); + } + + /* begin hashing */ + md5_init(&md5); + + rc_file_seek(file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) + { + rc_file_read(file_handle, buffer, (int)buffer_size); + + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) + { + rc_file_read(file_handle, buffer, (int)remaining); + + stop = buffer + remaining; + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)remaining); + } + + /* cleanup */ + rc_file_close(file_handle); + free(buffer); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_nintendo_ds(char hash[33], const char* path) +{ + uint8_t header[512]; + uint8_t* hash_buffer; + 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; + void* file_handle; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_SET); + if (rc_file_read(file_handle, header, sizeof(header)) != 512) + return rc_hash_error("Failed to read header"); + + if (header[0] == 0x2E && header[1] == 0x00 && header[2] == 0x00 && header[3] == 0xEA && + header[0xB0] == 0x44 && header[0xB1] == 0x46 && header[0xB2] == 0x96 && header[0xB3] == 0) + { + /* SuperCard header detected, ignore it */ + rc_hash_verbose("Ignoring SuperCard header"); + + offset = 512; + rc_file_seek(file_handle, offset, SEEK_SET); + rc_file_read(file_handle, header, sizeof(header)); + } + + arm9_addr = header[0x20] | (header[0x21] << 8) | (header[0x22] << 16) | (header[0x23] << 24); + arm9_size = header[0x2C] | (header[0x2D] << 8) | (header[0x2E] << 16) | (header[0x2F] << 24); + arm7_addr = header[0x30] | (header[0x31] << 8) | (header[0x32] << 16) | (header[0x33] << 24); + arm7_size = header[0x3C] | (header[0x3D] << 8) | (header[0x3E] << 16) | (header[0x3F] << 24); + icon_addr = header[0x68] | (header[0x69] << 8) | (header[0x6A] << 16) | (header[0x6B] << 24); + + if (arm9_size + arm7_size > 16 * 1024 * 1024) + { + /* sanity check - code blocks are typically less than 1MB each - assume not a DS ROM */ + snprintf((char*)header, sizeof(header), "arm9 code size (%u) + arm7 code size (%u) exceeds 16MB", arm9_size, arm7_size); + return rc_hash_error((const char*)header); + } + + hash_size = 0xA00; + if (arm9_size > hash_size) + hash_size = arm9_size; + if (arm7_size > hash_size) + hash_size = arm7_size; + + hash_buffer = (uint8_t*)malloc(hash_size); + if (!hash_buffer) + { + rc_file_close(file_handle); + + snprintf((char*)header, sizeof(header), "Failed to allocate %u bytes", hash_size); + return rc_hash_error((const char*)header); + } + + md5_init(&md5); + + rc_hash_verbose("Hashing 352 byte header"); + md5_append(&md5, header, 0x160); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Hashing %u byte arm9 code (at %08X)", arm9_size, arm9_addr); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, arm9_addr + offset, SEEK_SET); + rc_file_read(file_handle, hash_buffer, arm9_size); + md5_append(&md5, hash_buffer, arm9_size); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Hashing %u byte arm7 code (at %08X)", arm7_size, arm7_addr); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, arm7_addr + offset, SEEK_SET); + rc_file_read(file_handle, hash_buffer, arm7_size); + md5_append(&md5, hash_buffer, arm7_size); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Hashing 2560 byte icon and labels data (at %08X)", icon_addr); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, icon_addr + offset, SEEK_SET); + num_read = rc_file_read(file_handle, hash_buffer, 0xA00); + if (num_read < 0xA00) + { + /* some homebrew games don't provide a full icon block, and no data after the icon block. + * if we didn't get a full icon block, fill the remaining portion with 0s + */ + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Warning: only got %u bytes for icon and labels data, 0-padding to 2560 bytes", (unsigned)num_read); + verbose_message_callback((const char*)header); + } + + memset(&hash_buffer[num_read], 0, 0xA00 - num_read); + } + md5_append(&md5, hash_buffer, 0xA00); + + free(hash_buffer); + rc_file_close(file_handle); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_gamecube(char hash[33], const char* path) +{ + md5_state_t md5; + void* file_handle; + const uint32_t BASE_HEADER_SIZE = 0x2440; + const uint32_t MAX_HEADER_SIZE = 1024 * 1024; + uint32_t apploader_header_size, apploader_body_size, apploader_trailer_size, header_size; + uint8_t quad_buffer[4]; + uint8_t addr_buffer[0xD8]; + uint8_t* buffer; + uint32_t dol_offset; + uint32_t dol_offsets[18]; + uint32_t dol_sizes[18]; + uint32_t dol_buf_size = 0; + 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); + rc_file_read(file_handle, quad_buffer, 4); + if (quad_buffer[0] != 0xC2|| quad_buffer[1] != 0x33 || quad_buffer[2] != 0x9F || quad_buffer[3] != 0x3D) + { + rc_file_close(file_handle); + return rc_hash_error("Not a Gamecube disc"); + } + + /* GetApploaderSize */ + rc_file_seek(file_handle, BASE_HEADER_SIZE + 0x14, SEEK_SET); + apploader_header_size = 0x20; + rc_file_read(file_handle, quad_buffer, 4); + apploader_body_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(file_handle, quad_buffer, 4); + apploader_trailer_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + header_size = BASE_HEADER_SIZE + apploader_header_size + apploader_body_size + apploader_trailer_size; + if (header_size > MAX_HEADER_SIZE) header_size = MAX_HEADER_SIZE; + + /* Hash headers */ + buffer = (uint8_t*)malloc(header_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, header_size); + md5_init(&md5); + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %u byte header", header_size); + verbose_message_callback(message); + } + md5_append(&md5, buffer, header_size); + + /* GetBootDOLOffset + * Base header size is guaranteed larger than 0x423 therefore buffer contains dol_offset right now + */ + dol_offset = (buffer[0x420] << 24) | (buffer[0x421] << 16) | (buffer[0x422] << 8) | buffer[0x423]; + free(buffer); + + /* Find offsetsand sizes for the 7 main.dol code segments and 11 main.dol data segments */ + rc_file_seek(file_handle, dol_offset, SEEK_SET); + rc_file_read(file_handle, addr_buffer, 0xD8); + for (ix = 0; ix < 18; ix++) + { + dol_offsets[ix] = + (addr_buffer[0x0 + ix * 4] << 24) | + (addr_buffer[0x1 + ix * 4] << 16) | + (addr_buffer[0x2 + ix * 4] << 8) | + addr_buffer[0x3 + ix * 4]; + dol_sizes[ix] = + (addr_buffer[0x90 + ix * 4] << 24) | + (addr_buffer[0x91 + ix * 4] << 16) | + (addr_buffer[0x92 + ix * 4] << 8) | + addr_buffer[0x93 + ix * 4]; + dol_buf_size = (dol_sizes[ix] > dol_buf_size) ? dol_sizes[ix] : dol_buf_size; + } + + /* Iterate through the 18 main.dol segments and hash each */ + buffer = (uint8_t*)malloc(dol_buf_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + for (ix = 0; ix < 18; ix++) + { + if (dol_sizes[ix] == 0) + continue; + rc_file_seek(file_handle, dol_offsets[ix], SEEK_SET); + rc_file_read(file_handle, buffer, dol_sizes[ix]); + if (verbose_message_callback) + { + char message[128]; + if (ix < 7) + snprintf(message, sizeof(message), "Hashing %u byte main.dol code segment %u", dol_sizes[ix], ix); + else + snprintf(message, sizeof(message), "Hashing %u byte main.dol data segment %u", dol_sizes[ix], ix - 7); + verbose_message_callback(message); + } + md5_append(&md5, buffer, dol_sizes[ix]); + } + + /* Finalize */ + rc_file_close(file_handle); + free(buffer); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_pce(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it (expect ROM data to be multiple of 128KB) */ + uint32_t calc_size = ((uint32_t)buffer_size / 0x20000) * 0x20000; + if (buffer_size - calc_size == 512) + { + rc_hash_verbose("Ignoring PCE header"); + + buffer += 512; + buffer_size -= 512; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_pce_track(char hash[33], void* track_handle) +{ + uint8_t buffer[2048]; + md5_state_t md5; + 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 + * http://shu.sheldows.com/shu/download/pcedocs/pce_cdrom.html + */ + if (rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 1, buffer, 128) < 128) + { + return rc_hash_error("Not a PC Engine CD"); + } + + /* normal PC Engine CD will have a header block in sector 1 */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) + { + /* the title of the disc is the last 22 bytes of the header */ + md5_init(&md5); + md5_append(&md5, &buffer[106], 22); + + if (verbose_message_callback) + { + char message[128]; + buffer[128] = '\0'; + snprintf(message, sizeof(message), "Found PC Engine CD, title=%.22s", &buffer[106]); + verbose_message_callback(message); + } + + /* the first three bytes specify the sector of the program data, and the fourth byte + * is the number of sectors. + */ + sector = (buffer[0] << 16) + (buffer[1] << 8) + buffer[2]; + num_sectors = buffer[3]; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %d sectors starting at sector %d", num_sectors, sector); + verbose_message_callback(message); + } + + sector += rc_cd_first_track_sector(track_handle); + while (num_sectors > 0) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + /* GameExpress CDs use a standard Joliet filesystem - locate and hash the BOOT.BIN */ + else if ((sector = rc_cd_find_file_sector(track_handle, "BOOT.BIN", &size)) != 0 && size < MAX_BUFFER_SIZE) + { + md5_init(&md5); + while (size > sizeof(buffer)) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= sizeof(buffer); + } + + if (size > 0) + { + rc_cd_read_sector(track_handle, sector, buffer, size); + md5_append(&md5, buffer, size); + } + } + else + { + return rc_hash_error("Not a PC Engine CD"); + } + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_pce_cd(char hash[33], const char* path) +{ + int result; + void* track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_error("Could not open track"); + + result = rc_hash_pce_track(hash, track_handle); + + rc_cd_close_track(track_handle); + + return result; +} + +static int rc_hash_pcfx_cd(char hash[33], const char* path) +{ + uint8_t buffer[2048]; + void* track_handle; + md5_state_t md5; + int sector, num_sectors; + + /* PC-FX executable can be in any track. Assume it's in the largest data track and check there first */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LARGEST); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* PC-FX CD will have a header marker in sector 0 */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, 32); + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) != 0) + { + rc_cd_close_track(track_handle); + + /* not found in the largest data track, check track 2 */ + track_handle = rc_cd_open_track(path, 2); + if (!track_handle) + return rc_hash_error("Could not open track"); + + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, 32); + } + + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) == 0) + { + /* PC-FX boot header fills the first two sectors of the disc + * https://bitbucket.org/trap15/pcfxtools/src/master/pcfx-cdlink.c + * the important stuff is the first 128 bytes of the second sector (title being the first 32) */ + rc_cd_read_sector(track_handle, sector + 1, buffer, 128); + + md5_init(&md5); + md5_append(&md5, buffer, 128); + + if (verbose_message_callback) + { + char message[128]; + buffer[128] = '\0'; + snprintf(message, sizeof(message), "Found PC-FX CD, title=%.32s", &buffer[0]); + verbose_message_callback(message); + } + + /* the program sector is in bytes 33-36 (assume byte 36 is 0) */ + sector = (buffer[34] << 16) + (buffer[33] << 8) + buffer[32]; + + /* the number of sectors the program occupies is in bytes 37-40 (assume byte 40 is 0) */ + num_sectors = (buffer[38] << 16) + (buffer[37] << 8) + buffer[36]; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %d sectors starting at sector %d", num_sectors, sector); + verbose_message_callback(message); + } + + sector += rc_cd_first_track_sector(track_handle); + while (num_sectors > 0) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + else + { + int result = 0; + rc_cd_read_sector(track_handle, sector + 1, buffer, 128); + + /* some PC-FX CDs still identify as PCE CDs */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) + result = rc_hash_pce_track(hash, track_handle); + + rc_cd_close_track(track_handle); + if (result) + return result; + + return rc_hash_error("Not a PC-FX CD"); + } + + rc_cd_close_track(track_handle); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_dreamcast(char hash[33], const char* path) +{ + uint8_t buffer[256] = ""; + void* track_handle; + char exe_file[32] = ""; + uint32_t size; + uint32_t sector; + int result = 0; + md5_state_t md5; + int i = 0; + + /* track 03 is the data track that contains the TOC and IP.BIN */ + track_handle = rc_cd_open_track(path, 3); + if (track_handle) + { + /* first 256 bytes from first sector should have IP.BIN structure that stores game meta information + * https://mc.pp.se/dc/ip.bin.html */ + rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle), buffer, sizeof(buffer)); + } + + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) + { + if (track_handle) + rc_cd_close_track(track_handle); + + /* not a gd-rom dreamcast file. check for mil-cd by looking for the marker in the first data track */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_error("Could not open track"); + + rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle), buffer, sizeof(buffer)); + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) + { + /* did not find marker on track 3 or first data track */ + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Dreamcast CD"); + } + } + + /* start the hash with the game meta information */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)buffer, 256); + + if (verbose_message_callback) + { + char message[256]; + uint8_t* ptr = &buffer[0xFF]; + while (ptr > &buffer[0x80] && ptr[-1] == ' ') + --ptr; + *ptr = '\0'; + + snprintf(message, sizeof(message), "Found Dreamcast CD: %.128s (%.16s)", (const char*)&buffer[0x80], (const char*)&buffer[0x40]); + verbose_message_callback(message); + } + + /* the boot filename is 96 bytes into the meta information (https://mc.pp.se/dc/ip0000.bin.html) */ + /* remove whitespace from bootfile */ + i = 0; + while (!isspace((unsigned char)buffer[96 + i]) && i < 16) + ++i; + + /* sometimes boot file isn't present on meta information. + * nothing can be done, as even the core doesn't run the game in this case. */ + if (i == 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Boot executable not specified on IP.BIN"); + } + + memcpy(exe_file, &buffer[96], i); + exe_file[i] = '\0'; + + sector = rc_cd_find_file_sector(track_handle, exe_file, &size); + if (sector == 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Could not locate boot executable"); + } + + if (rc_cd_read_sector(track_handle, sector, buffer, 1)) + { + /* the boot executable is in the primary data track */ + } + else + { + rc_cd_close_track(track_handle); + + /* the boot executable is normally in the last track */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LAST); + } + + result = rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "boot executable"); + rc_cd_close_track(track_handle); + + rc_hash_finalize(&md5, hash); + return result; +} + +static int rc_hash_find_playstation_executable(void* track_handle, const char* boot_key, const char* cdrom_prefix, + char exe_name[], uint32_t exe_name_size, uint32_t* exe_size) +{ + uint8_t buffer[2048]; + uint32_t size; + char* ptr; + char* start; + const size_t boot_key_len = strlen(boot_key); + const size_t cdrom_prefix_len = strlen(cdrom_prefix); + int sector; + + sector = rc_cd_find_file_sector(track_handle, "SYSTEM.CNF", NULL); + if (!sector) + return 0; + + size = (uint32_t)rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer) - 1); + buffer[size] = '\0'; + + sector = 0; + for (ptr = (char*)buffer; *ptr; ++ptr) + { + if (strncmp(ptr, boot_key, boot_key_len) == 0) + { + ptr += boot_key_len; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (*ptr == '=') + { + ++ptr; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) + ptr += cdrom_prefix_len; + while (*ptr == '\\') + ++ptr; + + start = ptr; + while (!isspace((unsigned char)*ptr) && *ptr != ';') + ++ptr; + + size = (uint32_t)(ptr - start); + if (size >= exe_name_size) + size = exe_name_size - 1; + + memcpy(exe_name, start, size); + exe_name[size] = '\0'; + + if (verbose_message_callback) + { + snprintf((char*)buffer, sizeof(buffer), "Looking for boot executable: %s", exe_name); + verbose_message_callback((const char*)buffer); + } + + sector = rc_cd_find_file_sector(track_handle, exe_name, exe_size); + break; + } + } + + /* advance to end of line */ + while (*ptr && *ptr != '\n') + ++ptr; + } + + return sector; +} + +static int rc_hash_psx(char hash[33], const char* path) +{ + uint8_t buffer[32]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + uint32_t size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + sector = rc_hash_find_playstation_executable(track_handle, "BOOT", "cdrom:", exe_name, sizeof(exe_name), &size); + if (!sector) + { + sector = rc_cd_find_file_sector(track_handle, "PSX.EXE", &size); + if (sector) + memcpy(exe_name, "PSX.EXE", 8); + } + + if (!sector) + { + rc_hash_error("Could not locate primary executable"); + } + else if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) + { + rc_hash_error("Could not read primary executable"); + } + else + { + if (memcmp(buffer, "PS-X EXE", 7) != 0) + { + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "%s did not contain PS-X EXE marker", exe_name); + verbose_message_callback(message); + } + } + else + { + /* the PS-X EXE header specifies the executable size as a 4-byte value 28 bytes into the header, which doesn't + * include the header itself. We want to include the header in the hash, so append another 2048 to that value. + */ + size = (((uint8_t)buffer[31] << 24) | ((uint8_t)buffer[30] << 16) | ((uint8_t)buffer[29] << 8) | (uint8_t)buffer[28]) + 2048; + } + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(&md5, hash); + } + + rc_cd_close_track(track_handle); + + return result; +} + +static int rc_hash_ps2(char hash[33], const char* path) +{ + uint8_t buffer[4]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + uint32_t size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + sector = rc_hash_find_playstation_executable(track_handle, "BOOT2", "cdrom0:", exe_name, sizeof(exe_name), &size); + if (!sector) + { + rc_hash_error("Could not locate primary executable"); + } + else if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) + { + rc_hash_error("Could not read primary executable"); + } + else + { + if (memcmp(buffer, "\x7f\x45\x4c\x46", 4) != 0) + { + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "%s did not contain ELF marker", exe_name); + verbose_message_callback(message); + } + } + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(&md5, hash); + } + + rc_cd_close_track(track_handle); + + return result; +} + +static int rc_hash_psp(char hash[33], const char* path) +{ + void* track_handle; + uint32_t sector; + uint32_t size; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* http://www.romhacking.net/forum/index.php?topic=30899.0 + * PSP_GAME/PARAM.SFO contains key/value pairs identifying the game for the system (i.e. serial number, + * name, version). PSP_GAME/SYSDIR/EBOOT.BIN is the encrypted primary executable. + */ + sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\PARAM.SFO", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a PSP game disc"); + } + + md5_init(&md5); + if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\PARAM.SFO")) + { + rc_cd_close_track(track_handle); + return 0; + } + + sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\SYSDIR\\EBOOT.BIN", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Could not find primary executable"); + } + + if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\SYSDIR\\EBOOT.BIN")) + { + rc_cd_close_track(track_handle); + return 0; + } + + rc_cd_close_track(track_handle); + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_sega_cd(char hash[33], const char* path) +{ + uint8_t buffer[512]; + void* track_handle; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* the first 512 bytes of sector 0 are a volume header and ROM header that uniquely identify the game. + * After that is an arbitrary amount of code that ensures the game is being run in the correct region. + * Then more arbitrary code follows that actually starts the boot process. Somewhere in there, the + * primary executable is loaded. In many cases, a single game will have multiple executables, so even + * if we could determine the primary one, it's just the tip of the iceberg. As such, we've decided that + * hashing the volume and ROM headers is sufficient for identifying the game, and we'll have to trust + * that our players aren't modifying anything else on the disc. + */ + rc_cd_read_sector(track_handle, 0, buffer, sizeof(buffer)); + rc_cd_close_track(track_handle); + + if (memcmp(buffer, "SEGADISCSYSTEM ", 16) != 0 && /* Sega CD */ + memcmp(buffer, "SEGA SEGASATURN ", 16) != 0) /* Sega Saturn */ + { + return rc_hash_error("Not a Sega CD"); + } + + 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 */ + uint32_t calc_size = ((uint32_t)buffer_size / 0x2000) * 0x2000; + if (buffer_size - calc_size == 512) + { + rc_hash_verbose("Ignoring SNES header"); + + buffer += 512; + buffer_size -= 512; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +struct rc_buffered_file +{ + const uint8_t* read_ptr; + const uint8_t* data; + size_t data_size; +}; + +static struct rc_buffered_file rc_buffered_file; + +static void* rc_file_open_buffered_file(const char* path) +{ + struct rc_buffered_file* handle = (struct rc_buffered_file*)malloc(sizeof(struct rc_buffered_file)); + (void)path; + + if (handle) + memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + + return handle; +} + +void rc_file_seek_buffered_file(void* file_handle, int64_t offset, int origin) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + switch (origin) + { + case SEEK_SET: buffered_file->read_ptr = buffered_file->data + offset; break; + case SEEK_CUR: buffered_file->read_ptr += offset; break; + case SEEK_END: buffered_file->read_ptr = buffered_file->data + buffered_file->data_size + offset; break; + } + + if (buffered_file->read_ptr < buffered_file->data) + buffered_file->read_ptr = buffered_file->data; + else if (buffered_file->read_ptr > buffered_file->data + buffered_file->data_size) + buffered_file->read_ptr = buffered_file->data + buffered_file->data_size; +} + +int64_t rc_file_tell_buffered_file(void* file_handle) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + return (buffered_file->read_ptr - buffered_file->data); +} + +size_t rc_file_read_buffered_file(void* file_handle, void* buffer, size_t requested_bytes) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + const int64_t remaining = buffered_file->data_size - (buffered_file->read_ptr - buffered_file->data); + if ((int)requested_bytes > remaining) + requested_bytes = (int)remaining; + + memcpy(buffer, buffered_file->read_ptr, requested_bytes); + buffered_file->read_ptr += requested_bytes; + return requested_bytes; +} + +void rc_file_close_buffered_file(void* file_handle) +{ + free(file_handle); +} + +static int rc_hash_file_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size) +{ + struct rc_hash_filereader buffered_filereader_funcs; + struct rc_hash_filereader* old_filereader = filereader; + int result; + + memset(&buffered_filereader_funcs, 0, sizeof(buffered_filereader_funcs)); + buffered_filereader_funcs.open = rc_file_open_buffered_file; + buffered_filereader_funcs.close = rc_file_close_buffered_file; + buffered_filereader_funcs.read = rc_file_read_buffered_file; + buffered_filereader_funcs.seek = rc_file_seek_buffered_file; + buffered_filereader_funcs.tell = rc_file_tell_buffered_file; + filereader = &buffered_filereader_funcs; + + rc_buffered_file.data = rc_buffered_file.read_ptr = buffer; + rc_buffered_file.data_size = buffer_size; + + result = rc_hash_generate_from_file(hash, console_id, "[buffered file]"); + + filereader = old_filereader; + return result; +} + +int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size) +{ + switch (console_id) + { + default: + { + char message[128]; + snprintf(message, sizeof(message), "Unsupported console for buffer hash: %d", console_id); + return rc_hash_error(message); + } + + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_ARCADIA_2001: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_ADVANCE: + case RC_CONSOLE_GAMEBOY_COLOR: + case RC_CONSOLE_GAME_GEAR: + case RC_CONSOLE_INTELLIVISION: + case RC_CONSOLE_INTERTON_VC_4000: + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_MSX: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_PC8800: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: + case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: + case RC_CONSOLE_WONDERSWAN: + return rc_hash_buffer(hash, buffer, buffer_size); + + case RC_CONSOLE_ARDUBOY: + /* https://en.wikipedia.org/wiki/Intel_HEX */ + return rc_hash_text(hash, buffer, buffer_size); + + case RC_CONSOLE_ATARI_7800: + return rc_hash_7800(hash, buffer, buffer_size); + + case RC_CONSOLE_ATARI_LYNX: + return rc_hash_lynx(hash, buffer, buffer_size); + + case RC_CONSOLE_NINTENDO: + return rc_hash_nes(hash, buffer, buffer_size); + + 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); + + case RC_CONSOLE_NINTENDO_64: + case RC_CONSOLE_NINTENDO_3DS: + case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: + return rc_hash_file_from_buffer(hash, console_id, buffer, buffer_size); + } +} + +static int rc_hash_whole_file(char hash[33], const char* path) +{ + md5_state_t md5; + uint8_t* buffer; + int64_t size; + const size_t buffer_size = 65536; + void* file_handle; + size_t remaining; + int result = 0; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_END); + size = rc_file_tell(file_handle); + + if (verbose_message_callback) + { + char message[1024]; + if (size > MAX_BUFFER_SIZE) + snprintf(message, sizeof(message), "Hashing first %u bytes (of %u bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(path)); + else + snprintf(message, sizeof(message), "Hashing %s (%u bytes)", rc_path_get_filename(path), (unsigned)size); + verbose_message_callback(message); + } + + if (size > MAX_BUFFER_SIZE) + remaining = MAX_BUFFER_SIZE; + else + remaining = (size_t)size; + + md5_init(&md5); + + buffer = (uint8_t*)malloc(buffer_size); + if (buffer) + { + rc_file_seek(file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) + { + rc_file_read(file_handle, buffer, (int)buffer_size); + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) + { + rc_file_read(file_handle, buffer, (int)remaining); + md5_append(&md5, buffer, (int)remaining); + } + + free(buffer); + result = rc_hash_finalize(&md5, hash); + } + + rc_file_close(file_handle); + return result; +} + +static int rc_hash_buffered_file(char hash[33], uint32_t console_id, const char* path) +{ + uint8_t* buffer; + int64_t size; + int result = 0; + void* file_handle; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_END); + size = rc_file_tell(file_handle); + + if (verbose_message_callback) + { + char message[1024]; + if (size > MAX_BUFFER_SIZE) + snprintf(message, sizeof(message), "Buffering first %u bytes (of %d bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(path)); + else + snprintf(message, sizeof(message), "Buffering %s (%d bytes)", rc_path_get_filename(path), (unsigned)size); + verbose_message_callback(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + buffer = (uint8_t*)malloc((size_t)size); + if (buffer) + { + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, (int)size); + + result = rc_hash_generate_from_buffer(hash, console_id, buffer, (size_t)size); + + free(buffer); + } + + rc_file_close(file_handle); + return result; +} + +static int rc_hash_path_is_absolute(const char* path) +{ + if (!path[0]) + return 0; + + /* "/path/to/file" or "\path\to\file" */ + if (path[0] == '/' || path[0] == '\\') + return 1; + + /* "C:\path\to\file" */ + if (path[1] == ':' && path[2] == '\\') + return 1; + + /* "scheme:/path/to/file" */ + while (*path) + { + if (path[0] == ':' && path[1] == '/') + return 1; + + ++path; + } + + return 0; +} + +static const char* rc_hash_get_first_item_from_playlist(const char* path) +{ + char buffer[1024]; + char* disc_path; + char* ptr, *start, *next; + size_t num_read, path_len, file_len; + void* file_handle; + + file_handle = rc_file_open(path); + if (!file_handle) + { + rc_hash_error("Could not open playlist"); + return NULL; + } + + num_read = rc_file_read(file_handle, buffer, sizeof(buffer) - 1); + buffer[num_read] = '\0'; + + rc_file_close(file_handle); + + ptr = start = buffer; + do + { + /* ignore empty and commented lines */ + while (*ptr == '#' || *ptr == '\r' || *ptr == '\n') + { + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr) + ++ptr; + } + + /* find and extract the current line */ + start = ptr; + while (*ptr && *ptr != '\n') + ++ptr; + next = ptr; + + /* remove trailing whitespace - especially '\r' */ + while (ptr > start && isspace((unsigned char)ptr[-1])) + --ptr; + + /* if we found a non-empty line, break out of the loop to process it */ + file_len = ptr - start; + if (file_len) + break; + + /* did we reach the end of the file? */ + if (!*next) + return NULL; + + /* if the line only contained whitespace, keep searching */ + ptr = next + 1; + } while (1); + + if (verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "Extracted %.*s from playlist", (int)file_len, start); + verbose_message_callback(message); + } + + start[file_len++] = '\0'; + if (rc_hash_path_is_absolute(start)) + path_len = 0; + else + path_len = rc_path_get_filename(path) - path; + + disc_path = (char*)malloc(path_len + file_len + 1); + if (!disc_path) + return NULL; + + if (path_len) + memcpy(disc_path, path, path_len); + + memcpy(&disc_path[path_len], start, file_len); + return disc_path; +} + +static int rc_hash_generate_from_playlist(char hash[33], uint32_t console_id, const char* path) +{ + int result; + const char* disc_path; + + if (verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "Processing playlist: %s", rc_path_get_filename(path)); + verbose_message_callback(message); + } + + disc_path = rc_hash_get_first_item_from_playlist(path); + if (!disc_path) + return rc_hash_error("Failed to get first item from playlist"); + + result = rc_hash_generate_from_file(hash, console_id, disc_path); + + free((void*)disc_path); + return result; +} + +int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path) +{ + switch (console_id) + { + default: + { + char buffer[128]; + snprintf(buffer, sizeof(buffer), "Unsupported console for file hash: %d", console_id); + return rc_hash_error(buffer); + } + + case RC_CONSOLE_ARCADIA_2001: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_ADVANCE: + case RC_CONSOLE_GAMEBOY_COLOR: + case RC_CONSOLE_GAME_GEAR: + case RC_CONSOLE_INTELLIVISION: + case RC_CONSOLE_INTERTON_VC_4000: + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: + case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: + case RC_CONSOLE_WONDERSWAN: + /* generic whole-file hash - don't buffer */ + return rc_hash_whole_file(hash, path); + + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_MSX: + case RC_CONSOLE_PC8800: + /* generic whole-file hash with m3u support - don't buffer */ + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_whole_file(hash, path); + + case RC_CONSOLE_ARDUBOY: + case RC_CONSOLE_ATARI_7800: + 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); + + case RC_CONSOLE_3DO: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_3do(hash, path); + + case RC_CONSOLE_ARCADE: + return rc_hash_arcade(hash, path); + + case RC_CONSOLE_ATARI_JAGUAR_CD: + return rc_hash_jaguar_cd(hash, path); + + case RC_CONSOLE_DREAMCAST: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_dreamcast(hash, path); + + case RC_CONSOLE_GAMECUBE: + return rc_hash_gamecube(hash, path); + + case RC_CONSOLE_MS_DOS: + return rc_hash_ms_dos(hash, path); + + case RC_CONSOLE_NEO_GEO_CD: + return rc_hash_neogeo_cd(hash, path); + + case RC_CONSOLE_NINTENDO_64: + return rc_hash_n64(hash, path); + + case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: + return rc_hash_nintendo_ds(hash, path); + + case RC_CONSOLE_PC_ENGINE_CD: + if (rc_path_compare_extension(path, "cue") || rc_path_compare_extension(path, "chd")) + return rc_hash_pce_cd(hash, path); + + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_buffered_file(hash, console_id, path); + + case RC_CONSOLE_PCFX: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_pcfx_cd(hash, path); + + case RC_CONSOLE_PLAYSTATION: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_psx(hash, path); + + case RC_CONSOLE_PLAYSTATION_2: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_ps2(hash, path); + + case RC_CONSOLE_PSP: + return rc_hash_psp(hash, path); + + case RC_CONSOLE_SEGA_CD: + case RC_CONSOLE_SATURN: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_sega_cd(hash, path); + } +} + +static void rc_hash_iterator_append_console(struct rc_hash_iterator* iterator, uint8_t console_id) +{ + int i = 0; + while (iterator->consoles[i] != 0) + { + if (iterator->consoles[i] == console_id) + return; + + ++i; + } + + iterator->consoles[i] = console_id; +} + +static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, const char* path) +{ + size_t size = iterator->buffer_size; + if (size == 0) + { + /* attempt to use disk size to determine system */ + void* file = rc_file_open(path); + if (file) + { + rc_file_seek(file, 0, SEEK_END); + size = (size_t)rc_file_tell(file); + rc_file_close(file); + } + } + + if (size == 512 * 9 * 80) /* 360KB */ + { + /* FAT-12 3.5" DD (512 byte sectors, 9 sectors per track, 80 tracks per side */ + /* FAT-12 5.25" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (size == 512 * 9 * 80 * 2) /* 720KB */ + { + /* FAT-12 3.5" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (size == 512 * 9 * 40) /* 180KB */ + { + /* FAT-12 5.25" DD (512 byte sectors, 9 sectors per track, 40 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + + /* AMSDOS 3" - 40 tracks */ + iterator->consoles[1] = RC_CONSOLE_AMSTRAD_PC; + } + else if (size == 256 * 16 * 35) /* 140KB */ + { + /* Apple II new format - 256 byte sectors, 16 sectors per track, 35 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + else if (size == 256 * 13 * 35) /* 113.75KB */ + { + /* Apple II old format - 256 byte sectors, 13 sectors per track, 35 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + + /* once a best guess has been identified, make sure the others are added as fallbacks */ + + /* check MSX first, as Apple II isn't supported by RetroArch, and RAppleWin won't use the iterator */ + rc_hash_iterator_append_console(iterator, RC_CONSOLE_MSX); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_AMSTRAD_PC); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); +} + +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; + + memset(iterator, 0, sizeof(*iterator)); + iterator->buffer = buffer; + iterator->buffer_size = buffer_size; + + iterator->consoles[0] = 0; + + do + { + const char* ext = rc_path_get_extension(path); + switch (tolower(*ext)) + { + case '2': + if (rc_path_compare_extension(ext, "2d")) + { + iterator->consoles[0] = RC_CONSOLE_SHARPX1; + } + break; + + case '3': + if (rc_path_compare_extension(ext, "3ds") || + rc_path_compare_extension(ext, "3dsx")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; + } + break; + + case '7': + if (rc_path_compare_extension(ext, "7z")) + { + /* decompressing zip file not supported */ + iterator->consoles[0] = RC_CONSOLE_ARCADE; + need_path = 1; + } + break; + + case '8': + /* http://tibasicdev.wikidot.com/file-extensions */ + if (rc_path_compare_extension(ext, "83g") || + rc_path_compare_extension(ext, "83p")) + { + iterator->consoles[0] = RC_CONSOLE_TI83; + } + break; + + case 'a': + if (rc_path_compare_extension(ext, "a78")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_7800; + } + else if (rc_path_compare_extension(ext, "app") || + rc_path_compare_extension(ext, "axf")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; + } + break; + + case 'b': + if (rc_path_compare_extension(ext, "bin")) + { + if (buffer_size == 0) + { + /* raw bin file may be a CD track. if it's more than 32MB, try a CD hash. */ + void* file = rc_file_open(path); + if (file) + { + int64_t size; + + rc_file_seek(file, 0, SEEK_END); + size = rc_file_tell(file); + rc_file_close(file); + + if (size > 32 * 1024 * 1024) + { + iterator->consoles[0] = RC_CONSOLE_3DO; /* 4DO supports directly opening the bin file */ + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION; /* PCSX ReARMed supports directly opening the bin file*/ + iterator->consoles[2] = RC_CONSOLE_PLAYSTATION_2; /* PCSX2 supports directly opening the bin file*/ + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* Genesis Plus GX supports directly opening the bin file*/ + + /* fallback to megadrive which just does a full hash. */ + iterator->consoles[4] = RC_CONSOLE_MEGA_DRIVE; + break; + } + } + } + + /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, + * 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; + } + else if (rc_path_compare_extension(ext, "bs")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; + } + break; + + case 'c': + if (rc_path_compare_extension(ext, "cue")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + 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[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_ATARI_JAGUAR_CD; + need_path = 1; + } + else if (rc_path_compare_extension(ext, "chd")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + 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_PSP; + iterator->consoles[5] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[6] = RC_CONSOLE_3DO; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_PCFX; + need_path = 1; + } + else if (rc_path_compare_extension(ext, "col")) + { + iterator->consoles[0] = RC_CONSOLE_COLECOVISION; + } + else if (rc_path_compare_extension(ext, "cas")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (rc_path_compare_extension(ext, "chf")) + { + iterator->consoles[0] = RC_CONSOLE_FAIRCHILD_CHANNEL_F; + } + else if (rc_path_compare_extension(ext, "cart")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_CASSETTEVISION; + } + else if (rc_path_compare_extension(ext, "cci") || + rc_path_compare_extension(ext, "cia") || + rc_path_compare_extension(ext, "cxi")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; + } + break; + + case 'd': + if (rc_path_compare_extension(ext, "dsk")) + { + rc_hash_initialize_dsk_iterator(iterator, path); + } + else if (rc_path_compare_extension(ext, "d64")) + { + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + } + else if (rc_path_compare_extension(ext, "d88")) + { + iterator->consoles[0] = RC_CONSOLE_PC8800; + iterator->consoles[1] = RC_CONSOLE_SHARPX1; + } + else if (rc_path_compare_extension(ext, "dosz")) + { + iterator->consoles[0] = RC_CONSOLE_MS_DOS; + } + break; + + case 'e': + if (rc_path_compare_extension(ext, "elf")) + { + /* This should probably apply to more consoles in the future */ + /* Although in any case this just hashes the entire file */ + iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; + } + break; + + case 'f': + if (rc_path_compare_extension(ext, "fig")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; + } + else if (rc_path_compare_extension(ext, "fds")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO; + } + else if (rc_path_compare_extension(ext, "fd")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + } + break; + + case 'g': + if (rc_path_compare_extension(ext, "gba")) + { + iterator->consoles[0] = RC_CONSOLE_GAMEBOY_ADVANCE; + } + else if (rc_path_compare_extension(ext, "gbc")) + { + iterator->consoles[0] = RC_CONSOLE_GAMEBOY_COLOR; + } + else if (rc_path_compare_extension(ext, "gb")) + { + iterator->consoles[0] = RC_CONSOLE_GAMEBOY; + } + else if (rc_path_compare_extension(ext, "gg")) + { + iterator->consoles[0] = RC_CONSOLE_GAME_GEAR; + } + else if (rc_path_compare_extension(ext, "gdi")) + { + iterator->consoles[0] = RC_CONSOLE_DREAMCAST; + } + break; + + case 'h': + if (rc_path_compare_extension(ext, "hex")) + { + iterator->consoles[0] = RC_CONSOLE_ARDUBOY; + } + break; + + case 'i': + if (rc_path_compare_extension(ext, "iso")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[1] = RC_CONSOLE_PSP; + iterator->consoles[2] = RC_CONSOLE_3DO; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + need_path = 1; + } + break; + + case 'j': + if (rc_path_compare_extension(ext, "jag")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_JAGUAR; + } + break; + + case 'k': + if (rc_path_compare_extension(ext, "k7")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* tape */ + } + break; + + case 'l': + if (rc_path_compare_extension(ext, "lnx")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_LYNX; + } + break; + + case 'm': + if (rc_path_compare_extension(ext, "m3u")) + { + const char* disc_path = rc_hash_get_first_item_from_playlist(path); + if (!disc_path) /* did not find a disc */ + return; + + iterator->buffer = NULL; /* ignore buffer; assume it's the m3u contents */ + + path = iterator->path = disc_path; + continue; /* retry with disc_path */ + } + else if (rc_path_compare_extension(ext, "md")) + { + iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; + } + else if (rc_path_compare_extension(ext, "min")) + { + iterator->consoles[0] = RC_CONSOLE_POKEMON_MINI; + } + else if (rc_path_compare_extension(ext, "mx1")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (rc_path_compare_extension(ext, "mx2")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (rc_path_compare_extension(ext, "m5")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* cartridge */ + } + else if (rc_path_compare_extension(ext, "m7")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* cartridge */ + } + break; + + case 'n': + if (rc_path_compare_extension(ext, "nes")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO; + } + else if (rc_path_compare_extension(ext, "nds")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_DS; /* ASSERT: handles both DS and DSi */ + } + else if (rc_path_compare_extension(ext, "n64") || + rc_path_compare_extension(ext, "ndd")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; + } + else if (rc_path_compare_extension(ext, "ngc")) + { + iterator->consoles[0] = RC_CONSOLE_NEOGEO_POCKET; + } + else if (rc_path_compare_extension(ext, "nib")) + { + /* also Apple II, but both are full-file hashes */ + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + } + break; + + case 'p': + if (rc_path_compare_extension(ext, "pce")) + { + iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; + } + else if (rc_path_compare_extension(ext, "pgm")) + { + iterator->consoles[0] = RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER; + } + break; + + case 'r': + if (rc_path_compare_extension(ext, "rom")) + { + /* rom is associated with MSX, Thomson TO-8, and Fairchild Channel F. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + if (rc_path_compare_extension(ext, "ri")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + break; + + case 's': + if (rc_path_compare_extension(ext, "smc") || + rc_path_compare_extension(ext, "sfc") || + rc_path_compare_extension(ext, "swc")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; + } + else if (rc_path_compare_extension(ext, "sg")) + { + iterator->consoles[0] = RC_CONSOLE_SG1000; + } + else if (rc_path_compare_extension(ext, "sgx")) + { + iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; + } + else if (rc_path_compare_extension(ext, "sv")) + { + iterator->consoles[0] = RC_CONSOLE_SUPERVISION; + } + else if (rc_path_compare_extension(ext, "sap")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + } + break; + + case 't': + if (rc_path_compare_extension(ext, "tap")) + { + iterator->consoles[0] = RC_CONSOLE_ORIC; + } + else if (rc_path_compare_extension(ext, "tic")) + { + iterator->consoles[0] = RC_CONSOLE_TIC80; + } + else if (rc_path_compare_extension(ext, "tvc")) + { + iterator->consoles[0] = RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER; + } + break; + + case 'u': + if (rc_path_compare_extension(ext, "uze")) + { + iterator->consoles[0] = RC_CONSOLE_UZEBOX; + } + break; + + case 'v': + if (rc_path_compare_extension(ext, "vb")) + { + iterator->consoles[0] = RC_CONSOLE_VIRTUAL_BOY; + } + else if (rc_path_compare_extension(ext, "v64")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; + } + break; + + case 'w': + if (rc_path_compare_extension(ext, "wsc")) + { + iterator->consoles[0] = RC_CONSOLE_WONDERSWAN; + } + else if (rc_path_compare_extension(ext, "wasm")) + { + iterator->consoles[0] = RC_CONSOLE_WASM4; + } + else if (rc_path_compare_extension(ext, "woz")) + { + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + break; + + case 'z': + if (rc_path_compare_extension(ext, "zip")) + { + /* decompressing zip file not supported */ + iterator->consoles[0] = RC_CONSOLE_ARCADE; + need_path = 1; + } + else if (rc_path_compare_extension(ext, "z64")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; + } + break; + } + + if (verbose_message_callback) + { + char message[256]; + int count = 0; + while (iterator->consoles[count]) + ++count; + + snprintf(message, sizeof(message), "Found %d potential consoles for %s file extension", count, ext); + verbose_message_callback(message); + } + + /* loop is only for specific cases that redirect to another file - like m3u */ + break; + } while (1); + + if (need_path && !iterator->path) + iterator->path = strdup(path); + + /* if we didn't match the extension, default to something that does a whole file hash */ + if (!iterator->consoles[0]) + iterator->consoles[0] = RC_CONSOLE_GAMEBOY; +} + +void rc_hash_destroy_iterator(struct rc_hash_iterator* iterator) +{ + if (iterator->path) + { + free((void*)iterator->path); + iterator->path = NULL; + } +} + +int rc_hash_iterate(char hash[33], struct rc_hash_iterator* iterator) +{ + int next_console; + int result = 0; + + do + { + next_console = iterator->consoles[iterator->index]; + if (next_console == 0) + { + hash[0] = '\0'; + break; + } + + ++iterator->index; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Trying console %d", next_console); + verbose_message_callback(message); + } + + if (iterator->buffer) + result = rc_hash_generate_from_buffer(hash, next_console, iterator->buffer, iterator->buffer_size); + else + result = rc_hash_generate_from_file(hash, next_console, iterator->path); + + } while (!result); + + return result; +} diff --git a/3rdparty/rcheevos/src/rhash/md5.c b/3rdparty/rcheevos/src/rhash/md5.c new file mode 100644 index 0000000000..f3a5205669 --- /dev/null +++ b/3rdparty/rcheevos/src/rhash/md5.c @@ -0,0 +1,382 @@ +/* + Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.c is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order + either statically or dynamically; added missing #include + in library. + 2002-03-11 lpd Corrected argument list for main(), and added int return + type, in test program and T value program. + 2002-02-21 lpd Added missing #include in test program. + 2000-07-03 lpd Patched to eliminate warnings about "constant is + unsigned in ANSI C, signed in traditional"; made test program + self-checking. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). + 1999-05-03 lpd Original version. + */ + +#include "md5.h" +#include +#include + +#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ +#ifdef ARCH_IS_BIG_ENDIAN +# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) +#else +# define BYTE_ORDER 0 +#endif + +#define T_MASK ((md5_word_t)~0) +#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) +#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) +#define T3 0x242070db +#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) +#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) +#define T6 0x4787c62a +#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) +#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) +#define T9 0x698098d8 +#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) +#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) +#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) +#define T13 0x6b901122 +#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) +#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) +#define T16 0x49b40821 +#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) +#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) +#define T19 0x265e5a51 +#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) +#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) +#define T22 0x02441453 +#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) +#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) +#define T25 0x21e1cde6 +#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) +#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) +#define T28 0x455a14ed +#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) +#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) +#define T31 0x676f02d9 +#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) +#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) +#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) +#define T35 0x6d9d6122 +#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) +#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) +#define T38 0x4bdecfa9 +#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) +#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) +#define T41 0x289b7ec6 +#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) +#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) +#define T44 0x04881d05 +#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) +#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) +#define T47 0x1fa27cf8 +#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) +#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) +#define T50 0x432aff97 +#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) +#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) +#define T53 0x655b59c3 +#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) +#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) +#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) +#define T57 0x6fa87e4f +#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) +#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) +#define T60 0x4e0811a1 +#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) +#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) +#define T63 0x2ad7d2bb +#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) + + +static void +md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) +{ + md5_word_t + a = pms->abcd[0], b = pms->abcd[1], + c = pms->abcd[2], d = pms->abcd[3]; + md5_word_t t; +#if BYTE_ORDER > 0 + /* Define storage only for big-endian CPUs. */ + md5_word_t X[16]; +#else + /* Define storage for little-endian or both types of CPUs. */ + md5_word_t xbuf[16]; + const md5_word_t *X; +#endif + + { +#if BYTE_ORDER == 0 + /* + * Determine dynamically whether this is a big-endian or + * little-endian machine, since we can use a more efficient + * algorithm on the latter. + */ + static const int w = 1; + + if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ +#endif +#if BYTE_ORDER <= 0 /* little-endian */ + { + /* + * On little-endian machines, we can process properly aligned + * data without copying it. + */ + if (!((ptrdiff_t)data & 3)) { + /* data are properly aligned */ + X = (const md5_word_t *)data; + } else { + /* not aligned */ + memcpy(xbuf, data, 64); + X = xbuf; + } + } +#endif +#if BYTE_ORDER == 0 + else /* dynamic big-endian */ +#endif +#if BYTE_ORDER >= 0 /* big-endian */ + { + /* + * On big-endian machines, we must arrange the bytes in the + * right order. + */ + const md5_byte_t *xp = data; + int i; + +# if BYTE_ORDER == 0 + X = xbuf; /* (dynamic only) */ +# else +# define xbuf X /* (static only) */ +# endif + for (i = 0; i < 16; ++i, xp += 4) + xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24); + } +#endif + } + +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + + /* Round 1. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ +#define F(x, y, z) (((x) & (y)) | (~(x) & (z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + F(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 7, T1); + SET(d, a, b, c, 1, 12, T2); + SET(c, d, a, b, 2, 17, T3); + SET(b, c, d, a, 3, 22, T4); + SET(a, b, c, d, 4, 7, T5); + SET(d, a, b, c, 5, 12, T6); + SET(c, d, a, b, 6, 17, T7); + SET(b, c, d, a, 7, 22, T8); + SET(a, b, c, d, 8, 7, T9); + SET(d, a, b, c, 9, 12, T10); + SET(c, d, a, b, 10, 17, T11); + SET(b, c, d, a, 11, 22, T12); + SET(a, b, c, d, 12, 7, T13); + SET(d, a, b, c, 13, 12, T14); + SET(c, d, a, b, 14, 17, T15); + SET(b, c, d, a, 15, 22, T16); +#undef SET + + /* Round 2. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ +#define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + G(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 1, 5, T17); + SET(d, a, b, c, 6, 9, T18); + SET(c, d, a, b, 11, 14, T19); + SET(b, c, d, a, 0, 20, T20); + SET(a, b, c, d, 5, 5, T21); + SET(d, a, b, c, 10, 9, T22); + SET(c, d, a, b, 15, 14, T23); + SET(b, c, d, a, 4, 20, T24); + SET(a, b, c, d, 9, 5, T25); + SET(d, a, b, c, 14, 9, T26); + SET(c, d, a, b, 3, 14, T27); + SET(b, c, d, a, 8, 20, T28); + SET(a, b, c, d, 13, 5, T29); + SET(d, a, b, c, 2, 9, T30); + SET(c, d, a, b, 7, 14, T31); + SET(b, c, d, a, 12, 20, T32); +#undef SET + + /* Round 3. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + H(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 5, 4, T33); + SET(d, a, b, c, 8, 11, T34); + SET(c, d, a, b, 11, 16, T35); + SET(b, c, d, a, 14, 23, T36); + SET(a, b, c, d, 1, 4, T37); + SET(d, a, b, c, 4, 11, T38); + SET(c, d, a, b, 7, 16, T39); + SET(b, c, d, a, 10, 23, T40); + SET(a, b, c, d, 13, 4, T41); + SET(d, a, b, c, 0, 11, T42); + SET(c, d, a, b, 3, 16, T43); + SET(b, c, d, a, 6, 23, T44); + SET(a, b, c, d, 9, 4, T45); + SET(d, a, b, c, 12, 11, T46); + SET(c, d, a, b, 15, 16, T47); + SET(b, c, d, a, 2, 23, T48); +#undef SET + + /* Round 4. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ +#define I(x, y, z) ((y) ^ ((x) | ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + I(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 6, T49); + SET(d, a, b, c, 7, 10, T50); + SET(c, d, a, b, 14, 15, T51); + SET(b, c, d, a, 5, 21, T52); + SET(a, b, c, d, 12, 6, T53); + SET(d, a, b, c, 3, 10, T54); + SET(c, d, a, b, 10, 15, T55); + SET(b, c, d, a, 1, 21, T56); + SET(a, b, c, d, 8, 6, T57); + SET(d, a, b, c, 15, 10, T58); + SET(c, d, a, b, 6, 15, T59); + SET(b, c, d, a, 13, 21, T60); + SET(a, b, c, d, 4, 6, T61); + SET(d, a, b, c, 11, 10, T62); + SET(c, d, a, b, 2, 15, T63); + SET(b, c, d, a, 9, 21, T64); +#undef SET + + /* Then perform the following additions. (That is increment each + of the four registers by the value it had before this block + was started.) */ + pms->abcd[0] += a; + pms->abcd[1] += b; + pms->abcd[2] += c; + pms->abcd[3] += d; +} + +void +md5_init(md5_state_t *pms) +{ + pms->count[0] = pms->count[1] = 0; + pms->abcd[0] = 0x67452301; + pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; + pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; + pms->abcd[3] = 0x10325476; +} + +void +md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) +{ + const md5_byte_t *p = data; + int left = nbytes; + int offset = (pms->count[0] >> 3) & 63; + md5_word_t nbits = (md5_word_t)(nbytes << 3); + + if (nbytes <= 0) + return; + + /* Update the message length. */ + pms->count[1] += nbytes >> 29; + pms->count[0] += nbits; + if (pms->count[0] < nbits) + pms->count[1]++; + + /* Process an initial partial block. */ + if (offset) { + int copy = (offset + nbytes > 64 ? 64 - offset : nbytes); + + memcpy(pms->buf + offset, p, copy); + if (offset + copy < 64) + return; + p += copy; + left -= copy; + md5_process(pms, pms->buf); + } + + /* Process full blocks. */ + for (; left >= 64; p += 64, left -= 64) + md5_process(pms, p); + + /* Process a final partial block. */ + if (left) + memcpy(pms->buf, p, left); +} + +void +md5_finish(md5_state_t *pms, md5_byte_t digest[16]) +{ + static const md5_byte_t pad[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + md5_byte_t data[8]; + int i; + + /* Save the length before padding. */ + for (i = 0; i < 8; ++i) + data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); + /* Pad to 56 bytes mod 64. */ + md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); + /* Append the length. */ + md5_append(pms, data, 8); + for (i = 0; i < 16; ++i) + digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); +} diff --git a/3rdparty/rcheevos/src/rhash/md5.h b/3rdparty/rcheevos/src/rhash/md5.h new file mode 100644 index 0000000000..698c995d8f --- /dev/null +++ b/3rdparty/rcheevos/src/rhash/md5.h @@ -0,0 +1,91 @@ +/* + Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.h is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Removed support for non-ANSI compilers; removed + references to Ghostscript; clarified derivation from RFC 1321; + now handles byte order either statically or dynamically. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); + added conditionalization for C++ compilation from Martin + Purschke . + 1999-05-03 lpd Original version. + */ + +#ifndef md5_INCLUDED +# define md5_INCLUDED + +/* + * This package supports both compile-time and run-time determination of CPU + * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be + * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is + * defined as non-zero, the code will be compiled to run only on big-endian + * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to + * run on either big- or little-endian CPUs, but will run slightly less + * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. + */ + +typedef unsigned char md5_byte_t; /* 8-bit byte */ +typedef unsigned int md5_word_t; /* 32-bit word */ + +/* Define the state of the MD5 Algorithm. */ +typedef struct md5_state_s { + md5_word_t count[2]; /* message length in bits, lsw first */ + md5_word_t abcd[4]; /* digest buffer */ + md5_byte_t buf[64]; /* accumulate block */ +} md5_state_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Initialize the algorithm. */ +void md5_init(md5_state_t *pms); + +/* Append a string to the message. */ +void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes); + +/* Finish the message and return the digest. */ +void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); + +#ifdef __cplusplus +} /* end extern "C" */ +#endif + +#endif /* md5_INCLUDED */ diff --git a/3rdparty/rcheevos/src/rurl/url.c b/3rdparty/rcheevos/src/rurl/url.c new file mode 100644 index 0000000000..9897189c2f --- /dev/null +++ b/3rdparty/rcheevos/src/rurl/url.c @@ -0,0 +1,402 @@ +#include "rc_url.h" + +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +#if RCHEEVOS_URL_SSL +#define RCHEEVOS_URL_PROTOCOL "https" +#else +#define RCHEEVOS_URL_PROTOCOL "http" +#endif + +static int rc_url_encode(char* encoded, size_t len, const char* str) { + for (;;) { + switch (*str) { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + case '-': case '_': case '.': case '~': + if (len < 2) + return -1; + + *encoded++ = *str++; + --len; + break; + + case ' ': + if (len < 2) + return -1; + + *encoded++ = '+'; + ++str; + --len; + break; + + default: + if (len < 4) + return -1; + + snprintf(encoded, len, "%%%02x", (unsigned char)*str); + encoded += 3; + ++str; + len -= 3; + break; + + case '\0': + *encoded = 0; + return 0; + } + } +} + +int rc_url_award_cheevo(char* buffer, size_t size, const char* user_name, const char* login_token, + unsigned cheevo_id, int hardcore, const char* game_hash) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=awardachievement&u=%s&t=%s&a=%u&h=%d", + urle_user_name, + urle_login_token, + cheevo_id, + hardcore ? 1 : 0 + ); + + if (game_hash && strlen(game_hash) == 32 && (size - (size_t)written) >= 35) { + written += snprintf(buffer + written, size - (size_t)written, "&m=%s", game_hash); + } + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_submit_lboard(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned lboard_id, int value) { + char urle_user_name[64]; + char urle_login_token[64]; + char signature[64]; + unsigned char hash[16]; + md5_state_t state; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + /* Evaluate the signature. */ + snprintf(signature, sizeof(signature), "%u%s%d", lboard_id, user_name, value); + md5_init(&state); + md5_append(&state, (unsigned char*)signature, (int)strlen(signature)); + md5_finish(&state, hash); + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=submitlbentry&u=%s&t=%s&i=%u&s=%d&v=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + urle_user_name, + urle_login_token, + lboard_id, + value, + hash[ 0], hash[ 1], hash[ 2], hash[ 3], hash[ 4], hash[ 5], hash[ 6], hash[ 7], + hash[ 8], hash[ 9], hash[10], hash[11],hash[12], hash[13], hash[14], hash[15] + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_gameid(char* buffer, size_t size, const char* hash) { + int written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=gameid&m=%s", + hash + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_patch(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=patch&u=%s&t=%s&g=%u", + urle_user_name, + urle_login_token, + gameid + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_badge_image(char* buffer, size_t size, const char* badge_name) { + int written = snprintf( + buffer, + size, + "http://i.retroachievements.org/Badge/%s", + badge_name + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_login_with_password(char* buffer, size_t size, const char* user_name, const char* password) { + char urle_user_name[64]; + char urle_password[256]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_password, sizeof(urle_password), password) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=login&u=%s&p=%s", + urle_user_name, + urle_password + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_login_with_token(char* buffer, size_t size, const char* user_name, const char* login_token) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=login&u=%s&t=%s", + urle_user_name, + urle_login_token + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_unlock_list(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid, int hardcore) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=unlocks&u=%s&t=%s&g=%u&h=%d", + urle_user_name, + urle_login_token, + gameid, + hardcore ? 1 : 0 + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_post_playing(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=postactivity&u=%s&t=%s&a=3&m=%u", + urle_user_name, + urle_login_token, + gameid + ); + + return (size_t)written >= size ? -1 : 0; +} + +static int rc_url_append_param_equals(char* buffer, size_t buffer_size, size_t buffer_offset, const char* param) +{ + int written = 0; + size_t param_len; + + if (buffer_offset >= buffer_size) + return -1; + + if (buffer_offset) { + buffer += buffer_offset; + buffer_size -= buffer_offset; + + if (buffer[-1] != '?') { + *buffer++ = '&'; + buffer_size--; + written = 1; + } + } + + param_len = strlen(param); + if (param_len + 1 >= buffer_size) + return -1; + memcpy(buffer, param, param_len); + buffer[param_len] = '='; + + written += (int)param_len + 1; + return written + (int)buffer_offset; +} + +static int rc_url_append_unum(char* buffer, size_t buffer_size, size_t* buffer_offset, const char* param, unsigned value) +{ + int written = rc_url_append_param_equals(buffer, buffer_size, *buffer_offset, param); + if (written > 0) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%u", value); + + if (chars + written < (int)buffer_size) { + memcpy(&buffer[written], num, chars + 1); + *buffer_offset = written + chars; + return 0; + } + } + + return -1; +} + +static int rc_url_append_str(char* buffer, size_t buffer_size, size_t* buffer_offset, const char* param, const char* value) +{ + int written = rc_url_append_param_equals(buffer, buffer_size, *buffer_offset, param); + if (written > 0) { + buffer += written; + buffer_size -= written; + + if (rc_url_encode(buffer, buffer_size, value) == 0) { + written += (int)strlen(buffer); + *buffer_offset = written; + return 0; + } + } + + return -1; +} + +static int rc_url_build_dorequest(char* url_buffer, size_t url_buffer_size, size_t* buffer_offset, + const char* api, const char* user_name) +{ + const char* base_url = RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php"; + size_t written = strlen(base_url); + int failure = 0; + + if (url_buffer_size < written + 1) + return -1; + memcpy(url_buffer, base_url, written); + url_buffer[written++] = '?'; + + failure |= rc_url_append_str(url_buffer, url_buffer_size, &written, "r", api); + if (user_name) + failure |= rc_url_append_str(url_buffer, url_buffer_size, &written, "u", user_name); + + *buffer_offset += written; + return failure; +} + +int rc_url_ping(char* url_buffer, size_t url_buffer_size, char* post_buffer, size_t post_buffer_size, + const char* user_name, const char* login_token, unsigned gameid, const char* rich_presence) +{ + size_t written = 0; + int failure = rc_url_build_dorequest(url_buffer, url_buffer_size, &written, "ping", user_name); + failure |= rc_url_append_unum(url_buffer, url_buffer_size, &written, "g", gameid); + + written = 0; + failure |= rc_url_append_str(post_buffer, post_buffer_size, &written, "t", login_token); + + if (rich_presence && *rich_presence) + failure |= rc_url_append_str(post_buffer, post_buffer_size, &written, "m", rich_presence); + + if (failure) { + if (url_buffer_size) + url_buffer[0] = '\0'; + if (post_buffer_size) + post_buffer[0] = '\0'; + } + + return failure; +} + +int rc_url_get_lboard_entries(char* buffer, size_t size, unsigned lboard_id, unsigned first_index, unsigned count) +{ + size_t written = 0; + int failure = rc_url_build_dorequest(buffer, size, &written, "lbinfo", NULL); + failure |= rc_url_append_unum(buffer, size, &written, "i", lboard_id); + if (first_index > 1) + failure |= rc_url_append_unum(buffer, size, &written, "o", first_index - 1); + failure |= rc_url_append_unum(buffer, size, &written, "c", count); + + return failure; +} + +int rc_url_get_lboard_entries_near_user(char* buffer, size_t size, unsigned lboard_id, const char* user_name, unsigned count) +{ + size_t written = 0; + int failure = rc_url_build_dorequest(buffer, size, &written, "lbinfo", NULL); + failure |= rc_url_append_unum(buffer, size, &written, "i", lboard_id); + failure |= rc_url_append_str(buffer, size, &written, "u", user_name); + failure |= rc_url_append_unum(buffer, size, &written, "c", count); + + return failure; +} + +#undef RCHEEVOS_URL_PROTOCOL diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj index 604b7c95a9..124bb925b3 100644 --- a/pcsx2/pcsx2.vcxproj +++ b/pcsx2/pcsx2.vcxproj @@ -51,7 +51,7 @@ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\rapidyaml\src %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\rapidyaml\ext\c4core\src %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\rapidyaml\ext\c4core\src\c4\ext\fast_float\include - %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rcheevos\rcheevos\include;$(SolutionDir)3rdparty\rainterface + %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rcheevos\include;$(SolutionDir)3rdparty\rainterface %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\soundtouch\soundtouch %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\discord-rpc\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\jpgd