From 3ccaddc7e6a708542e1433dc5e99a79916419ce9 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 21 Feb 2021 16:58:40 +1000 Subject: [PATCH] dep: Add rcheevos --- CMakeLists.txt | 1 + dep/CMakeLists.txt | 4 + dep/rcheevos/.editorconfig | 15 + dep/rcheevos/.gitignore | 65 + dep/rcheevos/CHANGELOG.md | 187 ++ dep/rcheevos/CMakeLists.txt | 32 + dep/rcheevos/LICENSE | 21 + dep/rcheevos/README.md | 634 ++++++ dep/rcheevos/include/rc_consoles.h | 86 + dep/rcheevos/include/rc_hash.h | 132 ++ dep/rcheevos/include/rc_url.h | 35 + dep/rcheevos/include/rcheevos.h | 546 +++++ dep/rcheevos/rcheevos.vcxproj | 558 +++++ dep/rcheevos/rcheevos.vcxproj.filters | 96 + dep/rcheevos/src/rcheevos/alloc.c | 192 ++ dep/rcheevos/src/rcheevos/compat.c | 62 + dep/rcheevos/src/rcheevos/condition.c | 261 +++ dep/rcheevos/src/rcheevos/condset.c | 369 ++++ dep/rcheevos/src/rcheevos/consoleinfo.c | 659 ++++++ dep/rcheevos/src/rcheevos/format.c | 155 ++ dep/rcheevos/src/rcheevos/lboard.c | 263 +++ dep/rcheevos/src/rcheevos/memref.c | 225 ++ dep/rcheevos/src/rcheevos/operand.c | 470 +++++ dep/rcheevos/src/rcheevos/rc_compat.h | 60 + dep/rcheevos/src/rcheevos/rc_internal.h | 147 ++ dep/rcheevos/src/rcheevos/richpresence.c | 684 ++++++ dep/rcheevos/src/rcheevos/runtime.c | 691 ++++++ dep/rcheevos/src/rcheevos/runtime_progress.c | 533 +++++ dep/rcheevos/src/rcheevos/trigger.c | 224 ++ dep/rcheevos/src/rcheevos/value.c | 310 +++ dep/rcheevos/src/rhash/cdreader.c | 782 +++++++ dep/rcheevos/src/rhash/hash.c | 1993 ++++++++++++++++++ dep/rcheevos/src/rhash/md5.c | 381 ++++ dep/rcheevos/src/rhash/md5.h | 91 + dep/rcheevos/src/rurl/url.c | 373 ++++ duckstation.sln | 27 + 36 files changed, 11364 insertions(+) create mode 100644 dep/rcheevos/.editorconfig create mode 100644 dep/rcheevos/.gitignore create mode 100644 dep/rcheevos/CHANGELOG.md create mode 100644 dep/rcheevos/CMakeLists.txt create mode 100644 dep/rcheevos/LICENSE create mode 100644 dep/rcheevos/README.md create mode 100644 dep/rcheevos/include/rc_consoles.h create mode 100644 dep/rcheevos/include/rc_hash.h create mode 100644 dep/rcheevos/include/rc_url.h create mode 100644 dep/rcheevos/include/rcheevos.h create mode 100644 dep/rcheevos/rcheevos.vcxproj create mode 100644 dep/rcheevos/rcheevos.vcxproj.filters create mode 100644 dep/rcheevos/src/rcheevos/alloc.c create mode 100644 dep/rcheevos/src/rcheevos/compat.c create mode 100644 dep/rcheevos/src/rcheevos/condition.c create mode 100644 dep/rcheevos/src/rcheevos/condset.c create mode 100644 dep/rcheevos/src/rcheevos/consoleinfo.c create mode 100644 dep/rcheevos/src/rcheevos/format.c create mode 100644 dep/rcheevos/src/rcheevos/lboard.c create mode 100644 dep/rcheevos/src/rcheevos/memref.c create mode 100644 dep/rcheevos/src/rcheevos/operand.c create mode 100644 dep/rcheevos/src/rcheevos/rc_compat.h create mode 100644 dep/rcheevos/src/rcheevos/rc_internal.h create mode 100644 dep/rcheevos/src/rcheevos/richpresence.c create mode 100644 dep/rcheevos/src/rcheevos/runtime.c create mode 100644 dep/rcheevos/src/rcheevos/runtime_progress.c create mode 100644 dep/rcheevos/src/rcheevos/trigger.c create mode 100644 dep/rcheevos/src/rcheevos/value.c create mode 100644 dep/rcheevos/src/rhash/cdreader.c create mode 100644 dep/rcheevos/src/rhash/hash.c create mode 100644 dep/rcheevos/src/rhash/md5.c create mode 100644 dep/rcheevos/src/rhash/md5.h create mode 100644 dep/rcheevos/src/rurl/url.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e718cc79d..fb162441d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ if(NOT ANDROID) option(BUILD_NOGUI_FRONTEND "Build the NoGUI frontend" ON) option(BUILD_QT_FRONTEND "Build the Qt frontend" ON) option(ENABLE_DISCORD_PRESENCE "Build with Discord Rich Presence support" ON) + option(ENABLE_CHEEVOS "Build with RetroAchievements support" OFF) option(USE_SDL2 "Link with SDL2 for controller support" ON) endif() diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index fcc5b17f7..b5d2b6aaa 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -23,6 +23,10 @@ if(ENABLE_DISCORD_PRESENCE) add_subdirectory(discord-rpc) endif() +if(ENABLE_RETROACHIEVEMENTS) + add_subdirectory(rcheevos) +endif() + if(${CPU_ARCH} STREQUAL "aarch32" OR ${CPU_ARCH} STREQUAL "aarch64") add_subdirectory(vixl) endif() diff --git a/dep/rcheevos/.editorconfig b/dep/rcheevos/.editorconfig new file mode 100644 index 000000000..f07212da4 --- /dev/null +++ b/dep/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,cpp}] +charset = latin1 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +curly_bracket_next_line = false diff --git a/dep/rcheevos/.gitignore b/dep/rcheevos/.gitignore new file mode 100644 index 000000000..23ead4b3a --- /dev/null +++ b/dep/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/dep/rcheevos/CHANGELOG.md b/dep/rcheevos/CHANGELOG.md new file mode 100644 index 000000000..caae09ad9 --- /dev/null +++ b/dep/rcheevos/CHANGELOG.md @@ -0,0 +1,187 @@ +# v10.0.0 + +* renamed `rhash.h` to eliminate conflict with system headers, renamed `rconsoles.h` and `rurl.h` for consistency +* allow ranges in rich presence lookups +* add RC_CONDITION_RESET_NEXT_IF +* support MAXOF($) 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. +* reset to default cd reader if NULL is passed to rc_hash_init_custom_cdreader +* add hash support for RC_CONSOLE_DREAMCAST +* ignore headers for RC_CONSOLE_PC_ENGINE +* look for unique identifier in RC_CONSOLE_SEGA_CD and RC_CONSOLE_SATURN discs +* rename RC_CONSOLE_MAGNAVOX_ODYSSEY -> RC_CONSOLE_MAGNAVOX_ODYSSEY2 +* rename RC_CONSOLE_AMIGA_ST -> RC_CONSOLE_ATARI_ST +* 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/dep/rcheevos/CMakeLists.txt b/dep/rcheevos/CMakeLists.txt new file mode 100644 index 000000000..4b1b48507 --- /dev/null +++ b/dep/rcheevos/CMakeLists.txt @@ -0,0 +1,32 @@ +add_library(rcheevos + include/rc_consoles.h + include/rc_hash.h + include/rc_url.h + include/rcheevos.h + src/rcheevos/alloc.c + src/rcheevos/compat.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_compat.h + src/rcheevos/rc_internal.h + src/rcheevos/richpresence.c + src/rcheevos/runtime.c + src/rcheevos/runtime_progress.c + src/rcheevos/trigger.c + src/rcheevos/value.c + 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}/include") +target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") + + diff --git a/dep/rcheevos/LICENSE b/dep/rcheevos/LICENSE new file mode 100644 index 000000000..5f1faf3dc --- /dev/null +++ b/dep/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/dep/rcheevos/README.md b/dep/rcheevos/README.md new file mode 100644 index 000000000..af422f97d --- /dev/null +++ b/dep/rcheevos/README.md @@ -0,0 +1,634 @@ +# **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 or JSON parsing. Clients must get data from RetroAchievements, parse the JSON payloads and pass the results down to **rcheevos** for processing. (**TODO**: document the server API and JSON schema.) + +Not all structures defined by **rcheevos** can be created via the public API, but are exposed to allow interactions beyond just creation, destruction, and testing, such as the ones required by UI code that helps to create them. + +Finally, **rcheevos** does *not* allocate or manage memory by itself. All structures that can be returned by it have a function to determine the number of bytes needed to hold the structure, and another one that actually builds the structure using a caller-provided buffer to bake it. + +## Lua + +RetroAchievements is considering the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. The current expression-based implementation is often limiting on newer systems. + +At this point, to enable Lua support, you must compile with an additional compilation flag: `HAVE_LUA`, as neither the backend nor the UI for editing achievements are currently Lua-enabled. + +> **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/). + +### User Configuration + +There's only one thing that can be configured by users of **rcheevos**: `RC_ALIGNMENT`. This macro holds the alignment of allocations made in the buffer provided to the parsing functions, and the default value is `sizeof(void*)`. + +If your platform will benefit from a different value, define a new value for it on your compiler flags before compiling the code. It has to be a power of 2, but no checking is done. + +### Return values + +The functions that compute the amount of memory that something will take return the number of bytes, or a negative value from the following enumeration: + +```c +enum { + RC_OK = 0, + RC_INVALID_LUA_OPERAND = -1, + RC_INVALID_MEMORY_OPERAND = -2, + RC_INVALID_CONST_OPERAND = -3, + RC_INVALID_FP_OPERAND = -4, + RC_INVALID_CONDITION_TYPE = -5, + RC_INVALID_OPERATOR = -6, + RC_INVALID_REQUIRED_HITS = -7, + RC_DUPLICATED_START = -8, + RC_DUPLICATED_CANCEL = -9, + RC_DUPLICATED_SUBMIT = -10, + RC_DUPLICATED_VALUE = -11, + RC_DUPLICATED_PROGRESS = -12, + RC_MISSING_START = -13, + RC_MISSING_CANCEL = -14, + RC_MISSING_SUBMIT = -15, + RC_MISSING_VALUE = -16, + RC_INVALID_LBOARD_FIELD = -17, + RC_MISSING_DISPLAY_STRING = -18, + RC_OUT_OF_MEMORY = -19, + RC_INVALID_VALUE_FLAG = -20, + RC_MISSING_VALUE_MEASURED = -21, + RC_MULTIPLE_MEASURED = -22, + RC_INVALID_MEASURED_TARGET = -23, + RC_INVALID_COMPARISON = -24 +}; +``` + +To convert the return code into something human-readable, pass it to: +```c +const char* rc_error_str(int ret); +``` + +### Console identifiers + +This enumeration uniquely identifies each of the supported platforms in RetroAchievements. + +```c +enum { + RC_CONSOLE_MEGA_DRIVE = 1, + RC_CONSOLE_NINTENDO_64 = 2, + RC_CONSOLE_SUPER_NINTENDO = 3, + RC_CONSOLE_GAMEBOY = 4, + RC_CONSOLE_GAMEBOY_ADVANCE = 5, + RC_CONSOLE_GAMEBOY_COLOR = 6, + RC_CONSOLE_NINTENDO = 7, + RC_CONSOLE_PC_ENGINE = 8, + RC_CONSOLE_SEGA_CD = 9, + RC_CONSOLE_SEGA_32X = 10, + RC_CONSOLE_MASTER_SYSTEM = 11, + RC_CONSOLE_PLAYSTATION = 12, + RC_CONSOLE_ATARI_LYNX = 13, + RC_CONSOLE_NEOGEO_POCKET = 14, + RC_CONSOLE_GAME_GEAR = 15, + RC_CONSOLE_GAMECUBE = 16, + RC_CONSOLE_ATARI_JAGUAR = 17, + RC_CONSOLE_NINTENDO_DS = 18, + RC_CONSOLE_WII = 19, + RC_CONSOLE_WII_U = 20, + RC_CONSOLE_PLAYSTATION_2 = 21, + RC_CONSOLE_XBOX = 22, + RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, + RC_CONSOLE_POKEMON_MINI = 24, + RC_CONSOLE_ATARI_2600 = 25, + RC_CONSOLE_MS_DOS = 26, + RC_CONSOLE_ARCADE = 27, + RC_CONSOLE_VIRTUAL_BOY = 28, + RC_CONSOLE_MSX = 29, + RC_CONSOLE_COMMODORE_64 = 30, + RC_CONSOLE_ZX81 = 31, + RC_CONSOLE_ORIC = 32, + RC_CONSOLE_SG1000 = 33, + RC_CONSOLE_VIC20 = 34, + RC_CONSOLE_AMIGA = 35, + RC_CONSOLE_ATARI_ST = 36, + RC_CONSOLE_AMSTRAD_PC = 37, + RC_CONSOLE_APPLE_II = 38, + RC_CONSOLE_SATURN = 39, + RC_CONSOLE_DREAMCAST = 40, + RC_CONSOLE_PSP = 41, + RC_CONSOLE_CDI = 42, + RC_CONSOLE_3DO = 43, + RC_CONSOLE_COLECOVISION = 44, + RC_CONSOLE_INTELLIVISION = 45, + RC_CONSOLE_VECTREX = 46, + RC_CONSOLE_PC8800 = 47, + RC_CONSOLE_PC9800 = 48, + RC_CONSOLE_PCFX = 49, + RC_CONSOLE_ATARI_5200 = 50, + RC_CONSOLE_ATARI_7800 = 51, + RC_CONSOLE_X68K = 52, + RC_CONSOLE_WONDERSWAN = 53, + RC_CONSOLE_CASSETTEVISION = 54, + RC_CONSOLE_SUPER_CASSETTEVISION = 55, + RC_CONSOLE_NEO_GEO_CD = 56, + RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, + RC_CONSOLE_FM_TOWNS = 58, + RC_CONSOLE_ZX_SPECTRUM = 59, + RC_CONSOLE_GAME_AND_WATCH = 60, + RC_CONSOLE_NOKIA_NGAGE = 61, + RC_CONSOLE_NINTENDO_3DS = 62 +}; +``` + +### `rc_operand_t` + +An operand is the leaf node of RetroAchievements expressions, and can hold one of the following: + +* A constant integer or floating-point value +* A memory address of the system being emulated +* A reference to the Lua function that will be called to provide the value + +```c +typedef struct { + union { + /* A value read from memory. */ + rc_memref_value_t* memref; + + /* An integer value. */ + unsigned num; + + /* A floating point value. */ + double dbl; + + /* A reference to the Lua function that provides the value. */ + int luafunc; + }; + + /* specifies which member of the value union is being used */ + char type; + + /* the actual RC_MEMSIZE of the operand - memref.size may differ */ + char size; +} +rc_operand_t; +``` + +The `size` field, when applicable, holds one of these values: + +```c +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 +}; +``` + +The `type` field is always valid, and holds one of these values: + +```c +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 */ +}; +``` + +`RC_OPERAND_ADDRESS`, `RC_OPERAND_DELTA`, `RC_OPERAND_PRIOR`, `RC_OPERAND_BCD`, and `RC_OPERAND_INVERTED` mean that `memref` is active. `RC_OPERAND_CONST` means that `num` is active. `RC_OPERAND_FP` means that `dbl` is active. `RC_OPERAND_LUA` means `luafunc` is active. + + +### `rc_condition_t` + +A condition compares its two operands according to the defined operator. It also keeps track of other things to make it possible to code more advanced achievements. + +```c +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. */ + unsigned required_hits; + /* Number of hits so far. */ + unsigned current_hits; + + /* The next condition in the chain. */ + rc_condition_t* next; + + /* The type of the condition. */ + char type; + /* The comparison operator to use. */ + char oper; /* operator is a reserved word in C++. */ + /* Set if the condition needs to processed as part of the "check if paused" pass. */ + char pause; + /* Whether or not the condition evaluated as true on the last check. */ + char is_true; +}; +``` + +`type` can be one of these values: + +```c +enum { + RC_CONDITION_STANDARD, + RC_CONDITION_PAUSE_IF, + RC_CONDITION_RESET_IF, + RC_CONDITION_ADD_SOURCE, + RC_CONDITION_SUB_SOURCE, + RC_CONDITION_ADD_HITS, + RC_CONDITION_AND_NEXT, + RC_CONDITION_MEASURED, + RC_CONDITION_ADD_ADDRESS, + RC_CONDITION_TRIGGER, + RC_CONDITION_MEASURED_IF, + RC_CONDITION_RESET_NEXT_IF, +}; +``` + +`oper` is the comparison operator to be used when comparing the two operands: + +```c +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_condset_t` + +Condition sets are an ordered collection of conditions (`rc_condition_t`), which are usually and'ed together to help build complex expressions for achievements. + +```c +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. */ + char has_pause; +}; +``` + +### `rc_trigger_t` + +Triggers are the basic blocks of achievements and leaderboards. In fact, achievements are just triggers with some additional information like title, description, a badge, and some state, like whether it has already been awarded or not. All the logic to test if an achievement should be awarded is encapsulated in `rc_trigger_t`. + +```c +typedef struct { + /* 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_value_t* memrefs; +} +rc_trigger_t; +``` + +The size in bytes of memory a trigger needs to be created is given by the `rc_trigger_size` function: + +```c +int rc_trigger_size(const char* memaddr); +``` + +The return value is the size needed for the trigger described by the `memaddr` parameter, or a negative value with an [error code](#return-values). + +Once the memory size is known, `rc_parse_trigger` can be called to actually construct a trigger in the caller-provided buffer: + +```c +rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +``` + +`buffer` is the caller-allocated buffer, which must have enough space for the trigger. `memaddr` describes the trigger, and must be the same one used to compute the trigger's size with `rc_trigger_size`. `L` must be a valid Lua state, and `funcs_ndx` must be an index to the current Lua stack which contains a table which is a map of names to functions. This map is used to look for operands which are Lua functions. + +Once the trigger is created, `rc_evaluate_trigger` can be called to test whether the trigger fires or not. + +```c +int rc_evaluate_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); +``` + +`trigger` is the trigger to test. `peek` is a callback used to read bytes from the emulated memory. `ud` is an user-provided opaque value that is passed to `peek`. `L` is the Lua state in which context the Lua functions are looked for and called, if necessary. + +`rc_peek_t`'s signature is: + +```c +typedef unsigned (*rc_peek_t)(unsigned address, unsigned num_bytes, void* ud); +``` + +where `address` is the starting address to read from, `num_bytes` the number of bytes to read (1, 2, or 4, little-endian), and `ud` is the same value passed to `rc_test_trigger`. + +> Addresses passed to `peek` do *not* map 1:1 to the emulated memory. (**TODO**: document the mapping from `peek` addresses to emulated memory for each supported system.) + +The return value of `rc_evaluate_trigger` is one of the following: +```c +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 */ +}; +``` + +Finally, `rc_reset_trigger` can be used to reset the internal state of a trigger. + +```c +void rc_reset_trigger(rc_trigger_t* self); +``` + +### `rc_value_t` + +A value is a collection of conditions that result in a single RC_CONDITION_MEASURED expression. It's used to calculate the value for a leaderboard and for lookups in rich presence. + +```c +typedef struct { + /* The list of conditions to evaluate. */ + rc_condset_t* conditions; + + /* The memory references required by the value. */ + rc_memref_value_t* memrefs; +} +rc_value_t; +``` + +The size in bytes needed to create a value can be computed by `rc_value_size`: + +```c +int rc_value_size(const char* memaddr); +``` + +With the size at hand, the caller can allocate the necessary memory and pass it to `rc_parse_value` to create the value: + +```c +rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +``` + +`buffer`, `memaddr`, `L`, and `funcs_ndx` are the same as in [`rc_parse_trigger`](#rc_parse_trigger). + +To compute the value, use `rc_evaluate_value`: + +```c +int rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); +``` + +`value` is the value to compute the value of, and `peek`, `ud`, and `L`, are as in [`rc_test_trigger`](#rc_test_trigger). + +### `rc_lboard_t` + +Leaderboards track a value over time, starting when a trigger is fired. The leaderboard can be canceled depending on the value of another trigger, and submitted to the RetroAchievements server depending on a third trigger. + +The value submitted comes from the `value` field. Values displayed to the player come from the `progress` field unless it's `NULL`, in which case it's the same as `value`. + +```c +typedef struct { + rc_trigger_t start; + rc_trigger_t submit; + rc_trigger_t cancel; + rc_value_t value; + rc_value_t* progress; + rc_memref_value_t* memrefs; + + char state; +} +rc_lboard_t; +``` + +Leaderboards are created and parsed just the same as triggers and values: + +```c +int rc_lboard_size(const char* memaddr); +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +``` + +A leaderboard can be evaluated with the `rc_evaluate_lboard` function: + +```c +int rc_evaluate_lboard(rc_lboard_t* lboard, int* value, rc_peek_t peek, void* peek_ud, lua_State* L); +``` + +The function returns an action that must be performed by the caller, and `value` contains the value to be used for that action when the function returns. The action can be one of: + +```c +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 */ +}; +``` + +The caller must keep track of these values and do the necessary actions: + +* `RC_LBOARD_ACTIVE` and `RC_LBOARD_INACTIVE`: just signal that the leaderboard didn't change its state. +* `RC_LBOARD_STARTED`: indicates that the leaderboard has been started, so the caller can i.e. show a message for the player, and start showing its value in the UI. +* `RC_LBOARD_CANCELED`: the leaderboard has been canceled, and the caller can inform the user and stop showing its value. +* `RC_LBOARD_TRIGGERED`: the leaderboard has been finished, and the value must be submitted to the RetroAchievements server; the caller can also notify the player and stop showing the value in the UI. + +`rc_reset_lboard` resets the leaderboard: + +```c +void rc_reset_lboard(rc_lboard_t* lboard); +``` + +### `rc_runtime_t` + +The runtime encapsulates a set of achievements and leaderboards and manages processing them for each frame. When important things occur, events are raised for the caller via a callback. + +```c +typedef struct rc_runtime_t { + rc_runtime_trigger_t* triggers; + unsigned trigger_count; + unsigned trigger_capacity; + + rc_runtime_lboard_t* lboards; + unsigned lboard_count; + unsigned lboard_capacity; + + rc_runtime_richpresence_t* richpresence; + char* richpresence_display_buffer; + char richpresence_update_timer; + + rc_memref_value_t* memrefs; + rc_memref_value_t** next_memref; +} +rc_runtime_t; +``` + +The runtime must first be initialized. +```c +void rc_runtime_init(rc_runtime_t* runtime); +``` + +Then individual achievements, leaderboards, and even rich presence can be loaded into the runtime. These functions return RC_OK, or one of the negative value error codes listed above. +```c +int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); +``` + +The runtime should be called once per frame to evaluate the state of the active achievements/leaderboards: +```c +void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_peek_t peek, void* ud, lua_State* L); +``` + +The `event_handler` is a callback function that is called for each event that occurs when processing the frame. +```c +typedef struct rc_runtime_event_t { + unsigned id; + int value; + char type; +} +rc_runtime_event_t; + +typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); +``` + +The `event.type` field will be one of the following: +* RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED (id=achievement id) + An achievement starts in the RC_TRIGGER_STATE_WAITING state and cannot trigger until it has been false for at least one frame. This event indicates the achievement is no longer waiting and may trigger on a future frame. +* RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED (id=achievement id) + One or more conditions in the achievement have disabled the achievement. +* RC_RUNTIME_EVENT_ACHIEVEMENT_RESET (id=achievement id) + One or more conditions in the achievement have reset any progress captured in the achievement. +* RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED (id=achievement id) + All conditions for the achievement have been met and the user should be informed. + NOTE: If `rc_runtime_reset` is called without deactivating the achievement, it may trigger again. +* RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED (id=achievement id) + All non-trigger conditions for the achievement have been met. This typically indicates the achievement is a challenge achievement and the challenge is active. +* RC_RUNTIME_EVENT_LBOARD_STARTED (id=leaderboard id, value=leaderboard value) + The leaderboard's start condition has been met and the user should be informed that a leaderboard attempt has started. +* RC_RUNTIME_EVENT_LBOARD_CANCELED (id=leaderboard id, value=leaderboard value) + The leaderboard's cancel condition has been met and the user should be informed that a leaderboard attempt has failed. +* RC_RUNTIME_EVENT_LBOARD_UPDATED (id=leaderboard id, value=leaderboard value) + The leaderboard value has changed. +* RC_RUNTIME_EVENT_LBOARD_TRIGGERED (id=leaderboard id, value=leaderboard value) + The leaderboard's submit condition has been met and the user should be informed that a leaderboard attempt was successful. The value should be submitted. + +When an achievement triggers, it should be deactivated so it won't trigger again: +```c +void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); +``` +Additionally, the unlock should be submitted to the server. + +When a leaderboard triggers, it should not be deactivated in case the player wants to try again for a better score. The value should be submitted to the server. + +`rc_runtime_do_frame` also periodically updates the rich presense string (every 60 frames). To get the current value, call +```c +const char* rc_runtime_get_richpresence(const rc_runtime_t* runtime); +``` + +When the game is reset, the runtime should also be reset: +```c +void rc_runtime_reset(rc_runtime_t* runtime); +``` + +This ensures any active achievements/leaderboards are set back to their initial states and prevents unexpected triggers when the memory changes in atypical way. + + +### Value Formatting + +**rcheevos** includes helper functions to parse formatting strings from RetroAchievements, and format values according to them. + +`rc_parse_format` returns the format for the given string: + +```c +int rc_parse_format(const char* format_str); +``` + +The returned value is one of: + +```c +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_VALUE` is returned if `format_str` doesn't contain a valid format. + +`rc_format_value` can be used to format the given value into the provided buffer: + +```c +int rc_format_value(char* buffer, int size, int value, int format); +``` + +`buffer` receives `value` formatted according to `format`. No more than `size` characters will be written to `buffer`. 32 characters are enough to hold any valid value with any format. The returned value is the number of characters written. + +# **rurl** + +**rurl** 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 URL that are valid for the server. + +**rurl** does *not* make HTTP requests. + +## API + +### Return values + +All functions return `0` if successful, or `-1` in case of errors. Errors are usually because the provided buffer is too small to hold the URL. If your buffer is large and you're still receiving errors, please open an issue. + +### Functions + +All functions take a `buffer`, where the URL will be written into, and `size` with the size of the buffer. The other parameters are particular to the desired URL. + +```c +int rc_url_award_cheevo(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned cheevo_id, int hardcore); + +int rc_url_submit_lboard(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned lboard_id, unsigned value, unsigned char hash[16]); + +int rc_url_get_gameid(char* buffer, size_t size, unsigned char hash[16]); + +int rc_url_get_patch(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid); + +int rc_url_get_badge_image(char* buffer, size_t size, const char* badge_name); + +int rc_url_login_with_password(char* buffer, size_t size, const char* user_name, const char* password); + +int rc_url_login_with_token(char* buffer, size_t size, const char* user_name, const char* login_token); + +int rc_url_get_unlock_list(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid, int hardcore); + +int rc_url_post_playing(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid); +``` diff --git a/dep/rcheevos/include/rc_consoles.h b/dep/rcheevos/include/rc_consoles.h new file mode 100644 index 000000000..4dd1d9b25 --- /dev/null +++ b/dep/rcheevos/include/rc_consoles.h @@ -0,0 +1,86 @@ +#ifndef RC_CONSOLES_H +#define RC_CONSOLES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/*****************************************************************************\ +| Console identifiers | +\*****************************************************************************/ + +enum { + RC_CONSOLE_MEGA_DRIVE = 1, + RC_CONSOLE_NINTENDO_64 = 2, + RC_CONSOLE_SUPER_NINTENDO = 3, + RC_CONSOLE_GAMEBOY = 4, + RC_CONSOLE_GAMEBOY_ADVANCE = 5, + RC_CONSOLE_GAMEBOY_COLOR = 6, + RC_CONSOLE_NINTENDO = 7, + RC_CONSOLE_PC_ENGINE = 8, + RC_CONSOLE_SEGA_CD = 9, + RC_CONSOLE_SEGA_32X = 10, + RC_CONSOLE_MASTER_SYSTEM = 11, + RC_CONSOLE_PLAYSTATION = 12, + RC_CONSOLE_ATARI_LYNX = 13, + RC_CONSOLE_NEOGEO_POCKET = 14, + RC_CONSOLE_GAME_GEAR = 15, + RC_CONSOLE_GAMECUBE = 16, + RC_CONSOLE_ATARI_JAGUAR = 17, + RC_CONSOLE_NINTENDO_DS = 18, + RC_CONSOLE_WII = 19, + RC_CONSOLE_WII_U = 20, + RC_CONSOLE_PLAYSTATION_2 = 21, + RC_CONSOLE_XBOX = 22, + RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, + RC_CONSOLE_POKEMON_MINI = 24, + RC_CONSOLE_ATARI_2600 = 25, + RC_CONSOLE_MS_DOS = 26, + RC_CONSOLE_ARCADE = 27, + RC_CONSOLE_VIRTUAL_BOY = 28, + RC_CONSOLE_MSX = 29, + RC_CONSOLE_COMMODORE_64 = 30, + RC_CONSOLE_ZX81 = 31, + RC_CONSOLE_ORIC = 32, + RC_CONSOLE_SG1000 = 33, + RC_CONSOLE_VIC20 = 34, + RC_CONSOLE_AMIGA = 35, + RC_CONSOLE_ATARI_ST = 36, + RC_CONSOLE_AMSTRAD_PC = 37, + RC_CONSOLE_APPLE_II = 38, + RC_CONSOLE_SATURN = 39, + RC_CONSOLE_DREAMCAST = 40, + RC_CONSOLE_PSP = 41, + RC_CONSOLE_CDI = 42, + RC_CONSOLE_3DO = 43, + RC_CONSOLE_COLECOVISION = 44, + RC_CONSOLE_INTELLIVISION = 45, + RC_CONSOLE_VECTREX = 46, + RC_CONSOLE_PC8800 = 47, + RC_CONSOLE_PC9800 = 48, + RC_CONSOLE_PCFX = 49, + RC_CONSOLE_ATARI_5200 = 50, + RC_CONSOLE_ATARI_7800 = 51, + RC_CONSOLE_X68K = 52, + RC_CONSOLE_WONDERSWAN = 53, + RC_CONSOLE_CASSETTEVISION = 54, + RC_CONSOLE_SUPER_CASSETTEVISION = 55, + RC_CONSOLE_NEO_GEO_CD = 56, + RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, + RC_CONSOLE_FM_TOWNS = 58, + RC_CONSOLE_ZX_SPECTRUM = 59, + RC_CONSOLE_GAME_AND_WATCH = 60, + RC_CONSOLE_NOKIA_NGAGE = 61, + RC_CONSOLE_NINTENDO_3DS = 62, + + RC_CONSOLE_HUBS = 100, + RC_CONSOLE_EVENTS = 101 +}; + +const char* rc_console_name(int console_id); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_CONSOLES_H */ diff --git a/dep/rcheevos/include/rc_hash.h b/dep/rcheevos/include/rc_hash.h new file mode 100644 index 000000000..3c03265e1 --- /dev/null +++ b/dep/rcheevos/include/rc_hash.h @@ -0,0 +1,132 @@ +#ifndef RC_HASH_H +#define RC_HASH_H + +#include +#include +#include + +#include "rc_consoles.h" + +#ifdef __cplusplus +extern "C" { +#endif + + /* ===================================================== */ + + /* generates a hash from a block of memory. + * returns non-zero on success, or zero on failure. + */ + int rc_hash_generate_from_buffer(char hash[33], int console_id, uint8_t* buffer, size_t buffer_size); + + /* generates a hash from a file. + * returns non-zero on success, or zero on failure. + */ + int rc_hash_generate_from_file(char hash[33], int console_id, const char* path); + + /* ===================================================== */ + + /* data for rc_hash_iterate + */ + struct rc_hash_iterator + { + uint8_t* buffer; + size_t buffer_size; + uint8_t consoles[12]; + int index; + const char* path; + }; + + /* initializes a rc_hash_iterator + * - path must be provided + * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) + */ + void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size); + + /* releases resources associated to a rc_hash_iterator + */ + void 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. + */ + int 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_hash_message_callback)(const char*); + void rc_hash_init_error_message_callback(rc_hash_message_callback callback); + + /* specifies a function to call for verbose logging */ + void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback); + + /* ===================================================== */ + + /* opens a file */ + typedef void* (*rc_hash_filereader_open_file_handler)(const char* path_utf8); + + /* moves the file pointer - standard fseek parameters */ + typedef void (*rc_hash_filereader_seek_handler)(void* file_handle, size_t offset, int origin); + + /* locates the file pointer */ + typedef size_t (*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_hash_filereader_read_handler)(void* file_handle, void* buffer, size_t requested_bytes); + + /* closes the file */ + typedef void (*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; + }; + + void rc_hash_init_custom_filereader(struct rc_hash_filereader* reader); + + /* ===================================================== */ + + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) + + /* opens a track from the specified file. track 0 indicates the largest data track should be opened. + * returns a handle to be passed to the other functions, or NULL if the track could not be opened. + */ + typedef void* (*rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); + + /* attempts to read the specified number of bytes from the file starting at the read pointer. + * returns the number of bytes actually read. + */ + typedef size_t (*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_hash_cdreader_close_track_handler)(void* track_handle); + + /* convert absolute sector to track sector */ + typedef uint32_t(*rc_hash_cdreader_absolute_sector_to_track_sector)(void* track_handle, uint32_t sector); + + 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_absolute_sector_to_track_sector absolute_sector_to_track_sector; + }; + + void rc_hash_init_default_cdreader(); + void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader); + + /* ===================================================== */ + +#ifdef __cplusplus +} +#endif + +#endif /* RC_HASH_H */ diff --git a/dep/rcheevos/include/rc_url.h b/dep/rcheevos/include/rc_url.h new file mode 100644 index 000000000..a0e7a1176 --- /dev/null +++ b/dep/rcheevos/include/rc_url.h @@ -0,0 +1,35 @@ +#ifndef RC_URL_H +#define RC_URL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +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); + +int rc_url_submit_lboard(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned lboard_id, int value); + +int rc_url_get_gameid(char* buffer, size_t size, const char* hash); + +int rc_url_get_patch(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid); + +int rc_url_get_badge_image(char* buffer, size_t size, const char* badge_name); + +int rc_url_login_with_password(char* buffer, size_t size, const char* user_name, const char* password); + +int rc_url_login_with_token(char* buffer, size_t size, const char* user_name, const char* login_token); + +int rc_url_get_unlock_list(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid, int hardcore); + +int rc_url_post_playing(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid); + +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); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_URL_H */ diff --git a/dep/rcheevos/include/rcheevos.h b/dep/rcheevos/include/rcheevos.h new file mode 100644 index 000000000..e93858738 --- /dev/null +++ b/dep/rcheevos/include/rcheevos.h @@ -0,0 +1,546 @@ +#ifndef RCHEEVOS_H +#define RCHEEVOS_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct lua_State lua_State; + +/*****************************************************************************\ +| 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 +}; + +const char* rc_error_str(int ret); + +/*****************************************************************************\ +| 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 unsigned (*rc_peek_t)(unsigned address, unsigned 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_VARIABLE +}; + +typedef struct rc_memref_value_t { + /* The current value of this memory reference. */ + unsigned value; + /* The last differing value of this memory reference. */ + unsigned prior; + + /* The size of the value. */ + char size; + /* True if the value changed this frame. */ + char changed; + /* True if the reference will be used in indirection. + * NOTE: This is actually a property of the rc_memref_t, but we put it here to save space */ + char is_indirect; +} rc_memref_value_t; + +typedef struct rc_memref_t rc_memref_t; + +struct rc_memref_t { + /* The current value at the specified memory address. */ + rc_memref_value_t value; + + /* The memory address of this variable. */ + unsigned 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 { + union { + /* A value read from memory. */ + rc_memref_t* memref; + + /* An integer value. */ + unsigned 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 */ + char type; + + /* the actual RC_MEMSIZE of the operand - memref.size may differ */ + char size; +} +rc_operand_t; + +/*****************************************************************************\ +| Conditions | +\*****************************************************************************/ + +/* types */ +enum { + RC_CONDITION_STANDARD, + RC_CONDITION_PAUSE_IF, + RC_CONDITION_RESET_IF, + RC_CONDITION_ADD_SOURCE, + RC_CONDITION_SUB_SOURCE, + RC_CONDITION_ADD_HITS, + RC_CONDITION_AND_NEXT, + RC_CONDITION_MEASURED, + RC_CONDITION_ADD_ADDRESS, + RC_CONDITION_OR_NEXT, + RC_CONDITION_TRIGGER, + RC_CONDITION_MEASURED_IF, + RC_CONDITION_RESET_NEXT_IF, + RC_CONDITION_SUB_HITS +}; + +/* 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 +}; + +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. */ + unsigned required_hits; + /* Number of hits so far. */ + unsigned current_hits; + + /* The next condition in the chain. */ + rc_condition_t* next; + + /* The type of the condition. */ + char type; + + /* The comparison operator to use. */ + char oper; /* operator is a reserved word in C++. */ + + /* Set if the condition needs to processed as part of the "check if paused" pass. */ + char pause; + + /* Whether or not the condition evaluated true on the last check */ + char is_true; +}; + +/*****************************************************************************\ +| 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. */ + char has_pause; + + /* True if the set is currently paused. */ + char is_paused; +}; + +/*****************************************************************************\ +| 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 */ +}; + +typedef struct { + /* 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. */ + unsigned measured_value; + + /* The target state of the MEASURED condition */ + unsigned measured_target; + + /* The current state of the trigger */ + char state; + + /* True if at least one condition has a non-zero hit count */ + char has_hits; + + /* True if at least one condition has a non-zero required hit count */ + char has_required_hits; +} +rc_trigger_t; + +int rc_trigger_size(const char* memaddr); +rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +int rc_evaluate_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); +int rc_test_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); +void rc_reset_trigger(rc_trigger_t* self); + +/*****************************************************************************\ +| Values | +\*****************************************************************************/ + +typedef struct rc_value_t rc_value_t; + +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 value. */ + rc_memref_t* memrefs; + + /* The name of the variable. */ + const char* name; + + /* The next variable in the chain. */ + rc_value_t* next; +}; + +int rc_value_size(const char* memaddr); +rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +int rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); + +/*****************************************************************************\ +| 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 */ +}; + +typedef struct { + rc_trigger_t start; + rc_trigger_t submit; + rc_trigger_t cancel; + rc_value_t value; + rc_value_t* progress; + rc_memref_t* memrefs; + + char state; +} +rc_lboard_t; + +int rc_lboard_size(const char* memaddr); +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +int rc_evaluate_lboard(rc_lboard_t* lboard, int* value, rc_peek_t peek, void* peek_ud, lua_State* L); +void 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 +}; + +int rc_parse_format(const char* format_str); +int rc_format_value(char* buffer, int size, int value, int format); + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +typedef struct rc_richpresence_lookup_item_t rc_richpresence_lookup_item_t; + +struct rc_richpresence_lookup_item_t { + unsigned first; + unsigned 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; + unsigned short 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; + unsigned short 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; +}; + +typedef struct { + rc_richpresence_display_t* first_display; + rc_richpresence_lookup_t* first_lookup; + rc_memref_t* memrefs; + rc_value_t* variables; +} +rc_richpresence_t; + +int rc_richpresence_size(const char* script); +rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx); +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +typedef struct rc_runtime_trigger_t { + unsigned id; + rc_trigger_t* trigger; + void* buffer; + rc_memref_t* invalid_memref; + unsigned char md5[16]; + int serialized_size; + char owns_memrefs; +} +rc_runtime_trigger_t; + +typedef struct rc_runtime_lboard_t { + unsigned id; + int value; + rc_lboard_t* lboard; + void* buffer; + rc_memref_t* invalid_memref; + unsigned char md5[16]; + char owns_memrefs; +} +rc_runtime_lboard_t; + +typedef struct rc_runtime_richpresence_t { + rc_richpresence_t* richpresence; + void* buffer; + struct rc_runtime_richpresence_t* previous; + char owns_memrefs; +} +rc_runtime_richpresence_t; + +typedef struct rc_runtime_t { + rc_runtime_trigger_t* triggers; + unsigned trigger_count; + unsigned trigger_capacity; + + rc_runtime_lboard_t* lboards; + unsigned lboard_count; + unsigned lboard_capacity; + + rc_runtime_richpresence_t* richpresence; + + rc_memref_t* memrefs; + rc_memref_t** next_memref; + + rc_value_t* variables; + rc_value_t** next_variable; +} +rc_runtime_t; + +void rc_runtime_init(rc_runtime_t* runtime); +void rc_runtime_destroy(rc_runtime_t* runtime); + +int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* runtime, unsigned id); + +int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_lboard(rc_runtime_t* runtime, unsigned id); +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* runtime, unsigned id); + +int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, unsigned buffersize, rc_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 +}; + +typedef struct rc_runtime_event_t { + unsigned id; + int value; + char type; +} +rc_runtime_event_t; + +typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); + +void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_peek_t peek, void* ud, lua_State* L); +void rc_runtime_reset(rc_runtime_t* runtime); +void rc_runtime_invalidate_address(rc_runtime_t* runtime, unsigned address); + +int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L); +int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L); +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* serialized, lua_State* L); + +/*****************************************************************************\ +| 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 { + unsigned start_address; /* first address of block as queried by RetroAchievements */ + unsigned end_address; /* last address of block as queried by RetroAchievements */ + unsigned real_address; /* real address for first address of block */ + char type; /* RC_MEMORY_TYPE_ for block */ + const char* description; /* short description of block */ +} +rc_memory_region_t; + +typedef struct rc_memory_regions_t { + const rc_memory_region_t* region; + unsigned num_regions; +} +rc_memory_regions_t; + +const rc_memory_regions_t* rc_console_memory_regions(int console_id); + +#ifdef __cplusplus +} +#endif + +#endif /* RCHEEVOS_H */ diff --git a/dep/rcheevos/rcheevos.vcxproj b/dep/rcheevos/rcheevos.vcxproj new file mode 100644 index 000000000..886074a53 --- /dev/null +++ b/dep/rcheevos/rcheevos.vcxproj @@ -0,0 +1,558 @@ + + + + + DebugFast + ARM64 + + + DebugFast + Win32 + + + DebugFast + x64 + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseLTCG + ARM64 + + + ReleaseLTCG + Win32 + + + ReleaseLTCG + x64 + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} + Win32Proj + rcheevos + 10.0 + + + + StaticLibrary + true + v142 + NotSet + + + StaticLibrary + true + v142 + NotSet + + + StaticLibrary + true + v142 + NotSet + + + StaticLibrary + true + v142 + NotSet + + + StaticLibrary + true + v142 + NotSet + + + StaticLibrary + true + v142 + NotSet + + + StaticLibrary + false + v142 + true + NotSet + false + + + StaticLibrary + false + v142 + true + NotSet + false + + + StaticLibrary + false + v142 + true + NotSet + false + + + StaticLibrary + false + v142 + true + NotSet + false + + + StaticLibrary + false + v142 + true + NotSet + false + + + StaticLibrary + false + v142 + true + NotSet + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + true + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + true + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + true + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + true + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + true + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + false + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + false + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + false + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + false + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + false + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + false + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + + + + + + Level2 + Disabled + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(ProjectDir)include;%(AdditionalIncludeDirectories) + false + stdcpp17 + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + + + + + + + Level2 + Disabled + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(ProjectDir)include;%(AdditionalIncludeDirectories) + false + stdcpp17 + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + + + + + + + Level2 + Disabled + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(ProjectDir)include;%(AdditionalIncludeDirectories) + false + stdcpp17 + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + + + + + + + Level2 + Disabled + RC_DISABLE_LUA;_ITERATOR_DEBUG_LEVEL=1;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(ProjectDir)include;%(AdditionalIncludeDirectories) + Default + false + stdcpp17 + false + true + OnlyExplicitInline + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + + + + + + + Level2 + Disabled + RC_DISABLE_LUA;_ITERATOR_DEBUG_LEVEL=1;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(ProjectDir)include;%(AdditionalIncludeDirectories) + Default + false + stdcpp17 + false + true + OnlyExplicitInline + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + + + + + + + Level2 + Disabled + RC_DISABLE_LUA;_ITERATOR_DEBUG_LEVEL=1;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(ProjectDir)include;%(AdditionalIncludeDirectories) + Default + false + stdcpp17 + false + true + OnlyExplicitInline + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + + + + + Level2 + + + MaxSpeed + true + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(ProjectDir)include;%(AdditionalIncludeDirectories) + false + stdcpp17 + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + true + true + + + + + Level2 + + + MaxSpeed + true + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(ProjectDir)include;%(AdditionalIncludeDirectories) + true + stdcpp17 + true + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + true + true + + + + + Level2 + + + MaxSpeed + true + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(ProjectDir)include;%(AdditionalIncludeDirectories) + false + stdcpp17 + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + true + true + + + + + Level2 + + + MaxSpeed + true + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(ProjectDir)include;%(AdditionalIncludeDirectories) + false + stdcpp17 + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + true + true + + + + + Level2 + + + MaxSpeed + true + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(ProjectDir)include;%(AdditionalIncludeDirectories) + true + stdcpp17 + true + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + true + true + + + + + Level2 + + + MaxSpeed + true + RC_DISABLE_LUA;_CRT_NONSTDC_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(ProjectDir)include;%(AdditionalIncludeDirectories) + true + stdcpp17 + true + true + true + /Zo /utf-8 %(AdditionalOptions) + + + Windows + true + true + true + + + + + + \ No newline at end of file diff --git a/dep/rcheevos/rcheevos.vcxproj.filters b/dep/rcheevos/rcheevos.vcxproj.filters new file mode 100644 index 000000000..b23ecc10a --- /dev/null +++ b/dep/rcheevos/rcheevos.vcxproj.filters @@ -0,0 +1,96 @@ + + + + + {0d8ef204-a486-4873-a41d-743ca6cbe840} + + + {3aa9379e-4fd1-4772-b18c-b899631a4161} + + + {c95da822-5bca-4274-b57e-d2092e76e8f1} + + + {01fc10b0-c122-461b-b75a-f97c8b89d627} + + + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rcheevos + + + rurl + + + rhash + + + rhash + + + rhash + + + + + rcheevos + + + rcheevos + + + rhash + + + include + + + include + + + include + + + include + + + \ No newline at end of file diff --git a/dep/rcheevos/src/rcheevos/alloc.c b/dep/rcheevos/src/rcheevos/alloc.c new file mode 100644 index 000000000..afe889ed8 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/alloc.c @@ -0,0 +1,192 @@ +#include "rc_internal.h" + +#include +#include + +void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset) +{ + rc_scratch_buffer_t* buffer; + + /* 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 int aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); + *offset += (aligned_offset - *offset); + *offset += size; + } + + /* find a scratch buffer to hold the temporary data */ + buffer = &scratch->buffer; + do { + const int aligned_buffer_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const int remaining = sizeof(buffer->buffer) - aligned_buffer_offset; + + if (remaining >= size) { + /* claim the required space from an existing buffer */ + return rc_alloc(buffer->buffer, &buffer->offset, size, alignment, NULL, -1); + } + + if (!buffer->next) + break; + + buffer = buffer->next; + } while (1); + + /* not enough space in any existing buffer, allocate more */ + if (size > (int)sizeof(buffer->buffer)) { + /* caller is asking for more than we can fit in a standard rc_scratch_buffer_t. + * leverage the fact that the buffer is the last field and extend its size. + * this chunk will be exactly large enough to hold the needed data, and since offset + * will exceed sizeof(buffer->buffer), it will never be eligible to hold anything else. + */ + const int needed = sizeof(rc_scratch_buffer_t) - sizeof(buffer->buffer) + size; + buffer->next = (rc_scratch_buffer_t*)malloc(needed); + } + else { + buffer->next = (rc_scratch_buffer_t*)malloc(sizeof(rc_scratch_buffer_t)); + } + + if (!buffer->next) { + *offset = RC_OUT_OF_MEMORY; + return NULL; + } + + buffer = buffer->next; + buffer->offset = 0; + buffer->next = NULL; + + /* claim the required space from the new buffer */ + return rc_alloc(buffer->buffer, &buffer->offset, size, alignment, NULL, -1); +} + +void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset) { + void* 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 >= 0) { + /* 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) { + int 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, int length) { + int 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_alloc_scratch(NULL, &used, sizeof(rc_scratch_string_t), RC_ALIGNOF(rc_scratch_string_t), &parse->scratch, RC_OFFSETOF(parse->scratch.objs, __rc_scratch_string_t)); + ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, length + 1, RC_ALIGNOF(char), &parse->scratch, -1); + + 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.buffer.offset = 0; + parse->scratch.buffer.next = NULL; + parse->scratch.strings = NULL; + memset(&parse->scratch.objs, 0, sizeof(parse->scratch.objs)); + parse->first_memref = 0; + parse->variables = 0; + parse->measured_target = 0; + parse->has_required_hits = 0; +} + +void rc_destroy_parse_state(rc_parse_state_t* parse) +{ + rc_scratch_buffer_t* buffer = parse->scratch.buffer.next; + rc_scratch_buffer_t* next; + + while (buffer) { + next = buffer->next; + free(buffer); + buffer = next; + } +} + +const char* rc_error_str(int ret) +{ + switch (ret) { + case RC_OK: return "OK"; + case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; + case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; + case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; + case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; + case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; + case RC_INVALID_OPERATOR: return "Invalid operator"; + case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; + case RC_DUPLICATED_START: return "Duplicated start condition"; + case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; + case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; + case RC_DUPLICATED_VALUE: return "Duplicated value expression"; + case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; + case RC_MISSING_START: return "Missing start condition"; + case RC_MISSING_CANCEL: return "Missing cancel condition"; + case RC_MISSING_SUBMIT: return "Missing submit condition"; + case RC_MISSING_VALUE: return "Missing value expression"; + case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; + case RC_MISSING_DISPLAY_STRING: return "Missing display string"; + case RC_OUT_OF_MEMORY: return "Out of memory"; + case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; + case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; + case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; + case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; + case RC_INVALID_COMPARISON: return "Invalid comparison"; + case RC_INVALID_STATE: return "Invalid state"; + + default: return "Unknown error"; + } +} diff --git a/dep/rcheevos/src/rcheevos/compat.c b/dep/rcheevos/src/rcheevos/compat.c new file mode 100644 index 000000000..df75b9ed5 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/compat.c @@ -0,0 +1,62 @@ +#include "rc_compat.h" + +#include +#include + +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); + 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); + /* assume buffer is large enough and ignore size */ + result = vsprintf(buffer, format, args); + va_end(args); + + return result; +} diff --git a/dep/rcheevos/src/rcheevos/condition.c b/dep/rcheevos/src/rcheevos/condition.c new file mode 100644 index 000000000..10332dba5 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/condition.c @@ -0,0 +1,261 @@ +#include "rc_internal.h" + +#include + +char 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 '\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, int is_indirect) { + rc_condition_t* self; + const char* aux; + int ret2; + int can_modify = 0; + + aux = *memaddr; + self = RC_ALLOC(rc_condition_t, parse); + self->current_hits = 0; + + 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; + default: parse->offset = RC_INVALID_CONDITION_TYPE; return 0; + } + + aux += 2; + } + else { + self->type = RC_CONDITION_STANDARD; + } + + ret2 = rc_parse_operand(&self->operand1, &aux, 1, is_indirect, parse); + + if (ret2 < 0) { + parse->offset = ret2; + return 0; + } + + if (self->operand1.type == RC_OPERAND_FP) { + parse->offset = can_modify ? RC_INVALID_FP_OPERAND : RC_INVALID_COMPARISON; + return 0; + } + + self->oper = rc_parse_operator(&aux); + + 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: + /* modifying operators are only valid on modifying statements */ + if (can_modify) + break; + /* fallthrough */ + + case RC_INVALID_OPERATOR: + parse->offset = RC_INVALID_OPERATOR; + return 0; + + 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; + } + + ret2 = rc_parse_operand(&self->operand2, &aux, 1, is_indirect, parse); + + if (ret2 < 0) { + parse->offset = ret2; + 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 (!can_modify && self->operand2.type == RC_OPERAND_FP) { + parse->offset = RC_INVALID_COMPARISON; + return 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; + } + + 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; + } + + parse->has_required_hits = 1; + aux = end + 1; + } + else { + self->required_hits = 0; + } + + *memaddr = aux; + return self; +} + +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { + unsigned value1 = rc_evaluate_operand(&self->operand1, eval_state) + eval_state->add_value; + unsigned value2 = rc_evaluate_operand(&self->operand2, eval_state); + + switch (self->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; + case RC_OPERATOR_NONE: return 1; + default: return 1; + } +} + +int rc_evaluate_condition_value(rc_condition_t* self, rc_eval_state_t* eval_state) { + unsigned value = rc_evaluate_operand(&self->operand1, eval_state); + + switch (self->oper) { + case RC_OPERATOR_MULT: + if (self->operand2.type == RC_OPERAND_FP) + value = (int)((double)value * self->operand2.value.dbl); + else + value *= rc_evaluate_operand(&self->operand2, eval_state); + break; + + case RC_OPERATOR_DIV: + if (self->operand2.type == RC_OPERAND_FP) + { + if (self->operand2.value.dbl == 0.0) + value = 0; + else + value = (int)((double)value / self->operand2.value.dbl); + } + else + { + unsigned value2 = rc_evaluate_operand(&self->operand2, eval_state); + if (value2 == 0) + value = 0; + else + value /= value2; + } + break; + + case RC_OPERATOR_AND: + value &= rc_evaluate_operand(&self->operand2, eval_state); + break; + } + + return value; +} diff --git a/dep/rcheevos/src/rcheevos/condset.c b/dep/rcheevos/src/rcheevos/condset.c new file mode 100644 index 000000000..6ea7d8767 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/condset.c @@ -0,0 +1,369 @@ +#include "rc_internal.h" + +static void rc_update_condition_pause(rc_condition_t* condition, int* in_pause) { + if (condition->next != 0) { + rc_update_condition_pause(condition->next, in_pause); + } + + switch (condition->type) { + case RC_CONDITION_PAUSE_IF: + *in_pause = condition->pause = 1; + break; + + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_ADD_ADDRESS: + condition->pause = *in_pause; + break; + + default: + *in_pause = condition->pause = 0; + break; + } +} + +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_pause; + int in_add_address; + unsigned measured_target = 0; + + self = RC_ALLOC(rc_condset_t, parse); + self->has_pause = self->is_paused = 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_HITS: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + break; + + case RC_CONDITION_MEASURED: + 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; + + 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; + if ((*next)->oper != RC_OPERATOR_NONE) + (*next)->required_hits = measured_target; + } + 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 { + 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) { + in_pause = 0; + rc_update_condition_pause(self->conditions, &in_pause); + } + + return self; +} + +static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc_eval_state_t* eval_state) { + rc_condition_t* condition; + int set_valid, cond_valid, and_next, or_next, reset_next; + unsigned measured_value = 0; + unsigned total_hits = 0; + int can_measure = 1, measured_from_hits = 0; + + eval_state->primed = 1; + set_valid = 1; + and_next = 1; + or_next = 0; + reset_next = 0; + eval_state->add_value = 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: + eval_state->add_value += rc_evaluate_condition_value(condition, eval_state); + eval_state->add_address = 0; + continue; + + case RC_CONDITION_SUB_SOURCE: + eval_state->add_value -= rc_evaluate_condition_value(condition, eval_state); + eval_state->add_address = 0; + continue; + + case RC_CONDITION_ADD_ADDRESS: + eval_state->add_address = rc_evaluate_condition_value(condition, eval_state); + continue; + + case RC_CONDITION_MEASURED: + if (condition->required_hits == 0) { + /* Measured condition without a hit target measures the value of the left operand */ + measured_value = rc_evaluate_condition_value(condition, eval_state) + eval_state->add_value; + } + break; + + default: + break; + } + + /* STEP 2: evaluate the current condition */ + condition->is_true = rc_test_condition(condition, eval_state); + eval_state->add_value = 0; + 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) { + 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; + measured_value = total_hits; + } + break; + + case RC_CONDITION_MEASURED_IF: + if (!cond_valid) + 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 not suppressed, update the measured value */ + if (measured_value > eval_state->measured_value && can_measure) { + eval_state->measured_value = measured_value; + eval_state->measured_from_hits = 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) { + if ((self->is_paused = rc_test_condset_internal(self, 1, eval_state))) { + /* one or more Pause conditions exists, if any of them are true, stop processing this group */ + 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/dep/rcheevos/src/rcheevos/consoleinfo.c b/dep/rcheevos/src/rcheevos/consoleinfo.c new file mode 100644 index 000000000..053cc82f9 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/consoleinfo.c @@ -0,0 +1,659 @@ +#include "rcheevos.h" +#include "rc_consoles.h" + +#include + +const char* rc_console_name(int 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_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_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_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_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_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_3DS: + return "Nintendo 3DS"; + + case RC_CONSOLE_NOKIA_NGAGE: + return "Nokia N-Gage"; + + case RC_CONSOLE_ORIC: + return "Oric"; + + 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 "PCEngine"; + + 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_SATURN: + return "Sega Saturn"; + + case RC_CONSOLE_SG1000: + return "SG-1000"; + + case RC_CONSOLE_SUPER_NINTENDO: + return "Super Nintendo Entertainment System"; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return "Super CassetteVision"; + + case RC_CONSOLE_WONDERSWAN: + return "WonderSwan"; + + case RC_CONSOLE_VECTREX: + return "Vectrex"; + + case RC_CONSOLE_VIC20: + return "VIC-20"; + + case RC_CONSOLE_VIRTUAL_BOY: + return "Virtual Boy"; + + case RC_CONSOLE_WII: + return "Wii"; + + case RC_CONSOLE_WII_U: + return "Wii-U"; + + case RC_CONSOLE_X68K: + return "X68K"; + + case RC_CONSOLE_XBOX: + return "XBOX"; + + 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 }; + +/* ===== 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 }; + +/* ===== 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 }; + +/* ===== 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 ===== */ +static const rc_memory_region_t _rc_memory_regions_gameboy_advance[] = { + { 0x000000U, 0x007FFFU, 0x03000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x008000U, 0x047FFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 2 }; + +/* ===== 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%3Ftitle%3DMemory_Map */ +static const rc_memory_region_t _rc_memory_regions_intellivision[] = { + { 0x000000U, 0x00007FU, 0x000000U, RC_MEMORY_TYPE_VIDEO_RAM, "STIC Registers" }, + { 0x000080U, 0x0000FFU, 0x000080U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x000100U, 0x00035FU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x000360U, 0x0003FFU, 0x000360U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x000400U, 0x000FFFU, 0x000400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x002000U, 0x002FFFU, 0x002000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x003000U, 0x003FFFU, 0x003000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x004000U, 0x00FFFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_intellivision = { _rc_memory_regions_intellivision, 9 }; + +/* ===== Magnavox Odyssey 2 ===== */ +static const rc_memory_region_t _rc_memory_regions_magnavox_odyssey_2[] = { + { 0x000000U, 0x00003FU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_magnavox_odyssey_2 = { _rc_memory_regions_magnavox_odyssey_2, 1 }; + +/* ===== 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 }; + +/* ===== 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 }; + +/* ===== Neo Geo Pocket ===== */ +/* http://neopocott.emuunlim.com/docs/tech-11.txt */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = { + /* MednafenNGP exposes 16KB, but the doc suggests there's 24-32KB */ + { 0x000000U, 0x003FFFU, 0x000000U, 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 }; + +/* ===== 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 }; + +/* ===== 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_pcengine[] = { + { 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_pcengine = { _rc_memory_regions_pcengine, 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 }; + +/* ===== 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" } + /* TODO: should cartridge memory be exposed ($0000-$BFFF)? it's usually just ROM data, but may contain on-cartridge RAM + * This not is also concerning: http://www.smspower.org/Development/MemoryMap + * Cartridges may disable the system RAM and thus take over the full 64KB address space. */ +}; +static const rc_memory_regions_t rc_memory_regions_sg1000 = { _rc_memory_regions_sg1000, 1 }; + +/* ===== Super Cassette Vision ===== */ +static const rc_memory_region_t _rc_memory_regions_scv[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_READONLY, "System ROM" }, + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x008000U, 0x00DFFFU, 0x008000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM" }, + { 0x00E000U, 0x00FF7FU, 0x00E000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x00FF80U, 0x00FFFFU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_scv, 7 }; + +/* ===== 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 }; + +/* ===== 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 }; + +/* ===== 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(int console_id) +{ + switch (console_id) + { + case RC_CONSOLE_3DO: + return &rc_memory_regions_3do; + + case RC_CONSOLE_APPLE_II: + return &rc_memory_regions_appleii; + + 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: + 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_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_GAME_GEAR: + return &rc_memory_regions_game_gear; + + case RC_CONSOLE_INTELLIVISION: + return &rc_memory_regions_intellivision; + + 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: + case RC_CONSOLE_SEGA_32X: + /* NOTE: 32x adds an extra 512KB of memory (256KB RAM + 256KB VRAM) to the + * Genesis, but we currently don't support it. */ + return &rc_memory_regions_megadrive; + + case RC_CONSOLE_MSX: + return &rc_memory_regions_msx; + + case RC_CONSOLE_NEOGEO_POCKET: + return &rc_memory_regions_neo_geo_pocket; + + 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_ORIC: + return &rc_memory_regions_oric; + + case RC_CONSOLE_PC8800: + return &rc_memory_regions_pc8800; + + case RC_CONSOLE_PC_ENGINE: + return &rc_memory_regions_pcengine; + + case RC_CONSOLE_PCFX: + return &rc_memory_regions_pcfx; + + case RC_CONSOLE_PLAYSTATION: + return &rc_memory_regions_playstation; + + 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_VECTREX: + return &rc_memory_regions_vectrex; + + case RC_CONSOLE_VIRTUAL_BOY: + return &rc_memory_regions_virtualboy; + + case RC_CONSOLE_WONDERSWAN: + return &rc_memory_regions_wonderswan; + + default: + return &rc_memory_regions_none; + } +} diff --git a/dep/rcheevos/src/rcheevos/format.c b/dep/rcheevos/src/rcheevos/format.c new file mode 100644 index 000000000..087129322 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/format.c @@ -0,0 +1,155 @@ +#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; + } + + break; + + case 'T': + if (!strcmp(format_str, "IME")) { + return RC_FORMAT_FRAMES; + } + else if (!strcmp(format_str, "IMESECS")) { + return RC_FORMAT_SECONDS; + } + + 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 'O': + if (!strcmp(format_str, "THER")) { + return RC_FORMAT_SCORE; + } + + break; + } + + return RC_FORMAT_VALUE; +} + +static int rc_format_value_minutes(char* buffer, int size, unsigned minutes) { + unsigned hours; + + hours = minutes / 60; + minutes -= hours * 60; + return snprintf(buffer, size, "%uh%02u", hours, minutes); +} + +static int rc_format_value_seconds(char* buffer, int size, unsigned seconds) { + unsigned hours, minutes; + + /* 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, int size, unsigned centiseconds) { + unsigned 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; +} + +int rc_format_value(char* buffer, int size, int value, int format) { + int chars; + + switch (format) { + case RC_FORMAT_FRAMES: + /* 60 frames per second = 100 centiseconds / 60 frames; multiply frames by 100 / 60 */ + chars = rc_format_value_centiseconds(buffer, size, value * 10 / 6); + break; + + case RC_FORMAT_SECONDS: + chars = rc_format_value_seconds(buffer, size, value); + break; + + case RC_FORMAT_CENTISECS: + chars = rc_format_value_centiseconds(buffer, size, value); + break; + + case RC_FORMAT_SECONDS_AS_MINUTES: + chars = rc_format_value_minutes(buffer, size, value / 60); + break; + + case RC_FORMAT_MINUTES: + chars = rc_format_value_minutes(buffer, size, value); + break; + + case RC_FORMAT_SCORE: + chars = snprintf(buffer, size, "%06d", value); + break; + + default: + case RC_FORMAT_VALUE: + chars = snprintf(buffer, size, "%d", value); + break; + } + + return chars; +} diff --git a/dep/rcheevos/src/rcheevos/lboard.c b/dep/rcheevos/src/rcheevos/lboard.c new file mode 100644 index 000000000..d78e1e49c --- /dev/null +++ b/dep/rcheevos/src/rcheevos/lboard.c @@ -0,0 +1,263 @@ +#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; + } + + found |= RC_LBOARD_START; + memaddr += 4; + rc_parse_trigger_internal(&self->start, &memaddr, parse); + self->start.memrefs = 0; + + if (parse->offset < 0) { + return; + } + } + 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; + } + + found |= RC_LBOARD_CANCEL; + memaddr += 4; + rc_parse_trigger_internal(&self->cancel, &memaddr, parse); + self->cancel.memrefs = 0; + + if (parse->offset < 0) { + return; + } + } + 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; + } + + found |= RC_LBOARD_SUBMIT; + memaddr += 4; + rc_parse_trigger_internal(&self->submit, &memaddr, parse); + self->submit.memrefs = 0; + + if (parse->offset < 0) { + return; + } + } + 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; + } + + found |= RC_LBOARD_VALUE; + memaddr += 4; + rc_parse_value_internal(&self->value, &memaddr, parse); + self->value.memrefs = 0; + + if (parse->offset < 0) { + return; + } + } + 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; + } + + found |= RC_LBOARD_PROGRESS; + memaddr += 4; + + self->progress = RC_ALLOC(rc_value_t, parse); + rc_parse_value_internal(self->progress, &memaddr, parse); + self->progress->memrefs = 0; + + if (parse->offset < 0) { + return; + } + } + else { + parse->offset = RC_INVALID_LBOARD_FIELD; + return; + } + + if (memaddr[0] != ':' || memaddr[1] != ':') { + break; + } + + 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; + 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, int* 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; +} + +void rc_reset_lboard(rc_lboard_t* self) { + 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/dep/rcheevos/src/rcheevos/memref.c b/dep/rcheevos/src/rcheevos/memref.c new file mode 100644 index 000000000..e6bdd331d --- /dev/null +++ b/dep/rcheevos/src/rcheevos/memref.c @@ -0,0 +1,225 @@ +#include "rc_internal.h" + +#include /* malloc/realloc */ +#include /* memcpy */ + +#define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF + +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect) { + rc_memref_t** 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, char* size, unsigned* address) { + const char* aux = *memaddr; + char* end; + unsigned long value; + + if (*aux++ != '0') + return RC_INVALID_MEMORY_OPERAND; + + if (*aux != 'x' && *aux != 'X') + return RC_INVALID_MEMORY_OPERAND; + aux++; + + switch (*aux++) { + 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 'h': case 'H': *size = RC_MEMSIZE_8_BITS; break; + case 'w': case 'W': *size = RC_MEMSIZE_24_BITS; break; + case 'x': case 'X': *size = RC_MEMSIZE_32_BITS; break; + + 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': + aux--; + /* fallthrough */ + case ' ': + *size = RC_MEMSIZE_16_BITS; + break; + + default: + return RC_INVALID_MEMORY_OPERAND; + } + + value = strtoul(aux, &end, 16); + + if (end == aux) + return RC_INVALID_MEMORY_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + *address = (unsigned)value; + *memaddr = end; + return RC_OK; +} + +static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { + unsigned value; + + if (!peek) + return 0; + + switch (size) + { + case RC_MEMSIZE_BIT_0: + value = (peek(address, 1, ud) >> 0) & 1; + break; + + case RC_MEMSIZE_BIT_1: + value = (peek(address, 1, ud) >> 1) & 1; + break; + + case RC_MEMSIZE_BIT_2: + value = (peek(address, 1, ud) >> 2) & 1; + break; + + case RC_MEMSIZE_BIT_3: + value = (peek(address, 1, ud) >> 3) & 1; + break; + + case RC_MEMSIZE_BIT_4: + value = (peek(address, 1, ud) >> 4) & 1; + break; + + case RC_MEMSIZE_BIT_5: + value = (peek(address, 1, ud) >> 5) & 1; + break; + + case RC_MEMSIZE_BIT_6: + value = (peek(address, 1, ud) >> 6) & 1; + break; + + case RC_MEMSIZE_BIT_7: + value = (peek(address, 1, ud) >> 7) & 1; + break; + + case RC_MEMSIZE_LOW: + value = peek(address, 1, ud) & 0x0f; + break; + + case RC_MEMSIZE_HIGH: + value = (peek(address, 1, ud) >> 4) & 0x0f; + break; + + case RC_MEMSIZE_8_BITS: + value = peek(address, 1, ud); + break; + + case RC_MEMSIZE_16_BITS: + value = peek(address, 2, ud); + break; + + case RC_MEMSIZE_24_BITS: + /* peek 4 bytes - don't expect the caller to understand 24-bit numbers */ + value = peek(address, 4, ud) & 0x00FFFFFF; + break; + + case RC_MEMSIZE_32_BITS: + value = peek(address, 4, ud); + break; + + default: + value = 0; + break; + } + + return value; +} + +void rc_update_memref_value(rc_memref_value_t* memref, unsigned 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; +} + +unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state) { + /* if this is an indirect reference, handle the indirection. */ + if (memref->value.is_indirect) { + const unsigned new_address = memref->address + eval_state->add_address; + 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); +} + +unsigned rc_get_memref_value_value(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; + } +} diff --git a/dep/rcheevos/src/rcheevos/operand.c b/dep/rcheevos/src/rcheevos/operand.c new file mode 100644 index 000000000..4a76e0f30 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/operand.c @@ -0,0 +1,470 @@ +#include "rc_internal.h" + +#include +#include +#include + +#ifndef RC_DISABLE_LUA + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifdef __cplusplus +} +#endif + +#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(*aux)) { + return RC_INVALID_LUA_OPERAND; + } + +#ifndef RC_DISABLE_LUA + id = aux; +#endif + + while (isalnum(*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); + } + +#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, int is_indirect) { + const char* aux = *memaddr; + unsigned address; + char 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; + + switch (self->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: + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + case RC_MEMSIZE_BITCOUNT: + /* these can all share an 8-bit memref and just mask off the appropriate data in rc_evaluate_operand */ + size = RC_MEMSIZE_8_BITS; + break; + + default: + size = self->size; + break; + } + + 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, int is_trigger, int is_indirect, rc_parse_state_t* parse) { + const char* aux = *memaddr; + char* end; + int ret; + unsigned long value; + int negative; + + 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 */ + self->value.dbl = strtod(++aux, &end); + + if (end == aux) { + return RC_INVALID_FP_OPERAND; + } + + if (floor(self->value.dbl) == self->value.dbl) { + self->type = RC_OPERAND_CONST; + self->value.num = (unsigned)floor(self->value.dbl); + } + else { + self->type = RC_OPERAND_FP; + } + + aux = end; + break; + + case 'v': case 'V': /* signed integer constant */ + ++aux; + /* fallthrough */ + case '+': case '-': /* signed integer constant */ + negative = 0; + if (*aux == '-') + { + negative = 1; + ++aux; + } + else if (*aux == '+') + { + ++aux; + } + + value = strtoul(aux, &end, 10); + + if (end == aux) { + return RC_INVALID_CONST_OPERAND; + } + + if (value > 0x7fffffffU) { + value = 0x7fffffffU; + } + + self->type = RC_OPERAND_CONST; + + if (negative) + self->value.num = (unsigned)(-((long)value)); + else + self->value.num = (unsigned)value; + + aux = end; + break; + + case '0': + if (aux[1] == 'x' || aux[1] == 'X') { /* hex integer constant */ + /* fall through */ + default: + ret = rc_parse_operand_memory(self, &aux, parse, is_indirect); + + if (ret < 0) { + return ret; + } + + break; + } + + /* fall through 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) { + unsigned address = (unsigned)luaL_checkinteger(L, 1); + unsigned num_bytes = (unsigned)luaL_checkinteger(L, 2); + rc_luapeek_t* luapeek = (rc_luapeek_t*)lua_touserdata(L, 3); + + unsigned value = luapeek->peek(address, num_bytes, luapeek->ud); + + lua_pushinteger(L, value); + return 1; +} + +#endif /* RC_DISABLE_LUA */ + +static const unsigned char rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; + +unsigned rc_evaluate_operand(rc_operand_t* self, rc_eval_state_t* eval_state) { +#ifndef RC_DISABLE_LUA + rc_luapeek_t luapeek; +#endif /* RC_DISABLE_LUA */ + + unsigned value; + + /* step 1: read memory */ + switch (self->type) { + case RC_OPERAND_CONST: + return self->value.num; + + case RC_OPERAND_FP: + /* This is handled by rc_evaluate_condition_value. */ + return 0; + + case RC_OPERAND_LUA: + value = 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)) { + value = lua_toboolean(eval_state->L, -1); + } + else { + value = (unsigned)lua_tonumber(eval_state->L, -1); + } + } + + lua_pop(eval_state->L, 1); + } + +#endif /* RC_DISABLE_LUA */ + + break; + + default: + value = rc_get_memref_value(self->value.memref, self->type, eval_state); + break; + } + + /* step 2: mask off appropriate bits */ + switch (self->size) + { + case RC_MEMSIZE_BIT_0: + value = (value >> 0) & 1; + break; + + case RC_MEMSIZE_BIT_1: + value = (value >> 1) & 1; + break; + + case RC_MEMSIZE_BIT_2: + value = (value >> 2) & 1; + break; + + case RC_MEMSIZE_BIT_3: + value = (value >> 3) & 1; + break; + + case RC_MEMSIZE_BIT_4: + value = (value >> 4) & 1; + break; + + case RC_MEMSIZE_BIT_5: + value = (value >> 5) & 1; + break; + + case RC_MEMSIZE_BIT_6: + value = (value >> 6) & 1; + break; + + case RC_MEMSIZE_BIT_7: + value = (value >> 7) & 1; + break; + + case RC_MEMSIZE_LOW: + value = value & 0x0f; + break; + + case RC_MEMSIZE_HIGH: + value = (value >> 4) & 0x0f; + break; + + case RC_MEMSIZE_BITCOUNT: + value = rc_bits_set[(value & 0x0F)] + + rc_bits_set[((value >> 4) & 0x0F)]; + break; + + default: + break; + } + + /* step 3: apply logic */ + 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: + value = ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_24_BITS: + 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_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: + value ^= 0xffff; + break; + + case RC_MEMSIZE_24_BITS: + value ^= 0xffffff; + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_VARIABLE: + value ^= 0xffffffff; + break; + + default: + value ^= 0x01; + break; + } + break; + + default: + break; + } + + return value; +} diff --git a/dep/rcheevos/src/rcheevos/rc_compat.h b/dep/rcheevos/src/rcheevos/rc_compat.h new file mode 100644 index 000000000..62c362f47 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_compat.h @@ -0,0 +1,60 @@ +#ifndef RC_COMPAT_H +#define RC_COMPAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#if defined(MINGW) || defined(__MINGW32__) || defined(__MINGW64__) + +/* MinGW redefinitions */ + +#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 */ + +#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 */ + +#ifdef __cplusplus +} +#endif + +#endif /* RC_COMPAT_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_internal.h b/dep/rcheevos/src/rcheevos/rc_internal.h new file mode 100644 index 000000000..6b4c27d48 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_internal.h @@ -0,0 +1,147 @@ +#ifndef INTERNAL_H +#define INTERNAL_H + +#include "rcheevos.h" + +#ifdef __cplusplus +extern "C" { +#endif + +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))) + +typedef struct rc_scratch_buffer { + struct rc_scratch_buffer* next; + int offset; + unsigned char buffer[512 - 16]; +} +rc_scratch_buffer_t; + +typedef struct { + rc_scratch_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; + +typedef struct { + unsigned add_value; /* AddSource/SubSource */ + int add_hits; /* AddHits */ + unsigned add_address; /* AddAddress */ + + rc_peek_t peek; + void* peek_userdata; + lua_State* L; + + unsigned measured_value; /* Measured */ + char was_reset; /* ResetIf triggered */ + char has_hits; /* one of more hit counts is non-zero */ + char primed; /* true if all non-Trigger conditions are true */ + char measured_from_hits; /* true if the measured_value came from a condition's hit count */ + char was_cond_reset; /* ResetNextIf triggered */ +} +rc_eval_state_t; + +typedef struct { + int offset; + + lua_State* L; + int funcs_ndx; + + void* buffer; + rc_scratch_t scratch; + + rc_memref_t** first_memref; + rc_value_t** variables; + + unsigned measured_target; + + char has_required_hits; +} +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, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); +void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length); + +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect); +int rc_parse_memref(const char** memaddr, char* size, unsigned* address); +void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud); +void rc_update_memref_value(rc_memref_value_t* memref, unsigned value); +unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state); +unsigned rc_get_memref_value_value(rc_memref_value_t* memref, int operand_type); + +void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); + +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); + +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect); +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); +int rc_evaluate_condition_value(rc_condition_t* self, rc_eval_state_t* eval_state); + +int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_trigger, int is_indirect, rc_parse_state_t* parse); +unsigned rc_evaluate_operand(rc_operand_t* self, rc_eval_state_t* eval_state); +char rc_parse_operator(const char** memaddr); + +void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); +void rc_reset_value(rc_value_t* self); +rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse); +void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L); + +void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse); + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse); + +#ifdef __cplusplus +} +#endif + +#endif /* INTERNAL_H */ diff --git a/dep/rcheevos/src/rcheevos/richpresence.c b/dep/rcheevos/src/rcheevos/richpresence.c new file mode 100644 index 000000000..645ff181d --- /dev/null +++ b/dep/rcheevos/src/rcheevos/richpresence.c @@ -0,0 +1,684 @@ +#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 +}; + +static rc_memref_value_t* rc_alloc_helper_variable_memref_value(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { + const char* end; + rc_value_t* variable; + unsigned address; + char size; + + /* 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]) { + /* just a memory reference, allocate it */ + 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) { + const char* nextline; + const char* endline; + + /* get a single line */ + nextline = line; + while (*nextline && *nextline != '\n') + ++nextline; + + /* find a trailing comment marker (//) */ + endline = line; + while (endline < nextline && (endline[0] != '/' || endline[1] != '/' || (endline > line && endline[-1] == '\\'))) + ++endline; + + /* remove trailing whitespace */ + if (endline == nextline) { + if (endline > line && endline[-1] == '\r') + --endline; + } else { + while (endline > line && isspace(endline[-1])) + --endline; + } + + /* end is pointing at the first character to ignore - makes subtraction for length easier */ + *end = endline; + + if (*nextline == '\n') + ++nextline; + return nextline; +} + +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 */ + line = ++ptr; + while (ptr < endline && *ptr != '(') + ++ptr; + + if (ptr == endline) { + parse->offset = RC_MISSING_VALUE; + return 0; + } + + if (ptr > line) { + /* find the lookup and hook it up */ + lookup = first_lookup; + while (lookup) { + if (strncmp(lookup->name, line, ptr - line) == 0 && lookup->name[ptr - line] == '\0') { + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + *next = part; + next = &part->next; + + part->text = lookup->name; + part->lookup = lookup; + part->display_type = lookup->format; + + in = line; + line = ++ptr; + while (ptr < endline && *ptr != ')') + ++ptr; + if (*ptr == ')') { + part->value = rc_alloc_helper_variable_memref_value(line, (int)(ptr-line), parse); + if (parse->offset < 0) + return 0; + ++ptr; + } + else { + /* 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)); + } + + break; + } + + lookup = lookup->next; + } + + if (!lookup) { + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + /* find the closing parenthesis */ + while (ptr < endline && *ptr != ')') + ++ptr; + if (*ptr == ')') + ++ptr; + + /* assert: the allocated string is going to be smaller than the memory used for the parameter of the macro */ + part->display_type = RC_FORMAT_UNKNOWN_MACRO; + part->text = rc_alloc_str(parse, line, (int)(ptr - line)); + } + } + } + + 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; + rc_scratch_buffer_t* buffer; + const int alignment = sizeof(rc_richpresence_lookup_item_t*); + 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 - prefer scratch memory if available */ + size = count * sizeof(rc_richpresence_lookup_item_t*); + buffer = &parse->scratch.buffer; + do { + const int aligned_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const int remaining = sizeof(buffer->buffer) - aligned_offset; + + if (remaining >= size) { + items = (rc_richpresence_lookup_item_t**)&buffer->buffer[aligned_offset]; + break; + } + + buffer = buffer->next; + if (buffer == NULL) { + /* could not find large enough block of scratch memory; allocate. if allocation fails, + * we can still use the unbalanced tree, so just bail out */ + items = (rc_richpresence_lookup_item_t**)malloc(size); + if (items == NULL) + return; + + break; + } + } while (1); + + /* 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); + + if (buffer == NULL) + free(items); +} + +static void rc_insert_richpresence_lookup_item(rc_richpresence_lookup_t* lookup, + unsigned first, unsigned last, const char* label, int label_len, rc_parse_state_t* parse) +{ + 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; + unsigned first, last; + int base; + + do + { + line = nextline; + nextline = rc_parse_line(line, &endline); + + 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 = 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 = 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 chars; + + /* first pass: process macro initializers */ + line = script; + while (*line) + { + nextline = rc_parse_line(line, &endline); + 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); + 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 = rc_parse_format(format); + } else { + lookup->format = RC_FORMAT_VALUE; + } + } else if (strncmp(line, "Display:", 8) == 0) { + display = nextline; + + do { + line = nextline; + nextline = rc_parse_line(line, &endline); + } while (*line == '?'); + } + + line = nextline; + } + + *nextlookup = 0; + self->first_lookup = firstlookup; + + nextdisplay = &self->first_display; + + /* second pass, process display string*/ + if (display) { + line = display; + nextline = rc_parse_line(line, &endline); + + while (*line == '?') { + /* 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); + } + + line = nextline; + nextline = rc_parse_line(line, &endline); + } + + /* non-conditional display: string */ + *nextdisplay = rc_parse_richpresence_display_internal(line, endline, parse, firstlookup); + if (*nextdisplay) { + hasdisplay = 1; + nextdisplay = &((*nextdisplay)->next); + } + } + + /* finalize */ + *nextdisplay = 0; + + if (!hasdisplay && parse->offset > 0) { + parse->offset = RC_MISSING_DISPLAY_STRING; + } +} + +int rc_richpresence_size(const char* script) { + 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); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +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; + 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 : 0; +} + +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, unsigned buffersize) +{ + rc_richpresence_lookup_item_t* item; + char tmp[256]; + char* ptr = buffer; + const char* text; + size_t chars; + unsigned value; + + *ptr = '\0'; + while (part) { + switch (part->display_type) { + case RC_FORMAT_STRING: + text = part->text; + chars = strlen(text); + break; + + case RC_FORMAT_LOOKUP: + value = part->value->value; + text = part->lookup->default_label; + item = part->lookup->root; + while (item) { + if (value > item->last) { + item = item->right; + } + else if (value < item->first) { + item = item->left; + } + else { + text = item->label; + break; + } + } + + chars = strlen(text); + break; + + case RC_FORMAT_UNKNOWN_MACRO: + chars = snprintf(tmp, sizeof(tmp), "[Unknown macro]%s", part->text); + text = tmp; + break; + + default: + value = part->value->value; + chars = rc_format_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, unsigned 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, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { + rc_update_richpresence(richpresence, peek, peek_ud, L); + return rc_get_richpresence_display_string(richpresence, buffer, buffersize, peek, peek_ud, L); +} diff --git a/dep/rcheevos/src/rcheevos/runtime.c b/dep/rcheevos/src/rcheevos/runtime.c new file mode 100644 index 000000000..52c63281e --- /dev/null +++ b/dep/rcheevos/src/rcheevos/runtime.c @@ -0,0 +1,691 @@ +#include "rc_internal.h" + +#include "../rhash/md5.h" + +#include +#include + +#define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 + +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) { + unsigned 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; +} + +static void rc_runtime_checksum(const char* memaddr, unsigned char* 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, unsigned 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, unsigned id) { + unsigned 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, unsigned id, const char* memaddr, lua_State* L, int funcs_idx) { + void* trigger_buffer; + rc_trigger_t* trigger; + rc_runtime_trigger_t* runtime_trigger; + rc_parse_state_t parse; + unsigned char md5[16]; + int size; + unsigned 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, unsigned id) +{ + unsigned 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; +} + +static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, unsigned 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, unsigned id) { + unsigned 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, unsigned id, const char* memaddr, lua_State* L, int funcs_idx) { + void* lboard_buffer; + unsigned char md5[16]; + rc_lboard_t* lboard; + rc_parse_state_t parse; + int size; + unsigned 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 */ + self->lboards[self->lboard_count].id = id; + self->lboards[self->lboard_count].value = 0; + self->lboards[self->lboard_count].lboard = lboard; + self->lboards[self->lboard_count].buffer = lboard_buffer; + self->lboards[self->lboard_count].invalid_memref = NULL; + memcpy(self->lboards[self->lboard_count].md5, md5, 16); + self->lboards[self->lboard_count].owns_memrefs = rc_runtime_allocated_memrefs(self); + ++self->lboard_count; + + /* 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, unsigned id) +{ + unsigned 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_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_richpresence_display_t* display; + rc_parse_state_t parse; + int size; + + if (script == NULL) + return RC_MISSING_DISPLAY_STRING; + + size = rc_richpresence_size(script); + if (size < 0) + return size; + + previous = self->richpresence; + if (previous) { + if (!previous->owns_memrefs) { + free(previous->buffer); + previous = previous->previous; + } + } + + 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; + 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 */ + display = richpresence->first_display; + while (display != NULL) { + rc_reset_trigger(&display->trigger); + display = display->next; + } + } + + return RC_OK; +} + +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, unsigned buffersize, rc_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_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 trigger_state; + + 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; + } + + trigger_state = trigger->state; + switch (rc_evaluate_trigger(trigger, peek, ud, L)) + { + case RC_TRIGGER_STATE_RESET: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_RESET; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + 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: + if (trigger_state != 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: + if (trigger_state != 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: + if (trigger_state != RC_TRIGGER_STATE_ACTIVE) { + 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; + unsigned 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) { + rc_richpresence_display_t* display = self->richpresence->richpresence->first_display; + while (display != 0) { + rc_reset_trigger(&display->trigger); + display = display->next; + } + } + + 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 (cond->operand1.value.memref == memref || cond->operand2.value.memref == memref) + return 1; + } + + return 0; +} + +static 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; +} + +static 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; +} + +void rc_runtime_invalidate_address(rc_runtime_t* self, unsigned address) { + unsigned i; + rc_memref_t* memref; + rc_memref_t** last_memref; + + if (!self->memrefs) + return; + + /* 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 = &self->memrefs; + memref = *last_memref; + do { + if (memref->address == address && !memref->value.is_indirect) { + *last_memref = memref->next; + break; + } + + last_memref = &memref->next; + memref = *last_memref; + } while (memref); + + /* if the address is only used indirectly, don't disable anything dependent on it */ + if (!memref) + return; + + /* 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; + } + } + } +} diff --git a/dep/rcheevos/src/rcheevos/runtime_progress.c b/dep/rcheevos/src/rcheevos/runtime_progress.c new file mode 100644 index 000000000..47e6b337f --- /dev/null +++ b/dep/rcheevos/src/rcheevos/runtime_progress.c @@ -0,0 +1,533 @@ +#include "rc_internal.h" + +#include "../rhash/md5.h" + +#include + +#define RC_RUNTIME_MARKER 0x0A504152 /* RAP\n */ + +#define RC_RUNTIME_CHUNK_MEMREFS 0x4645524D /* MREF */ +#define RC_RUNTIME_CHUNK_ACHIEVEMENT 0x56484341 /* ACHV */ + +#define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ + +typedef struct rc_runtime_progress_t { + rc_runtime_t* runtime; + + int offset; + unsigned char* buffer; + + int 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_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, unsigned 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 unsigned rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) +{ + unsigned 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, unsigned char* 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, unsigned char* 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, unsigned 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) +{ + unsigned length; + int offset; + + progress->offset = (progress->offset + 3) & ~0x03; /* align to 4 byte boundary */ + + if (progress->buffer) { + /* ignore chunk size field when calculating chunk size */ + length = (unsigned)(progress->offset - progress->chunk_size_offset - 4); + + /* 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, 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; + unsigned int 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) +{ + unsigned entries; + unsigned address, flags, value, prior; + char 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; + unsigned 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; + unsigned flags; + + condset->is_paused = 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) { + 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) { + 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 int rc_runtime_progress_write_trigger(rc_runtime_progress_t* progress, 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 = 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) +{ + unsigned 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; + + switch (runtime_trigger->trigger->state) + { + case RC_TRIGGER_STATE_DISABLED: + case RC_TRIGGER_STATE_INACTIVE: + case RC_TRIGGER_STATE_TRIGGERED: + /* don't store state for inactive or triggered achievements */ + continue; + + default: + break; + } + + 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) +{ + unsigned id = rc_runtime_progress_read_uint(progress); + unsigned 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_serialize_internal(rc_runtime_progress_t* progress) +{ + md5_state_t state; + unsigned char 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_achievements(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, (rc_runtime_t*)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; + + rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + progress.buffer = (unsigned char*)buffer; + + return rc_runtime_progress_serialize_internal(&progress); +} + +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* serialized, lua_State* L) +{ + rc_runtime_progress_t progress; + md5_state_t state; + unsigned char md5[16]; + unsigned chunk_id; + unsigned chunk_size; + unsigned next_chunk_offset; + unsigned i; + int result = RC_OK; + + rc_runtime_progress_init(&progress, runtime, L); + progress.buffer = (unsigned char*)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) { + switch (runtime_trigger->trigger->state) + { + case RC_TRIGGER_STATE_DISABLED: + case RC_TRIGGER_STATE_INACTIVE: + case RC_TRIGGER_STATE_TRIGGERED: + /* don't update state for inactive or triggered achievements */ + break; + + default: + /* 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; + break; + } + } + } + + 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_ACHIEVEMENT: + result = rc_runtime_progress_read_achievement(&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); + } + } + + return result; +} diff --git a/dep/rcheevos/src/rcheevos/trigger.c b/dep/rcheevos/src/rcheevos/trigger.c new file mode 100644 index 000000000..b952804bb --- /dev/null +++ b/dep/rcheevos/src/rcheevos/trigger.c @@ -0,0 +1,224 @@ +#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; + + 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_value = 0; + self->measured_target = parse->measured_target; + 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; + 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 : 0; +} + +static void rc_reset_trigger_hitcounts(rc_trigger_t* self) { + rc_condset_t* condset; + + if (self->requirement != 0) { + rc_reset_condset(self->requirement); + } + + condset = self->alternative; + + while (condset != 0) { + 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; + + /* previously triggered, do nothing - return INACTIVE so caller doesn't think it triggered again */ + if (self->state == RC_TRIGGER_STATE_TRIGGERED) + return RC_TRIGGER_STATE_INACTIVE; + + /* unsupported, do nothing - return INACTIVE */ + if (self->state == RC_TRIGGER_STATE_DISABLED) + return RC_TRIGGER_STATE_INACTIVE; + + /* update the memory references */ + rc_update_memref_values(self->memrefs, peek, ud); + + /* not yet active, only update the memrefs so deltas are correct when it becomes active */ + if (self->state == RC_TRIGGER_STATE_INACTIVE) + return RC_TRIGGER_STATE_INACTIVE; + + /* 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 != 0); + + /* 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) + self->measured_value = eval_state.measured_value; + + /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ + /* otherwise, if the state is WAITING, proceed to activating the trigger */ + if (self->state == RC_TRIGGER_STATE_WAITING && ret) { + rc_reset_trigger(self); + self->has_hits = 0; + return RC_TRIGGER_STATE_WAITING; + } + + if (eval_state.was_reset) { + /* if any ResetIf condition was true, reset the hit counts */ + rc_reset_trigger_hitcounts(self); + + /* if the measured value came from a hit count, reset it too */ + if (eval_state.measured_from_hits) + self->measured_value = 0; + + /* if there were hit counts to clear, return RESET, but don't change the state */ + if (self->has_hits) { + self->has_hits = 0; + return RC_TRIGGER_STATE_RESET; + } + + /* any hits that were tallied were just reset */ + eval_state.has_hits = 0; + is_primed = 0; + } + else if (ret) { + /* 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) { + rc_reset_trigger_hitcounts(self); + + self->state = RC_TRIGGER_STATE_WAITING; + self->measured_value = 0; + self->has_hits = 0; +} diff --git a/dep/rcheevos/src/rcheevos/value.c b/dep/rcheevos/src/rcheevos/value.c new file mode 100644 index 000000000..2251e27ad --- /dev/null +++ b/dep/rcheevos/src/rcheevos/value.c @@ -0,0 +1,310 @@ +#include "rc_internal.h" + +#include /* memset */ +#include /* isdigit */ + +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; + int end_of_clause; + + /* convert legacy format into condset */ + self->conditions = RC_ALLOC(rc_condset_t, parse); + self->conditions->has_pause = 0; + self->conditions->is_paused = 0; + + next = &self->conditions->conditions; + next_clause = &self->conditions->next; + + for (;;) { + ptr = &buffer[2]; + end_of_clause = 0; + + do { + switch (**memaddr) { + case '_': /* add next */ + case '$': /* maximum of */ + case '\0': /* end of string */ + case ':': /* end of leaderboard clause */ + case ')': /* end of rich presence macro */ + end_of_clause = 1; + *ptr = '\0'; + break; + + case '*': + *ptr++ = '*'; + + buffer_ptr = *memaddr + 1; + if (*buffer_ptr == '-') { + /* negative value automatically needs prefix, 'f' handles both float and digits, so use it */ + *ptr++ = 'f'; + } + else { + /* if it looks like a floating point number, add the 'f' prefix */ + while (isdigit(*buffer_ptr)) + ++buffer_ptr; + if (*buffer_ptr == '.') + *ptr++ = 'f'; + } + break; + + default: + *ptr++ = **memaddr; + break; + } + + ++(*memaddr); + } while (!end_of_clause); + + buffer_ptr = buffer; + cond = rc_parse_condition(&buffer_ptr, parse, 0); + if (parse->offset < 0) { + return; + } + + switch (cond->oper) { + case RC_OPERATOR_MULT: + case RC_OPERATOR_DIV: + case RC_OPERATOR_AND: + case RC_OPERATOR_NONE: + break; + + default: + parse->offset = RC_INVALID_OPERATOR; + return; + } + + cond->pause = 0; + *next = cond; + + switch ((*memaddr)[-1]) { + case '_': /* add next */ + next = &cond->next; + break; + + case '$': /* max of */ + cond->type = RC_CONDITION_MEASURED; + cond->next = 0; + *next_clause = RC_ALLOC(rc_condset_t, parse); + (*next_clause)->has_pause = 0; + (*next_clause)->is_paused = 0; + next = &(*next_clause)->conditions; + next_clause = &(*next_clause)->next; + break; + + default: /* end of valid string */ + --(*memaddr); /* undo the increment we performed when copying the string */ + cond->type = RC_CONDITION_MEASURED; + cond->next = 0; + *next_clause = 0; + return; + } + } +} + +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; + 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 : 0; +} + +int rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + int result = 0; + int paused = 1; + + rc_update_memref_values(self->memrefs, peek, ud); + + 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 = 0; + } + + if (paused) { + /* capture the first valid measurement */ + result = (int)eval_state.measured_value; + paused = 0; + } + 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 ((int)eval_state.measured_value > result) + result = (int)eval_state.measured_value; + } + } + + if (!paused) { + /* if not paused, store the value so that it's available when paused. */ + rc_update_memref_value(&self->value, result); + } + else { + /* when paused, the Measured value will not be captured, use the last captured value. */ + result = self->value.value; + } + + return result; +} + +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; +} + +void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { + parse->variables = variables; + *variables = 0; +} + +rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) +{ + rc_value_t** variables = parse->variables; + rc_value_t* value; + const char* name; + unsigned 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) { + while (variable) { + rc_evaluate_value(variable, peek, ud, L); + variable = variable->next; + } +} diff --git a/dep/rcheevos/src/rhash/cdreader.c b/dep/rcheevos/src/rhash/cdreader.c new file mode 100644 index 000000000..599662e9e --- /dev/null +++ b/dep/rcheevos/src/rhash/cdreader.c @@ -0,0 +1,782 @@ +#include "rc_hash.h" + +#include "../rcheevos/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, size_t offset, int origin); +extern size_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; + int sector_size; + int sector_header_size; + int first_sector_offset; + int first_sector; +}; + +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 unsigned char sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + }; + + unsigned char header[32]; + const int toc_sector = 16; + + cdrom->sector_size = 0; + cdrom->sector_header_size = 0; + + rc_file_seek(cdrom->file_handle, toc_sector * 2352 + cdrom->first_sector_offset, SEEK_SET); + rc_file_read(cdrom->file_handle, header, sizeof(header)); + + 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; + } + else + { + rc_file_seek(cdrom->file_handle, toc_sector * 2336 + cdrom->first_sector_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; + } + else + { + rc_file_seek(cdrom->file_handle, toc_sector * 2048 + cdrom->first_sector_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)); + cdrom->file_handle = file_handle; + + cdreader_determine_sector_size(cdrom); + + if (cdrom->sector_size == 0) + { + size_t size; + + rc_file_seek(cdrom->file_handle, 0, SEEK_END); + size = ftell(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; + } + } + + 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 size_t cdreader_get_bin_size(const char* cue_path, const char* bin_name) +{ + size_t size = 0; + char* bin_filename = cdreader_get_bin_path(cue_path, bin_name); + if (bin_filename) + { + void* 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); + } + + free(bin_filename); + } + + return size; +} + +static void* cdreader_open_cue_track(const char* path, uint32_t track) +{ + void* file_handle; + size_t file_offset = 0; + char buffer[1024], mode[16]; + char* bin_filename; + char file[256]; + char *ptr, *ptr2, *end; + int current_track = 0; + int sector_size = 0; + int track_first_sector = 0; + int previous_sector_size = 0; + int previous_index_sector_offset = 0; + int previous_track_is_data = 0; + int previous_track_sector_offset = 0; + char previous_track_mode[16]; + int largest_track = 0; + int largest_track_sector_count = 0; + int largest_track_offset = 0; + char largest_track_mode[16]; + char largest_track_file[256]; + int offset = 0; + int done = 0; + size_t num_read = 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; + + for (ptr = buffer; ptr < end; ++ptr) + { + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "INDEX ", 6) == 0) + { + int m = 0, s = 0, f = 0; + int index, sector_offset; + + ptr += 6; + index = atoi(ptr); + + while (*ptr != ' ' && *ptr != '\n') + ++ptr; + while (*ptr == ' ') + ++ptr; + + /* convert mm:ss:ff to sector count */ + sscanf(ptr, "%d:%d:%d", &m, &s, &f); + sector_offset = ((m * 60) + s) * 75 + f; + sector_offset -= previous_index_sector_offset; + + if (index == 1) + track_first_sector += sector_offset; + + /* if looking for the largest data track, determine previous track size */ + if (index == 1 && track == RC_HASH_CDTRACK_LARGEST && previous_track_is_data) + { + if (sector_offset > largest_track_sector_count) + { + largest_track_sector_count = sector_offset; + largest_track_offset = previous_track_sector_offset; + largest_track = current_track - 1; + memcpy(largest_track_mode, previous_track_mode, sizeof(largest_track_mode)); + strcpy(largest_track_file, file); + } + } + + /* calculate the true offset and update the counters for the next INDEX marker */ + offset += sector_offset * previous_sector_size; + previous_sector_size = sector_size; + previous_index_sector_offset += sector_offset; + + if (index == 1) + { + if (verbose_message_callback) + { + char message[128]; + char* scan = mode; + while (*scan && !isspace(*scan)) + ++scan; + *scan = '\0'; + + snprintf(message, sizeof(message), "Found %s track %d (sector size %d, track starts at %d)", mode, current_track, sector_size, offset); + verbose_message_callback(message); + } + + if (current_track == (int)track) + { + done = 1; + break; + } + + memcpy(previous_track_mode, mode, sizeof(previous_track_mode)); + previous_track_is_data = (memcmp(mode, "MODE", 4) == 0); + previous_track_sector_offset = offset; + + if (previous_track_is_data && track == RC_HASH_CDTRACK_FIRST_DATA) + { + track = current_track; + done = 1; + break; + } + } + } + else if (strncasecmp(ptr, "TRACK ", 6) == 0) + { + ptr += 6; + current_track = atoi(ptr); + + while (*ptr != ' ') + ++ptr; + while (*ptr == ' ') + ++ptr; + memcpy(mode, ptr, sizeof(mode)); + + previous_sector_size = sector_size; + + if (memcmp(mode, "MODE", 4) == 0) + { + sector_size = atoi(ptr + 6); + } + else + { + /* assume AUDIO */ + sector_size = 2352; + } + } + else if (strncasecmp(ptr, "FILE ", 5) == 0) + { + if (previous_sector_size > 0) + { + /* determine previous track size */ + int sector_count = (int)cdreader_get_bin_size(path, file) / previous_sector_size; + track_first_sector += sector_count; + + /* 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) + { + if (sector_count > largest_track_sector_count) + { + largest_track_sector_count = sector_count; + largest_track_offset = previous_track_sector_offset; + largest_track = current_track; + memcpy(largest_track_mode, previous_track_mode, sizeof(largest_track_mode)); + strcpy(largest_track_file, file); + } + } + } + + 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(file)) + { + memcpy(file, ptr, ptr2 - ptr); + file[ptr2 - ptr] = '\0'; + } + else + { + file[0] = '\0'; + } + + current_track = 0; + previous_sector_size = 0; + previous_index_sector_offset = 0; + offset = 0; + } + + while (*ptr && *ptr != '\n') + ++ptr; + } + + if (done) + break; + + file_offset += (ptr - buffer); + rc_file_seek(file_handle, file_offset, SEEK_SET); + + } while (1); + + rc_file_close(file_handle); + + if (track == RC_HASH_CDTRACK_LARGEST) + { + previous_track_is_data = (memcmp(mode, "MODE", 4) == 0); + if (previous_track_is_data) + { + int sector_count = (int)cdreader_get_bin_size(path, file) / previous_sector_size; + sector_count -= previous_index_sector_offset; + + if (sector_count > largest_track_sector_count) + { + largest_track_sector_count = sector_count; + largest_track_offset = previous_track_sector_offset; + largest_track = current_track; + memcpy(largest_track_mode, previous_track_mode, sizeof(largest_track_mode)); + strcpy(largest_track_file, file); + } + } + + if (largest_track > 0) + { + current_track = largest_track; + track = (uint32_t)largest_track; + offset = largest_track_offset; + memcpy(mode, largest_track_mode, sizeof(mode)); + strcpy(file, largest_track_file); + } + } + + if (current_track == (int)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->first_sector_offset = offset; + cdrom->first_sector = track_first_sector; + + /* verify existance of bin file */ + bin_filename = cdreader_get_bin_path(path, file); + if (bin_filename) + { + if (cdreader_open_bin(cdrom, bin_filename, mode)) + { + if (verbose_message_callback) + { + if (cdrom->first_sector_offset) + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d, track starts at %d)", track, cdrom->sector_size, cdrom->first_sector_offset); + else + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", track, cdrom->sector_size); + + verbose_message_callback((const char*)buffer); + } + } + 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]; + size_t track_size; + int track_type; + char* bin_path = ""; + uint32_t current_track = 0; + char* ptr, *ptr2, *end; + int lba = 0; + + uint32_t largest_track = 0; + size_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; + size_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] [?] */ + current_track = (uint32_t)atoi(ptr); + if (track && current_track != track) + continue; + + while (isdigit(*ptr)) + ++ptr; + ++ptr; + + lba = atoi(ptr); + while (isdigit(*ptr)) + ++ptr; + ++ptr; + + track_type = atoi(ptr); + while (isdigit(*ptr)) + ++ptr; + ++ptr; + + ptr2 = sector_size; + while (isdigit(*ptr)) + *ptr2++ = *ptr++; + *ptr2 = '\0'; + ++ptr; + + ptr2 = file; + if (*ptr == '\"') + { + ++ptr; + while (*ptr != '\"') + *ptr2++ = *ptr++; + ++ptr; + } + else + { + while (*ptr != ' ') + *ptr2++ = *ptr++; + } + *ptr2 = '\0'; + + if (track == current_track || (track == RC_HASH_CDTRACK_FIRST_DATA && track_type == 4)) + { + 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(largest_track_file, file); + strcpy(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(file, largest_track_file); + strcpy(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->first_sector_offset = 0; + cdrom->first_sector = lba; + + if (verbose_message_callback) + { + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", 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; + } + + 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) +{ + size_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; + + sector_start = sector * cdrom->sector_size + cdrom->sector_header_size + cdrom->first_sector_offset; + + while (requested_bytes > 2048) + { + rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, 2048); + total_read += num_read; + + if (num_read < 2048) + return total_read; + + buffer_ptr += 2048; + sector_start += cdrom->sector_size; + requested_bytes -= 2048; + } + + 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_absolute_sector_to_track_sector(void* track_handle, uint32_t sector) +{ + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (cdrom) + return sector - cdrom->first_sector; + + return 0; +} + +void rc_hash_init_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.absolute_sector_to_track_sector = cdreader_absolute_sector_to_track_sector; + + rc_hash_init_custom_cdreader(&cdreader); +} diff --git a/dep/rcheevos/src/rhash/hash.c b/dep/rcheevos/src/rhash/hash.c new file mode 100644 index 000000000..8d771ae7e --- /dev/null +++ b/dep/rcheevos/src/rhash/hash.c @@ -0,0 +1,1993 @@ +#include "rc_hash.h" + +#include "../rcheevos/rc_compat.h" + +#include "md5.h" + +#include + +/* 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 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) +{ + return fopen(path, "rb"); +} + +static void filereader_seek(void* file_handle, size_t offset, int origin) +{ + fseek((FILE*)file_handle, (long)offset, origin); +} + +static size_t filereader_tell(void* file_handle) +{ + return ftell((FILE*)file_handle); +} + +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); +} + +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); + + 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, size_t offset, int origin) +{ + if (filereader) + filereader->seek(file_handle, offset, origin); +} + +size_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_absolute_sector_to_track_sector(void* track_handle, uint32_t sector) +{ + if (cdreader && cdreader->absolute_sector_to_track_sector) + return cdreader->absolute_sector_to_track_sector(track_handle, sector); + + rc_hash_error("no hook registered for cdreader_absolute_sector_to_track_sector"); + return sector; +} + +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, unsigned* size) +{ + uint8_t buffer[2048], *tmp; + int sector; + size_t filename_length; + const char* slash; + + if (!track_handle) + return 0; + + 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 + { + /* find the cd information */ + if (!rc_cd_read_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); + } + + /* fetch and process the directory record */ + sector = rc_cd_absolute_sector_to_track_sector(track_handle, sector); + if (!rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) + return 0; + + tmp = buffer; + while (tmp < buffer + sizeof(buffer)) + { + if (!*tmp) + return 0; + + /* filename is 33 bytes into the record and the format is "FILENAME;version" or "DIRECTORY" */ + if ((tmp[33 + filename_length] == ';' || tmp[33 + filename_length] == '\0') && + strncasecmp((const char*)(tmp + 33), path, filename_length) == 0) + { + sector = tmp[2] | (tmp[3] << 8) | (tmp[4] << 16); + + if (verbose_message_callback) + { + snprintf((char*)buffer, sizeof(buffer), "Found %s at sector %d", path, sector); + verbose_message_callback((const char*)buffer); + } + + 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; + } + + 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 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], 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_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 = 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_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_lynx(char hash[33], 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_nes(char hash[33], 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_nintendo_ds(char hash[33], const char* path) +{ + uint8_t header[512]; + uint8_t* hash_buffer; + unsigned int hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; + size_t num_read; + int 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_pce(char hash[33], 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; + int sector, num_sectors; + unsigned 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 + */ + rc_cd_read_sector(track_handle, 1, buffer, 128); + + /* 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); + } + + 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 */ + rc_cd_read_sector(track_handle, 0, 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"); + + rc_cd_read_sector(track_handle, 0, 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, 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); + } + + 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, 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[2048]; + void* track_handle; + void* last_track_handle; + char exe_file[32] = ""; + unsigned size; + size_t num_read = 0; + 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) + return rc_hash_error("Could not open track"); + + /* 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, 0, buffer, sizeof(buffer)); + + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Dreamcast CD"); + } + + 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); + } + + /* remove whitespace from bootfile */ + i = 0; + while (!isspace(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); + + rc_cd_close_track(track_handle); + + if (sector == 0) + return rc_hash_error("Could not locate boot executable"); + + /* last track contains the boot executable */ + last_track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LAST); + + sector = rc_cd_absolute_sector_to_track_sector(last_track_handle, sector); + + if ((num_read = rc_cd_read_sector(last_track_handle, sector, buffer, sizeof(buffer))) < sizeof(buffer)) + rc_hash_error("Could not read boot executable"); + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %s contents (%u bytes)", exe_file, size); + verbose_message_callback(message); + } + + do + { + md5_append(&md5, buffer, (int)num_read); + + size -= (unsigned)num_read; + if (size == 0) + break; + + ++sector; + if (size >= sizeof(buffer)) + num_read = rc_cd_read_sector(last_track_handle, sector, buffer, sizeof(buffer)); + else + num_read = rc_cd_read_sector(last_track_handle, sector, buffer, size); + } while (num_read > 0); + + rc_cd_close_track(last_track_handle); + + result = rc_hash_finalize(&md5, hash); + + return result; +} + +static int rc_hash_psx(char hash[33], const char* path) +{ + uint8_t buffer[2048]; + char exe_name[64] = ""; + char* ptr; + char* start; + void* track_handle; + uint32_t sector; + unsigned size; + size_t num_read; + 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_cd_find_file_sector(track_handle, "SYSTEM.CNF", NULL); + if (!sector) + { + sector = rc_cd_find_file_sector(track_handle, "PSX.EXE", &size); + if (sector) + strcpy(exe_name, "PSX.EXE"); + } + else + { + size = (unsigned)rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer) - 1); + buffer[size] = '\0'; + + for (ptr = (char*)buffer; *ptr; ++ptr) + { + if (strncmp(ptr, "BOOT", 4) == 0) + { + ptr += 4; + while (isspace(*ptr)) + ++ptr; + + if (*ptr == '=') + { + ++ptr; + while (isspace(*ptr)) + ++ptr; + + if (strncmp(ptr, "cdrom:", 6) == 0) + ptr += 6; + if (*ptr == '\\') + ++ptr; + + start = ptr; + while (!isspace(*ptr) && *ptr != ';') + ++ptr; + + size = (unsigned)(ptr - start); + if (size >= sizeof(exe_name)) + size = sizeof(exe_name) - 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, &size); + break; + } + } + + /* advance to end of line */ + while (*ptr && *ptr != '\n') + ++ptr; + } + } + + if (!sector) + { + rc_hash_error("Could not locate primary executable"); + } + else if ((num_read = 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; + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %s title (%u bytes) and contents (%u bytes) ", exe_name, (unsigned)strlen(exe_name), size); + verbose_message_callback(message); + } + + /* there's also a few games that are 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 in the hash + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + do + { + md5_append(&md5, buffer, (int)num_read); + + size -= (unsigned)num_read; + if (size == 0) + break; + + ++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); + + result = rc_hash_finalize(&md5, hash); + } + + rc_cd_close_track(track_handle); + + return result; +} + +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_snes(char hash[33], 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); +} + +int rc_hash_generate_from_buffer(char hash[33], int console_id, 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_APPLE_II: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_7800: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + 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_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_MSX: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_NINTENDO_64: + 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_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WONDERSWAN: + return rc_hash_buffer(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_NINTENDO: + return rc_hash_snes(hash, buffer, buffer_size); + } +} + +static int rc_hash_whole_file(char hash[33], int console_id, const char* path) +{ + md5_state_t md5; + uint8_t* buffer; + size_t size; + const size_t buffer_size = 65536; + void* file_handle; + 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) + size = MAX_BUFFER_SIZE; + + md5_init(&md5); + + buffer = (uint8_t*)malloc(buffer_size); + if (buffer) + { + rc_file_seek(file_handle, 0, SEEK_SET); + while (size >= buffer_size) + { + rc_file_read(file_handle, buffer, (int)buffer_size); + md5_append(&md5, buffer, (int)buffer_size); + size -= buffer_size; + } + + if (size > 0) + { + rc_file_read(file_handle, buffer, (int)size); + md5_append(&md5, buffer, (int)size); + } + + free(buffer); + result = rc_hash_finalize(&md5, hash); + } + + rc_file_close(file_handle); + return result; +} + +static int rc_hash_buffered_file(char hash[33], int console_id, const char* path) +{ + uint8_t* buffer; + size_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); + 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); + + 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(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], int 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], int 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_APPLE_II: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_7800: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + 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_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_NINTENDO_64: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WONDERSWAN: + /* generic whole-file hash - don't buffer */ + return rc_hash_whole_file(hash, console_id, path); + + 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, console_id, path); + + case RC_CONSOLE_ATARI_LYNX: + case RC_CONSOLE_NINTENDO: + 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_NINTENDO_DS: + return rc_hash_nintendo_ds(hash, path); + + case RC_CONSOLE_PC_ENGINE: + 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_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_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, int 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 = 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; + } + 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_APPLE_II); +} + +void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, 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 '7': + if (rc_path_compare_extension(ext, "7z")) + { + /* decompressing zip file not supported */ + iterator->consoles[0] = RC_CONSOLE_ARCADE; + need_path = 1; + } + break; + + case 'a': + if (rc_path_compare_extension(ext, "a78")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_7800; + } + 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) + { + size_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_SEGA_CD; /* Genesis Plus GX supports directly opening the bin file*/ + + /* fallback to megadrive which just does a full hash */ + iterator->consoles[3] = RC_CONSOLE_MEGA_DRIVE; + break; + } + } + } + + /* bin is associated with MegaDrive, Sega32X and Atari 2600. 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_PC_ENGINE; + iterator->consoles[2] = RC_CONSOLE_3DO; + iterator->consoles[3] = RC_CONSOLE_PCFX; + iterator->consoles[4] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + need_path = 1; + } + else if (rc_path_compare_extension(ext, "chd")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + iterator->consoles[1] = RC_CONSOLE_DREAMCAST; + iterator->consoles[2] = RC_CONSOLE_PC_ENGINE; + iterator->consoles[3] = RC_CONSOLE_3DO; + iterator->consoles[4] = RC_CONSOLE_PCFX; + iterator->consoles[5] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + 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; + } + break; + + case 'd': + if (rc_path_compare_extension(ext, "dsk")) + { + rc_hash_initialize_dsk_iterator(iterator, path); + } + else if (rc_path_compare_extension(ext, "d88")) + { + iterator->consoles[0] = RC_CONSOLE_PC8800; + } + 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; + } + 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 'i': + if (rc_path_compare_extension(ext, "iso")) + { + iterator->consoles[0] = RC_CONSOLE_3DO; + iterator->consoles[1] = 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 '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; + } + 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; + } + 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; + } + break; + + case 'p': + if (rc_path_compare_extension(ext, "pce")) + { + iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; + } + break; + + case 'r': + if (rc_path_compare_extension(ext, "rom")) + { + 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; + } + break; + + case 't': + if (rc_path_compare_extension(ext, "tap")) + { + iterator->consoles[0] = RC_CONSOLE_ORIC; + } + break; + + case 'v': + if (rc_path_compare_extension(ext, "vb")) + { + iterator->consoles[0] = RC_CONSOLE_VIRTUAL_BOY; + } + break; + + case 'w': + if (rc_path_compare_extension(ext, "wsc")) + { + iterator->consoles[0] = RC_CONSOLE_WONDERSWAN; + } + 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; + } + 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/dep/rcheevos/src/rhash/md5.c b/dep/rcheevos/src/rhash/md5.c new file mode 100644 index 000000000..c35d96c5e --- /dev/null +++ b/dep/rcheevos/src/rhash/md5.c @@ -0,0 +1,381 @@ +/* + 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 + +#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 (!((data - (const md5_byte_t *)0) & 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/dep/rcheevos/src/rhash/md5.h b/dep/rcheevos/src/rhash/md5.h new file mode 100644 index 000000000..698c995d8 --- /dev/null +++ b/dep/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/dep/rcheevos/src/rurl/url.c b/dep/rcheevos/src/rurl/url.c new file mode 100644 index 000000000..b6a4469b9 --- /dev/null +++ b/dep/rcheevos/src/rurl/url.c @@ -0,0 +1,373 @@ +#include "rc_url.h" + +#include "../rcheevos/rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +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, + "http://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%u", lboard_id, user_name, lboard_id); + md5_init(&state); + md5_append(&state, (unsigned char*)signature, (int)strlen(signature)); + md5_finish(&state, hash); + + written = snprintf( + buffer, + size, + "http://retroachievements.org/dorequest.php?r=submitlbentry&u=%s&t=%s&i=%u&s=%d&v=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + 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, + "http://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, + "http://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[64]; + 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, + "http://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, + "http://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, + "http://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, + "http://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 = sprintf(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 = "http://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); + 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; +} diff --git a/duckstation.sln b/duckstation.sln index c4e37c84c..e4ee12a20 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -61,6 +61,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-nogui", "src\du EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-sdl", "src\duckstation-sdl\duckstation-sdl.vcxproj", "{DAA8F93D-9C17-4DE2-BD0B-57891E0FF0D9}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rcheevos", "dep\rcheevos\rcheevos.vcxproj", "{4BA0A6D4-3AE1-42B2-9347-096FD023FF64}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -741,6 +743,30 @@ Global {DAA8F93D-9C17-4DE2-BD0B-57891E0FF0D9}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64 {DAA8F93D-9C17-4DE2-BD0B-57891E0FF0D9}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32 {DAA8F93D-9C17-4DE2-BD0B-57891E0FF0D9}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Debug|ARM64.Build.0 = Debug|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Debug|x64.ActiveCfg = Debug|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Debug|x64.Build.0 = Debug|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Debug|x86.ActiveCfg = Debug|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Debug|x86.Build.0 = Debug|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.DebugFast|ARM64.ActiveCfg = DebugFast|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.DebugFast|ARM64.Build.0 = DebugFast|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.DebugFast|x64.Build.0 = DebugFast|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.DebugFast|x86.ActiveCfg = DebugFast|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.DebugFast|x86.Build.0 = DebugFast|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Release|ARM64.ActiveCfg = Release|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Release|ARM64.Build.0 = Release|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Release|x64.ActiveCfg = Release|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Release|x64.Build.0 = Release|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Release|x86.ActiveCfg = Release|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.Release|x86.Build.0 = Release|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.ReleaseLTCG|ARM64.Build.0 = ReleaseLTCG|ARM64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32 + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -765,6 +791,7 @@ Global {9C8DDEB0-2B8F-4F5F-BA86-127CDF27F035} = {BA490C0E-497D-4634-A21E-E65012006385} {8906836E-F06E-46E8-B11A-74E5E8C7B8FB} = {BA490C0E-497D-4634-A21E-E65012006385} {39F0ADFF-3A84-470D-9CF0-CA49E164F2F3} = {BA490C0E-497D-4634-A21E-E65012006385} + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} = {BA490C0E-497D-4634-A21E-E65012006385} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {26E40B32-7C1D-48D0-95F4-1A500E054028}